diff --git a/FTP/FTPClient.cpp b/FTP/FTPClient.cpp index 849eab6..b6264f2 100644 --- a/FTP/FTPClient.cpp +++ b/FTP/FTPClient.cpp @@ -146,12 +146,12 @@ bool CFTPClient::CleanupSession() { * @param [in] fnCallback callback to progress function * */ -void CFTPClient::SetProgressFnCallback(void *pOwner, const ProgressFnCallback &fnCallback) { +void CFTPClient::SetProgressFnCallback(void *pOwner, const ProgressFnCallback &fnCallback, const bool enable /*= true*/) { m_ProgressStruct.pOwner = pOwner; m_fnProgressCallback = fnCallback; m_ProgressStruct.pCurl = m_pCurlSession; m_ProgressStruct.dLastRunTime = 0; - m_bProgressCallbackSet = true; + m_bProgressCallbackSet = enable; } /** @@ -166,7 +166,7 @@ void CFTPClient::SetProxy(const std::string &strProxy) { std::string strUri = strProxy; std::transform(strUri.begin(), strUri.end(), strUri.begin(), ::toupper); - if (strUri.compare(0, 4, "HTTP") != 0) + if (strUri.compare(0, 5, "HTTP:") != 0) m_strProxy = "http://" + strProxy; else m_strProxy = strProxy; @@ -208,7 +208,7 @@ std::string CFTPClient::ParseURL(const std::string &strRemoteFile) const { // boost::to_upper(strUri); std::transform(strUri.begin(), strUri.end(), strUri.begin(), ::toupper); - if (strUri.compare(0, 3, "FTP") != 0 && strUri.compare(0, 4, "SFTP") != 0) { + if (strUri.compare(0, 4, "FTP:") != 0 && strUri.compare(0, 5, "SFTP:") != 0) { switch (m_eFtpProtocol) { case FTP_PROTOCOL::FTP: case FTP_PROTOCOL::FTPES: @@ -532,8 +532,8 @@ bool CFTPClient::Info(const std::string &strRemoteFile, struct FileInfo &oFileIn } res = curl_easy_getinfo(m_pCurlSession, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &oFileInfo.dFileSize); - if (CURLE_OK == res && oFileInfo.dFileSize > 0.0) { - bRes = true; + if (CURLE_OK != res || oFileInfo.dFileSize < 0.0) { + bRes = false; } } else if (m_eSettingsFlags & ENABLE_LOG) m_oLog(StringFormat(LOG_ERROR_CURL_FILETIME_FORMAT, strRemoteFile.c_str(), res, curl_easy_strerror(res))); @@ -781,6 +781,90 @@ bool CFTPClient::DownloadWildcard(const std::string &strLocalDir, const std::str return bRet; } +/** + * @brief uploads a user data using readFn to a remote folder. + * + * @param [in] readFn Reading function, corresponds to CURLOPT_READFUNCTION. + * @param [in] userData user data passed to readFn as last parameter. + * @param [in] strRemoteFile Complete URN of the remote location (with the file + * name) encoded in UTF-8 format. + * @param [in] bCreateDir Enable or disable creation of remote missing + * directories contained in the URN. + * + * @retval true Data successfully uploaded. + * @retval false Data couldn't be uploaded. Check the log messages for more + * information. + */ +bool CFTPClient::UploadFile(CFTPClient::CurlReadFn readFn, void *userData, const std::string &strRemoteFile, + const bool &bCreateDir, curl_off_t fileSize) const { + if (readFn == nullptr || strRemoteFile.empty()) + return false; + + if (!m_pCurlSession) { + if (m_eSettingsFlags & ENABLE_LOG) m_oLog(LOG_ERROR_CURL_NOT_INIT_MSG); + + return false; + } + // Reset is mandatory to avoid bad surprises + curl_easy_reset(m_pCurlSession); + + std::string strLocalRemoteFile = ParseURL(strRemoteFile); + + bool bRes = false; + + /* specify target */ + curl_easy_setopt(m_pCurlSession, CURLOPT_URL, strLocalRemoteFile.c_str()); + + /* we want to use our own read function */ + curl_easy_setopt(m_pCurlSession, CURLOPT_READFUNCTION, readFn); + + /* now specify which file to upload */ + curl_easy_setopt(m_pCurlSession, CURLOPT_READDATA, userData); + + /* Set the size of the file to upload (optional). If you give a *_LARGE + option you MUST make sure that the type of the passed-in argument is a + curl_off_t. If you use CURLOPT_INFILESIZE (without _LARGE) you must + make sure that to pass in a type 'long' argument. */ + curl_easy_setopt(m_pCurlSession, CURLOPT_INFILESIZE_LARGE, fileSize); + + /* enable uploading */ + curl_easy_setopt(m_pCurlSession, CURLOPT_UPLOAD, 1L); + + if (bCreateDir) curl_easy_setopt(m_pCurlSession, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR); + + CURLcode res = Perform(); + + if (res != CURLE_OK) { + if (m_eSettingsFlags & ENABLE_LOG) + m_oLog(StringFormat(LOG_ERROR_CURL_UPLOAD_FORMAT, strRemoteFile.c_str(), res, curl_easy_strerror(res))); + } else + bRes = true; + + return bRes; +} + +/** + * @brief uploads data from a stream to a remote folder. + * + * @param [in] inputStream Stream containing data which will be uploaded. + * @param [in] strRemoteFile Complete URN of the remote location (with the file + * name) encoded in UTF-8 format. + * @param [in] bCreateDir Enable or disable creation of remote missing + * directories contained in the URN. + * @param [in] fileSize + * + * @retval true Successfully uploaded the inputStream. + * @retval false The inputStream couldn't be uploaded. Check the log messages for more + * information. + */ +bool CFTPClient::UploadFile(std::istream &inputStream, const std::string &strRemoteFile, const bool &bCreateDir, + curl_off_t fileSize) const { + if ( !inputStream ) + return false; + + return UploadFile(ReadFromStreamCallback, static_cast(&inputStream), strRemoteFile, bCreateDir, fileSize); +} + /** * @brief uploads a local file to a remote folder. * @@ -807,16 +891,7 @@ bool CFTPClient::DownloadWildcard(const std::string &strLocalDir, const std::str bool CFTPClient::UploadFile(const std::string &strLocalFile, const std::string &strRemoteFile, const bool &bCreateDir) const { if (strLocalFile.empty() || strRemoteFile.empty()) return false; - if (!m_pCurlSession) { - if (m_eSettingsFlags & ENABLE_LOG) m_oLog(LOG_ERROR_CURL_NOT_INIT_MSG); - - return false; - } - // Reset is mandatory to avoid bad surprises - curl_easy_reset(m_pCurlSession); - std::ifstream InputFile; - std::string strLocalRemoteFile = ParseURL(strRemoteFile); struct stat file_info; bool bRes = false; @@ -837,35 +912,7 @@ bool CFTPClient::UploadFile(const std::string &strLocalFile, const std::string & return false; } - /* specify target */ - curl_easy_setopt(m_pCurlSession, CURLOPT_URL, strLocalRemoteFile.c_str()); - - /* we want to use our own read function */ - curl_easy_setopt(m_pCurlSession, CURLOPT_READFUNCTION, ReadFromFileCallback); - - /* now specify which file to upload */ - curl_easy_setopt(m_pCurlSession, CURLOPT_READDATA, &InputFile); - - /* Set the size of the file to upload (optional). If you give a *_LARGE - option you MUST make sure that the type of the passed-in argument is a - curl_off_t. If you use CURLOPT_INFILESIZE (without _LARGE) you must - make sure that to pass in a type 'long' argument. */ - curl_easy_setopt(m_pCurlSession, CURLOPT_INFILESIZE_LARGE, static_cast(file_info.st_size)); - - /* enable uploading */ - curl_easy_setopt(m_pCurlSession, CURLOPT_UPLOAD, 1L); - - if (bCreateDir) curl_easy_setopt(m_pCurlSession, CURLOPT_FTP_CREATE_MISSING_DIRS, CURLFTP_CREATE_DIR); - - // TODO add the possibility to rename the file upon upload finish.... - - CURLcode res = Perform(); - - if (res != CURLE_OK) { - if (m_eSettingsFlags & ENABLE_LOG) - m_oLog(StringFormat(LOG_ERROR_CURL_UPLOAD_FORMAT, strLocalFile.c_str(), res, curl_easy_strerror(res))); - } else - bRes = true; + bRes = UploadFile(InputFile, strRemoteFile, bCreateDir, file_info.st_size); } InputFile.close(); @@ -919,7 +966,7 @@ bool CFTPClient::AppendFile(const std::string &strLocalFile, const size_t fileOf curl_easy_setopt(m_pCurlSession, CURLOPT_URL, strLocalRemoteFile.c_str()); /* we want to use our own read function */ - curl_easy_setopt(m_pCurlSession, CURLOPT_READFUNCTION, ReadFromFileCallback); + curl_easy_setopt(m_pCurlSession, CURLOPT_READFUNCTION, ReadFromStreamCallback); /* now specify which file to upload */ curl_easy_setopt(m_pCurlSession, CURLOPT_READDATA, &InputFile); @@ -1161,13 +1208,13 @@ size_t CFTPClient::WriteToMemory(void *buff, size_t size, size_t nmemb, void *da * @param ptr pointer of max size (size*nmemb) to write data to it * @param size size parameter * @param nmemb memblock parameter - * @param stream pointer to user data (file stream) + * @param stream pointer to user data (input stream) * * @return (size * nmemb) */ -size_t CFTPClient::ReadFromFileCallback(void *ptr, size_t size, size_t nmemb, void *stream) { - std::ifstream *pFileStream = reinterpret_cast(stream); - if (pFileStream->is_open()) { +size_t CFTPClient::ReadFromStreamCallback(void *ptr, size_t size, size_t nmemb, void *stream) { + auto *pFileStream = reinterpret_cast(stream); + if (!pFileStream->fail()) { pFileStream->read(reinterpret_cast(ptr), size * nmemb); return pFileStream->gcount(); } diff --git a/FTP/FTPClient.h b/FTP/FTPClient.h index 76f3854..8625630 100644 --- a/FTP/FTPClient.h +++ b/FTP/FTPClient.h @@ -42,6 +42,7 @@ class CFTPClient { // Public definitions using ProgressFnCallback = std::function; using LogFnCallback = std::function; + using CurlReadFn = size_t (*) (void *, size_t, size_t, void *); // Used to download many items at once struct WildcardTransfersCallbackData { @@ -118,7 +119,7 @@ class CFTPClient { CFTPClient &operator=(CFTPClient &&) = delete; // Setters - Getters (for unit tests) - void SetProgressFnCallback(void *pOwner, const ProgressFnCallback &fnCallback); + void SetProgressFnCallback(void *pOwner, const ProgressFnCallback &fnCallback, const bool enable = true); void SetProxy(const std::string &strProxy); void SetProxyUserPwd(const std::string &strProxyUserPwd); inline void SetTimeout(const int &iTimeout) { m_iCurlTimeout = iTimeout; } @@ -164,6 +165,12 @@ class CFTPClient { bool DownloadWildcard(const std::string &strLocalDir, const std::string &strRemoteWildcard) const; + bool UploadFile(CurlReadFn readFn, void *userData, const std::string &strRemoteFile, const bool &bCreateDir = false, + curl_off_t fileSize = -1) const; + + bool UploadFile(std::istream &inputStream, const std::string &strRemoteFile, const bool &bCreateDir = false, + curl_off_t fileSize = -1) const; + bool UploadFile(const std::string &strLocalFile, const std::string &strRemoteFile, const bool &bCreateDir = false) const; bool AppendFile(const std::string &strLocalFile, const size_t fileOffset, const std::string &strRemoteFile, @@ -196,7 +203,7 @@ class CFTPClient { // Curl callbacks static size_t WriteInStringCallback(void *ptr, size_t size, size_t nmemb, void *data); static size_t WriteToFileCallback(void *ptr, size_t size, size_t nmemb, void *data); - static size_t ReadFromFileCallback(void *ptr, size_t size, size_t nmemb, void *stream); + static size_t ReadFromStreamCallback(void *ptr, size_t size, size_t nmemb, void *stream); static size_t ThrowAwayCallback(void *ptr, size_t size, size_t nmemb, void *data); static size_t WriteToMemory(void *ptr, size_t size, size_t nmemb, void *data); diff --git a/README.md b/README.md index d39c05e..615ffd0 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ Example : (Run only FTP tests) ftp=yes ; SFTP tests (not implemented) are disabled sftp=no -; HTTP Proxy tests are disabled +; HTTP Proxy tests are enabled http-proxy=yes [ftp] diff --git a/TestFTP/main.cpp b/TestFTP/main.cpp index bd7c0c2..843285c 100644 --- a/TestFTP/main.cpp +++ b/TestFTP/main.cpp @@ -461,6 +461,51 @@ TEST_F(FTPClientTest, TestUploadAndRemoveFile) { std::cout << "FTP tests are disabled !" << std::endl; } +TEST_F(FTPClientTest, TestUploadStreamAndRemove) { + if (FTP_TEST_ENABLED) { + // to display a beautiful progress bar on console + m_pFTPClient->SetProgressFnCallback(m_pFTPClient.get(), &TestUPProgressCallback); + + std::ostringstream ssTimestamp; + TimeStampTest(ssTimestamp); + + // create dummy string stream + std::stringstream ofTestUpload; + ASSERT_TRUE(static_cast(ofTestUpload)); + + ofTestUpload << "Unit Test 'TestUploadStreamAndRemove' executed on " + ssTimestamp.str() + "\n" + + "This file is uploaded via FTPClient-C++ API.\n" + + "If this file exists, that means that the unit test is passed.\n"; + ASSERT_TRUE(static_cast(ofTestUpload)); + + // Upload steam + ASSERT_TRUE(static_cast(ofTestUpload)); + ASSERT_TRUE(m_pFTPClient->UploadFile(ofTestUpload, SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt")); + + std::cout << std::endl; + + // Download the uploaded stream into a vector of bytes + { + std::vector uploadedFileBytes; + EXPECT_TRUE(m_pFTPClient->DownloadFile(SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt", uploadedFileBytes)); + + std::cout << std::endl; + + /* check the SHA1 sum of the uploaded file */ + std::string contentStr = ofTestUpload.str(); + std::vector contentBytes(contentStr.begin(), contentStr.end()); + std::string expectedSha1Sum = sha1sum(contentBytes); + std::string resultSha1Sum = sha1sum(uploadedFileBytes); + + EXPECT_TRUE(expectedSha1Sum == resultSha1Sum); + } + + // Remove file + ASSERT_TRUE(m_pFTPClient->RemoveFile(SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt")); + } else + std::cout << "FTP tests are disabled !" << std::endl; +} + #ifdef WINDOWS TEST_F(FTPClientTest, TestUploadFileNameWithAccents) { if (FTP_TEST_ENABLED) { @@ -815,6 +860,51 @@ TEST_F(SFTPClientTest, TestUploadAndRemoveFile) { std::cout << "SFTP tests are disabled !" << std::endl; } +TEST_F(SFTPClientTest, TestUploadStreamAndRemove) { + if (SFTP_TEST_ENABLED) { + // to display a beautiful progress bar on console + m_pSFTPClient->SetProgressFnCallback(m_pSFTPClient.get(), &TestUPProgressCallback); + + std::ostringstream ssTimestamp; + TimeStampTest(ssTimestamp); + + // create dummy string stream + std::stringstream ofTestUpload; + ASSERT_TRUE(static_cast(ofTestUpload)); + + ofTestUpload << "Unit Test 'TestUploadStreamAndRemove' executed on " + ssTimestamp.str() + "\n" + + "This file is uploaded via FTPClient-C++ API.\n" + + "If this file exists, that means that the unit test is passed.\n"; + ASSERT_TRUE(static_cast(ofTestUpload)); + + // Upload steam + ASSERT_TRUE(static_cast(ofTestUpload)); + ASSERT_TRUE(m_pSFTPClient->UploadFile(ofTestUpload, SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt")); + + std::cout << std::endl; + + // Download the uploaded stream into a vector of bytes + { + std::vector uploadedFileBytes; + EXPECT_TRUE(m_pSFTPClient->DownloadFile(SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt", uploadedFileBytes)); + + std::cout << std::endl; + + /* check the SHA1 sum of the uploaded file */ + std::string contentStr = ofTestUpload.str(); + std::vector contentBytes(contentStr.begin(), contentStr.end()); + std::string expectedSha1Sum = sha1sum(contentBytes); + std::string resultSha1Sum = sha1sum(uploadedFileBytes); + + EXPECT_TRUE(expectedSha1Sum == resultSha1Sum); + } + + // Remove file + ASSERT_TRUE(m_pSFTPClient->RemoveFile(SFTP_REMOTE_UPLOAD_FOLDER + "test_upload_stream.txt")); + } else + std::cout << "SFTP tests are disabled !" << std::endl; +} + // TODO : this unit test can't be executed elsewhere #if 0 TEST_F(SFTPClientTest, TestAppend /*AndRemoveFile*/) { diff --git a/TestFTP/simpleini/SimpleIni.h b/TestFTP/simpleini/SimpleIni.h index 0da9414..8f0d322 100644 --- a/TestFTP/simpleini/SimpleIni.h +++ b/TestFTP/simpleini/SimpleIni.h @@ -319,7 +319,7 @@ class CSimpleIniTempl { #endif /** Strict less ordering by name of key only */ - struct KeyOrder : std::binary_function { + struct KeyOrder { bool operator()(const Entry &lhs, const Entry &rhs) const { const static SI_STRLESS isLess = SI_STRLESS(); return isLess(lhs.pItem, rhs.pItem); @@ -327,7 +327,7 @@ class CSimpleIniTempl { }; /** Strict less ordering by order, and then name of key */ - struct LoadOrder : std::binary_function { + struct LoadOrder { bool operator()(const Entry &lhs, const Entry &rhs) const { if (lhs.nOrder != rhs.nOrder) { return lhs.nOrder < rhs.nOrder; diff --git a/TestFTP/test_utils.cpp b/TestFTP/test_utils.cpp index d491bc0..e198e2e 100644 --- a/TestFTP/test_utils.cpp +++ b/TestFTP/test_utils.cpp @@ -36,7 +36,7 @@ std::mutex g_mtxConsoleMutex; bool GlobalTestInit(const std::string& strConfFile) { CSimpleIniA ini; - ini.LoadFile(strConfFile.c_str()); + if (ini.LoadFile(strConfFile.c_str()) != SI_Error::SI_OK) return false; std::string strTmp; strTmp = ini.GetValue("tests", "ftp", "");