Jelajahi Sumber

V1.6.7
1、完成了对CurlFtp中FTP功能的修改,SFTP还不能使用

Apple 5 hari lalu
induk
melakukan
2ee71e4b6e
2 mengubah file dengan 634 tambahan dan 187 penghapusan
  1. 613 176
      module/CurlFtp/CurlFtp.cpp
  2. 21 11
      module/CurlFtp/CurlFtp.h

+ 613 - 176
module/CurlFtp/CurlFtp.cpp

@@ -347,6 +347,54 @@ static int progress_callback(void *clientp,
     return 0;
 }
 
+/* ======================== 通配符列表示例所需回调 ======================== */
+/* 分块开始回调:收集文件信息并跳过实际传输 */
+static long chunk_bgn_cb(const void* transfer_info, void* ptr, int /*remains*/)
+{
+    const struct curl_fileinfo* finfo = static_cast<const struct curl_fileinfo*>(transfer_info);
+    if(finfo == nullptr || ptr == nullptr)
+    {
+        return CURL_CHUNK_BGN_FUNC_OK;
+    }
+
+    std::vector<CF_FileInfo>* out = static_cast<std::vector<CF_FileInfo>*>(ptr);
+
+    // 跳过 "." 和 ".." 等无效名称
+    if(finfo->filename && (std::string(finfo->filename) == "." || std::string(finfo->filename) == ".."))
+    {
+        return CURL_CHUNK_BGN_FUNC_SKIP;
+    }
+
+    CF_FileInfo fi;
+    if(finfo->filetype == CURLFILETYPE_DIRECTORY)
+    {
+        fi.type = CF_FileType::DIR;
+    }
+    else if(finfo->filetype == CURLFILETYPE_FILE)
+    {
+        fi.type = CF_FileType::FILE;
+    }
+    else
+    {
+        // 其他类型(链接、设备等)不处理
+        return CURL_CHUNK_BGN_FUNC_SKIP;
+    }
+
+    fi.size = static_cast<uint64_t>(finfo->size);
+    fi.name = (finfo->filename ? std::string(finfo->filename) : std::string());
+
+    out->push_back(std::move(fi));
+
+    // 仅做枚举,不实际下载数据
+    return CURL_CHUNK_BGN_FUNC_SKIP;
+}
+
+/* 分块结束回调:无特别处理 */
+static long chunk_end_cb(void* /*ptr*/)
+{
+    return CURL_CHUNK_END_FUNC_OK;
+}
+
 
 /* 使用Windows API进行编码转换 */
 #if defined(_WIN32)
@@ -389,7 +437,7 @@ const std::regex parseReg4(R"(^-.*$)");
  */
 static bool parseFileInfo(const std::string& strSrc, std::vector<CF_FileInfo>& fileInfo)
 {
-    SPDLOG_INFO("SRC FILE INFO:\n{}", strSrc);
+    SPDLOG_DEBUG("SRC FILE INFO:\n{}", strSrc);
 #if defined(_WIN32)
     // auto str1 = GBToUTF8(strSrc.c_str());
     std::string str2 = strSrc;
@@ -445,6 +493,30 @@ static bool parseFileInfo(const std::string& strSrc, std::vector<CF_FileInfo>& f
 }
 
 
+/* 辅助:拆分绝对路径为 父目录(以/结尾) + 基名 */
+static void splitParentAndBasePath(const std::string& absPath, std::string& parentDir, std::string& fileName)
+{
+    auto pos = absPath.find_last_of('/');
+    if(pos == std::string::npos)
+    {
+        parentDir = "/";
+        fileName = absPath;
+        return;
+    }
+    parentDir = absPath.substr(0, pos + 1);
+    fileName = absPath.substr(pos + 1);
+    if(parentDir.empty()) parentDir = "/";
+}
+
+/* 去掉最后的“/” */
+std::string trimTrailingSlash(std::string s) 
+{
+    if (!s.empty() && s.back() == '/') 
+        s.pop_back();
+    return s;
+}
+
+
 /* ==================================================================================
  * *********************************** 成员函数 *************************************
  * ================================================================================== */
@@ -520,6 +592,25 @@ void CurlFtp::enableCurlDebug(bool isPrint)
 }
 
 
+/* 获取列表,包括文件和文件夹 */
+bool CurlFtp::getList(std::string dir, std::vector<CF_FileInfo>& fileInfoList)
+{
+    if(m_IP.empty())
+    {
+        LOG_WARN("IP or port is empty");
+        return false;
+    }
+
+    resetCurl(m_curl);
+
+    /* 获取文件信息 */
+    listByWildcard(m_curl, dir, fileInfoList);
+
+    // curl_easy_cleanup(curl);
+    
+    return true;
+}
+
 /* 列出文件列表 */
 bool CurlFtp::getFileList(std::string dir, std::vector<std::string>& fileList)
 {
@@ -533,7 +624,7 @@ bool CurlFtp::getFileList(std::string dir, std::vector<std::string>& fileList)
 
     std::vector<CF_FileInfo> listInfo;
     /* 获取文件信息 */
-    listAll(m_curl, dirTmp, listInfo);
+    listByWildcard(m_curl, dirTmp, listInfo);
     for(const CF_FileInfo& fi : listInfo)
     {
         // LOG_INFO("type = {}, size = {}, name = {}", (int)fi.type, fi.size, fi.name);
@@ -570,7 +661,7 @@ bool CurlFtp::getDirList(std::string dir, std::vector<std::string>& dirList)
 
     std::vector<CF_FileInfo> listInfo;
     /* 获取文件信息 */
-    listAll(m_curl, dirTmp, listInfo);
+    listByWildcard(m_curl, dirTmp, listInfo);
     for(const CF_FileInfo& fi : listInfo)
     {
         // LOG_INFO("type = {}, size = {}, name = {}", (int)fi.type, fi.size, fi.name);
@@ -594,7 +685,7 @@ bool CurlFtp::getDirList(std::string dir, std::vector<std::string>& dirList)
  * @return true 
  * @return false 
  */
-bool CurlFtp::isDirExist(const std::string& dir)
+bool CurlFtp::existsDir(const std::string& dir)
 {
     LOG_DEBUG("Check remote dir:" << dir);
     if(m_ftpUrl.empty())
@@ -606,99 +697,14 @@ bool CurlFtp::isDirExist(const std::string& dir)
     {
         return true;
     }
-    bool result = false;
-    if(m_isSftp)
-    {
-        result = checkSftpDirExist(dir);
-    }else {
-        result = checkFtpDirExist(dir);
-    }
     
-    return result;
+    return checkFtpDirExist(dir);
 }
 
 
-/**
- * @brief 创建FTP文件夹,只能创建一层文件夹
- * 
- * @param ftpDir 文件夹路径
- * @return true 文件夹创建成功或者文件夹存在
- * @return false 
- */
-bool CurlFtp::createDirectory(const std::string& ftpDir)
-{
-    if(m_ftpUrl.empty())
-    {
-        LOG_ERROR("ftpUrl is empty");
-        return false;
-    }
-    /* 先检查FTP文件夹是否存在,如果存在直接返回true */
-    if(isDirExist(ftpDir))
-    {
-        return true;
-    }
-
-    /* 检查传入的文件夹格式 */
-    auto dirTmp = checkDirPath(ftpDir);
-
-    // CURL *curl = curl_easy_init();
-    // if(curl == nullptr)
-    // {
-    //     LOG_ERROR("Create FTP DIR, curl init failed !");
-    //     return false;
-    // }
-    // resetCurl(m_curl);
-    setCommonCurlOptions(m_curl, m_ftpUrl, 30);
-
-    // curl_easy_setopt(m_curl, CURLOPT_URL, m_ftpUrl.c_str());
-    // curl_easy_setopt(m_curl, CURLOPT_USERNAME, m_username.c_str());
-    // curl_easy_setopt(m_curl, CURLOPT_PASSWORD, m_password.c_str());
-    /* 创建FTP头信息 */
-    struct curl_slist *headerlist = NULL;
-    std::string mkdir;
-    if(m_isSftp)
-    {
-        mkdir = "MKDIR " + dirTmp;
-    }else {
-        mkdir = "MKD " + dirTmp;
-    }
-    headerlist = curl_slist_append(headerlist, mkdir.c_str());
-    /* 设置FTP命令行选项 */
-    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
-    /* 不包含实体 */
-    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
-    /* 启用跟随重定向 */
-    // curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1L);
-    /* 设置超时时间 */
-    // curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, 30L);
-    // curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, 30L);
-
-    /* 设置SFTP */
-    // setSftp(m_curl);
-
-    // if(m_enableCurlDebug)
-    // {
-    //     /* 启用调试信息 */
-    //     curl_easy_setopt(m_curl, CURLOPT_VERBOSE, 1L);
-    // }
-    // 启用持久连接
-    // curl_easy_setopt(m_curl, CURLOPT_TCP_KEEPALIVE, 1L);
-
-    LOG_DEBUG("Create remote dir: " << dirTmp);
-    // CURLcode res = curl_easy_perform(curl);
-    bool ret = performCurl(m_curl);
-    if(!ret)
-    {
-        LOG_ERROR("Failed to create remote Dir");
-    }
-
-    // curl_easy_cleanup(curl);
-    curl_slist_free_all(headerlist);
-    return ret;
-}
 
 /* 创建FTP文件夹,递归创建 */
-bool CurlFtp::createDirectories(const std::string& ftpDir)
+bool CurlFtp::createDirectory(const std::string& ftpDir)
 {
     /* 检查路径格式,并去掉第一个 / */
     std::string ftpDirTmp = checkDirPath(ftpDir);
@@ -728,7 +734,7 @@ bool CurlFtp::createDirectories(const std::string& ftpDir)
     for(const std::string& dir : dirList)
     {
         // LOG_DEBUG("Create dir: {}", dir);
-        if(!createDirectory(dir))
+        if(!createOneDirectory(dir))
         {
             LOG_ERROR("Failed to create dir: " << dir);
             return false;
@@ -741,7 +747,10 @@ bool CurlFtp::createDirectories(const std::string& ftpDir)
 
 
 
-/* 删除文件 */
+
+
+/* 删除文件,文件后面不能带有“/”否则会报错
+    删除方式,先 CWD 到该文件目录,再执行 DELE 删除命令 */
 bool CurlFtp::deleteFile(const std::string& remoteFile)
 {
     if(m_ftpUrl.empty())
@@ -749,12 +758,78 @@ bool CurlFtp::deleteFile(const std::string& remoteFile)
         LOG_ERROR("ftpUrl is empty");
         return false;
     }
+    /* 规范化文件路径(以/开头,且不以/结尾) */
+    std::string filePath = checkFilePath(remoteFile);
+    if(filePath.empty())
+    {
+        return false;
+    }
 
+    /* 切到父目录后再删除基名(兼容更多服务器) */
+    std::string parentDir, fileName;
+    splitParentAndBasePath(filePath, parentDir, fileName);
+    if(fileName.empty())
+    {
+        LOG_ERROR("Delete file base name is empty: " << filePath);
+        return false;
+    }
 
-    return true;
+    std::string parentUrl = m_ftpUrl + parentDir; // parentDir 以'/'结尾
+    if(!setCommonCurlOptions(m_curl, parentUrl, 10))
+    {
+        return false;
+    }
+
+    struct curl_slist *headerlist = nullptr;
+    std::string deleteCmd;
+    if(m_isSftp)
+    {
+        deleteCmd = "rm " + fileName;   // sftp 使用 rm
+    }
+    else
+    {
+        deleteCmd = "DELE " + fileName; // ftp 使用 DELE
+    }
+    headerlist = curl_slist_append(headerlist, deleteCmd.c_str());
+    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+    LOG_DEBUG("Delete remote file: " << filePath << ", parent: " << parentDir);
+    bool ret = performCurl(m_curl);
+    curl_slist_free_all(headerlist);
+    if(ret)
+    {
+        return true;
+    }
+
+    /* 回退:尝试使用绝对路径删除(有些服务器允许) */
+    LOG_WARN("Delete by basename failed, try absolute path: " << filePath);
+    std::string rootUrl = m_ftpUrl + "/";
+    if(!setCommonCurlOptions(m_curl, rootUrl, 10))
+    {
+        return false;
+    }
+    headerlist = nullptr;
+    if(m_isSftp)
+    {
+        deleteCmd = "rm " + filePath;
+    }
+    else
+    {
+        deleteCmd = "DELE " + filePath;
+    }
+    headerlist = curl_slist_append(headerlist, deleteCmd.c_str());
+    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+    bool ret2 = performCurl(m_curl);
+    curl_slist_free_all(headerlist);
+    if(!ret2)
+    {
+        LOG_ERROR("Failed to delete remote file: " << filePath);
+    }
+    return ret2;
 }
 
-/* 删除文件夹 */
+/* 删除文件夹,递归删除 */
 bool CurlFtp::deleteDirectory(const std::string& ftpDir)
 {
     if(m_ftpUrl.empty())
@@ -762,8 +837,120 @@ bool CurlFtp::deleteDirectory(const std::string& ftpDir)
         LOG_ERROR("ftpUrl is empty");
         return false;
     }
+    /* 规范化目录路径,并去掉末尾斜杠 */
+    std::string dirWithSlash = checkDirPath(ftpDir);
+    std::string dirNoSlash = trimTrailingSlash(dirWithSlash);
+    if(dirNoSlash.empty() || dirNoSlash == "/")
+    {
+        LOG_ERROR("Refuse to delete root dir: " << dirNoSlash);
+        return false;
+    }
 
-    return true;
+    /* 递归删除:先删除内部所有文件和子目录,再删除自身 */
+    std::vector<CF_FileInfo> vecChildren;
+    // 使用通配符列举该目录下的所有条目
+    if(!listByWildcard(m_curl, dirWithSlash, vecChildren))
+    {
+        LOG_WARN("List children failed before delete: " << dirWithSlash);
+    }
+    // 先删文件, 先切换到该目录,避免某些服务器删除失败
+    if(!changeToDir(dirWithSlash))
+    {
+        LOG_ERROR("ChangeToDir failed: " << dirWithSlash);
+        return false;
+    }
+    for(const auto& fi : vecChildren)
+    {
+        /* 软连接这些也用文件删除,不过目前貌似无法获取到软连接 */
+        if(fi.type != CF_FileType::DIR)
+        {
+            if(fi.name.empty() || fi.name == "." || fi.name == "..")
+            {
+                continue;
+            }
+            if(!deleteFileRelative(fi.name))
+            {
+                LOG_ERROR("Failed to delete file: " << fi.name);
+                // 回退:尝试使用绝对路径删除
+                std::string absFile = dirWithSlash + fi.name;
+                if(!deleteFile(absFile))
+                {
+                    return false;
+                }
+            }
+        }
+    }
+    // 再递归删子目录
+    for(const auto& fi : vecChildren)
+    {
+        if(fi.type == CF_FileType::DIR)
+        {
+            if(fi.name.empty() || fi.name == "." || fi.name == "..")
+            {
+                continue;
+            }
+            std::string childDir = dirWithSlash + fi.name;
+            std::string childNorm = checkDirPath(childDir);
+            if(childNorm == dirWithSlash)
+            {
+                LOG_WARN("Skip self-recursion: " << childNorm);
+                continue;
+            }
+            if(!deleteDirectory(childNorm))
+            {
+                LOG_ERROR("Failed to delete subdir: " << childDir);
+                return false;
+            }
+        }
+    }
+
+    /* 删除自身目录,走到这一步,这个文件夹内的所有文件都被删除了 */
+    std::string parentDir, baseName;
+    splitParentAndBasePath(dirNoSlash, parentDir, baseName);
+    if(baseName.empty())
+    {
+        LOG_ERROR("Delete directory base name is empty: " << dirNoSlash);
+        return false;
+    }
+    /* 切换到父文件夹并删除当前文件夹 */
+    if(!changeToDir(parentDir))
+    {
+        LOG_ERROR("ChangeToDir parent failed: " << parentDir);
+        return false;
+    }
+    if(deleteDirectoryRelative(baseName))
+    {
+        return true;
+    }
+
+    /* 绝对路径删除回退(部分服务器支持) */
+    {
+        std::string rootUrl = m_ftpUrl + "/";
+        if(!setCommonCurlOptions(m_curl, rootUrl, 10))
+        {
+            return false;
+        }
+        struct curl_slist *headerlist = nullptr;
+        std::string deleteCmd;
+        if(m_isSftp)
+        {
+            deleteCmd = std::string("rmdir ") + dirNoSlash;
+        }
+        else
+        {
+            deleteCmd = std::string("RMD ") + dirNoSlash;
+        }
+        headerlist = curl_slist_append(headerlist, deleteCmd.c_str());
+        curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+        curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+        bool ret2 = performCurl(m_curl);
+        curl_slist_free_all(headerlist);
+        if(!ret2)
+        {
+            LOG_ERROR("Failed to delete remote directory: " << dirNoSlash);
+        }
+        return ret2;
+    }
 }
 
 
@@ -926,11 +1113,11 @@ bool CurlFtp::uploadFile(const std::string& localFile, const std::string& remote
     std::string remoteDirTmp = remoteFileTmp.substr(0, remoteFileTmp.find_last_of("/"));
     // printf("uploadFile: %d\n", __LINE__);
     /* 检查远程FTP上的文件夹是否存在,不存在则创建 */
-    if(!isDirExist(remoteDirTmp))
+    if(!existsDir(remoteDirTmp))
     {
         if(isCreateDir)
         {
-            if(!createDirectories(remoteDirTmp))
+            if(!createDirectory(remoteDirTmp))
             {
                 // LOG_ERROR("Failed to create remote dir: {}", remoteFileTmp);
                 return false;
@@ -1030,11 +1217,11 @@ bool CurlFtp::uploadData(char* srcData, size_t size, const std::string& remoteFi
     std::string remoteFileTmp = checkFilePath(remoteFile);
     std::string remoteDirTmp = remoteFileTmp.substr(0, remoteFileTmp.find_last_of("/"));
     /* 检查远程FTP上的文件夹是否存在,不存在则创建 */
-    if(!isDirExist(remoteDirTmp))
+    if(!existsDir(remoteDirTmp))
     {
         if(isCreateDir)
         {
-            if(!createDirectories(remoteDirTmp))
+            if(!createDirectory(remoteDirTmp))
             {
                 // LOG_ERROR("Failed to create remote dir: {}", remoteFileTmp);
                 return false;
@@ -1101,7 +1288,59 @@ bool CurlFtp::uploadData(char* srcData, size_t size, const std::string& remoteFi
  * @return true 
  * @return false 
  */
-bool CurlFtp::listAll(CURL* curl, std::string dir, std::vector<CF_FileInfo>& fileInfoList)
+// bool CurlFtp::listAll(CURL* curl, std::string dir, std::vector<CF_FileInfo>& fileInfoList)
+// {
+//     if(m_IP.empty())
+//     {
+//         LOG_ERROR("IP is empty");
+//         return false;
+//     }
+
+//     // bool result = false;
+//     std::string strSrc;
+//     /* 先设置FTP地址 */
+//     std::string ftpUrl = m_ftpUrl + dir;
+//     if(!setCommonCurlOptions(curl, ftpUrl, 10))
+//     {
+//         return false;
+//     }
+
+//     /* 设置列出文件命令,只列出文件名称,不携带信息 */
+//     // curl_easy_setopt(m_curl, CURLOPT_DIRLISTONLY, 1L);
+//     /* 设置为文本传输 */
+//     curl_easy_setopt(curl, CURLOPT_TRANSFERTEXT, 1L);
+//     /* 设置回调函数 */
+//     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback);
+//     /* 设置需要写入的容器,回调函数的第四个参数 */
+//     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &strSrc);
+
+//     // 禁用被动模式,设置为0是禁用(如果需要)
+//     // curl_easy_setopt(curl, CURLOPT_FTP_USE_EPSV, 1L);
+//     /* 发送请求 */
+//     // CURLcode res = curl_easy_perform(curl);
+//     bool ret = performCurl(curl);
+//     if(!ret)
+//     {
+//         LOG_ERROR("Failed to get file listUrl = " << ftpUrl);
+//         return false;
+//     }
+//     /* 解析字符串 */
+//     parseFileInfo(strSrc, fileInfoList);
+
+//     return true;
+// }
+
+
+/**
+ * @brief 列出文件列表,这个需要的参数很多,无法设置成静态函数
+ * 
+ * @param curl CURL句柄
+ * @param dir 文件夹,相对路径,不带有IP和端口号
+ * @param fileList 返回值,文件列表
+ * @return true 
+ * @return false 
+ */
+bool CurlFtp::listByWildcard(CURL* curl, const std::string& dir, std::vector<CF_FileInfo>& out)
 {
     if(m_IP.empty())
     {
@@ -1109,36 +1348,33 @@ bool CurlFtp::listAll(CURL* curl, std::string dir, std::vector<CF_FileInfo>& fil
         return false;
     }
 
-    // bool result = false;
-    std::string strSrc;
-    /* 先设置FTP地址 */
-    std::string ftpUrl = m_ftpUrl + dir;
-    if(!setCommonCurlOptions(curl, m_ftpUrl + dir, 10))
+    /* 规范化目录并追加通配符 */
+    std::string dirWithSlash = checkDirPath(dir);   // 保证以'/'结束
+    std::string ftpUrl = m_ftpUrl + dirWithSlash + "*";
+
+    if(!setCommonCurlOptions(curl, ftpUrl, 10))
     {
         return false;
     }
 
-    /* 设置列出文件命令,只列出文件名称,不携带信息 */
-    // curl_easy_setopt(m_curl, CURLOPT_DIRLISTONLY, 1L);
-    /* 设置回调函数 */
-    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeStringCallback);
-    /* 设置需要写入的容器,回调函数的第四个参数 */
-    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &strSrc);
+    /* 启用通配符匹配并注册回调,仅收集信息不下载 */
+    curl_easy_setopt(m_curl, CURLOPT_WILDCARDMATCH, 1L);
+    curl_easy_setopt(m_curl, CURLOPT_CHUNK_BGN_FUNCTION, chunk_bgn_cb);
+    curl_easy_setopt(m_curl, CURLOPT_CHUNK_END_FUNCTION, chunk_end_cb);
+    curl_easy_setopt(m_curl, CURLOPT_CHUNK_DATA, &out);
 
-    // 禁用被动模式,设置为0是禁用(如果需要)
-    // curl_easy_setopt(curl, CURLOPT_FTP_USE_EPSV, 1L);
-    /* 发送请求 */
-    // CURLcode res = curl_easy_perform(curl);
-    bool ret = performCurl(curl);
-    if(!ret)
+    // FTP 文本模式对目录兼容性更好(SFTP 忽略该设置)
+    if(m_isSftp == false)
     {
-        LOG_ERROR("Failed to get file listUrl = " << ftpUrl);
-        return false;
+        curl_easy_setopt(m_curl, CURLOPT_TRANSFERTEXT, 1L);
     }
-    /* 解析字符串 */
-    parseFileInfo(strSrc, fileInfoList);
 
-    return true;
+    bool ret = performCurl(m_curl);
+    if(!ret)
+    {
+        LOG_ERROR("Wildcard list failed, Url = " << ftpUrl);
+    }
+    return ret;
 }
 
 
@@ -1192,10 +1428,9 @@ std::string CurlFtp::checkDirPath(const std::string& dir)
     std::regex reg1(R"(//)");
     dirTmp = std::regex_replace(dirTmp, reg1, "/");
     /* 检查是否以“/”结束 */
-    std::regex reg2(R"([.]*/$)");
-    if(!std::regex_match(dirTmp, reg2))
+    if (dirTmp.back() != '/')
     {
-        dirTmp = dirTmp + "/";
+        dirTmp.push_back('/');
     }
 
     return dirTmp;
@@ -1284,14 +1519,14 @@ bool CurlFtp::checkLocalDirExist(const std::string& localDir)
 bool CurlFtp::checkLocalFileExist(const std::string& localFile)
 {
     /* 去掉最后的‘/’ */
-    std::regex reg(R"([.]*/$)");
-    std::string localDirTmp = std::regex_replace(localFile, reg, "");
+    // std::regex reg(R"([.]*/$)");
+    std::string localDirTmp = trimTrailingSlash(localFile);
     /* 检查文件是否存在 */
     bool result = false;
 // #if (__cplusplus >= 201703L)
-//     result = std::filesystem::exists(localFile);
+//     result = std::filesystem::exists(localDirTmp);
 // #else
-    result = QFile(localFile.c_str()).exists();
+    result = QFile(localDirTmp.c_str()).exists();
 // #endif /* (__cplusplus >= 201703L) */
 
     return result;
@@ -1316,7 +1551,11 @@ bool CurlFtp::performCurl(CURL* curl)
             curl_easy_setopt(curl, CURLOPT_USERNAME, m_username.c_str());
             curl_easy_setopt(curl, CURLOPT_PASSWORD, m_password.c_str());
         } 
-        else
+        else if(res == CURLE_REMOTE_FILE_NOT_FOUND)
+        {
+            LOG_WARN("Remote file or directory not found, error code: " << (int)res << ", " << curl_easy_strerror(res));
+            return true;
+        } else
         {
             LOG_ERROR("Perform curl failed, error code: " << (int)res << ", " << curl_easy_strerror(res));
             LOG_ERROR("Retry times: " << (4 - retry));
@@ -1368,6 +1607,7 @@ bool CurlFtp::setSftp(CURL* curl)
 
 /**
  * @brief 检查FTP文件夹是否存在,注意,传入的文件夹最后一定要带/,否则会检查的是文件
+ *        现在检测方式和SFTP一致,都使用列出上一层目录的方式进行检测
  * 
  * @param dir 
  * @return true 
@@ -1377,69 +1617,111 @@ bool CurlFtp::checkFtpDirExist(const std::string& dir)
 {
     /* 检查传入的文件夹 */
     auto dirTmp = checkDirPath(dir);
-    if(!setCommonCurlOptions(m_curl, m_ftpUrl + dir, 10))
-    {
-        return false;
-    }
-    // resetCurl(m_curl);
-    // std::string ftpUrl = m_ftpUrl + dirTmp;
-    // curl_easy_setopt(m_curl, CURLOPT_URL, ftpUrl.c_str());
-    // curl_easy_setopt(m_curl, CURLOPT_USERNAME, m_username.c_str());
-    // curl_easy_setopt(m_curl, CURLOPT_PASSWORD, m_password.c_str());
-    /* 获取文件夹是否存在,不需要接收文件 */
-    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
-    // curl_easy_setopt(m_curl, CURLOPT_HEADER, 1L);
+    // dirTmp = m_ftpUrl + dirTmp;
+    // if(!setCommonCurlOptions(m_curl, dirTmp, 10))
+    // {
+    //     return false;
+    // }
+//     /* 获取文件夹是否存在,不需要接收文件 */
+//     curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+//     // curl_easy_setopt(m_curl, CURLOPT_HEADER, 1L);
+
+//     /* 启用持久连接 */
+//     // curl_easy_setopt(m_curl, CURLOPT_TCP_KEEPALIVE, 1L);
+//     /* 这里只需要执行一次 */
+//     CURLcode res = curl_easy_perform(m_curl);
+//     bool result = true;
+//     if(res == CURLE_REMOTE_FILE_NOT_FOUND)
+//     {
+//         result = false;
+//     }
+//     else if(res == CURLE_OK)
+//     {
+//         result = true;
+//     } else 
+//     {
+//         LOG_ERROR("Check remote dir error, error code: " << (int)res << ", " << curl_easy_strerror(res));
+//         result = false;
+//     }
+//     // LOG_DEBUG("Check remote dir: {}, res: {}", dir, (int)res);
 
-    /* 启用持久连接 */
-    // curl_easy_setopt(m_curl, CURLOPT_TCP_KEEPALIVE, 1L);
-    /* 这里只需要执行一次 */
-    CURLcode res = curl_easy_perform(m_curl);
-    bool result = true;
-    if(res == CURLE_REMOTE_FILE_NOT_FOUND)
+    /* 获取文件夹 */
+    std::string parentDir = QFileInfo(QString::fromStdString(dir)).path().toStdString();
+    std::vector<CF_FileInfo> vecDir;
+    bool ret = listByWildcard(m_curl, parentDir, vecDir);
+    if(!ret)
     {
-        result = false;
+        LOG_ERROR("Failed to check ftp dir: " << dir);
+        return false;
     }
-    else if(res == CURLE_OK)
-    {
-        result = true;
-    } else 
+    /* 取出本层文件夹名称 */
+    std::string dirName = QFileInfo(QString::fromStdString(dir)).fileName().toStdString();
+    /* 判断是否存在 */
+    bool result = false;
+    for(const auto& fi : vecDir)
     {
-        LOG_ERROR("Check remote dir error, error code: " << (int)res << ", " << curl_easy_strerror(res));
-        result = false;
+        if(fi.name == dirName && fi.type == CF_FileType::DIR)
+        {
+            result = true;
+            break;
+        }
     }
-    // LOG_DEBUG("Check remote dir: {}, res: {}", dir, (int)res);
     return result;
 }
 
 /* 检查SFTP文件夹是否存在,这里通过列出上一层的文件夹中的内容,然后进行对比判断的 */
-bool CurlFtp::checkSftpDirExist(const std::string& dir)
+// bool CurlFtp::checkSftpDirExist(const std::string& dir)
+// {
+//     /* 取出上一层文件夹路径 */
+//     std::string parentDir = QFileInfo(QString::fromStdString(dir)).path().toStdString();
+//     std::vector<std::string> vecDir;
+//     bool ret = getDirList(parentDir, vecDir);
+//     if(!ret)
+//     {
+//         LOG_ERROR("Failed to check sftp dir: " << dir);
+//         return false;
+//     }
+//     /* 取出本层文件夹名称 */
+//     std::string dirName = QFileInfo(QString::fromStdString(dir)).fileName().toStdString();
+//     /* 判断是否存在 */
+//     bool result = false;
+//     for(const std::string& str : vecDir)
+//     {
+//         if(str == dirName)
+//         {
+//             result = true;
+//             break;
+//         }
+//     }
+//     return result;
+// }
+
+/* 检查FTP/SFTP文件是否存在,使用通配符获取文件列表匹配 */
+bool CurlFtp::checkFtpFileExist(const std::string& file)
 {
+    /* 判断文件字符是否符合规范 */
+    auto fileTmp = checkFilePath(file);
+    if(fileTmp.empty())
+    {
+        LOG_ERROR("File path is not correct: " << file);
+        return false;
+    }
     /* 取出上一层文件夹路径 */
-// #if (__cplusplus >= 201703L)
-//     std::string parentDir = std::filesystem::path(dir).parent_path().string();
-// #else
-    std::string parentDir = QFileInfo(QString::fromStdString(dir)).path().toStdString();
-// #endif /* (__cplusplus >= 201703L) */
-    // LOG_DEBUG("Parent dir: {}", parentDir);
-    std::vector<std::string> vecDir;
-    bool ret = getDirList(parentDir, vecDir);
+    std::string parentDir = QFileInfo(QString::fromStdString(file)).path().toStdString();
+    std::vector<CF_FileInfo> vecFiles;
+    bool ret = listByWildcard(m_curl, parentDir, vecFiles);
     if(!ret)
     {
-        LOG_ERROR("Failed to check sftp dir: " << dir);
+        LOG_ERROR("Failed to check ftp file: " << file);
         return false;
     }
-    /* 取出本层文件夹名称 */
-// #if (__cplusplus >= 201703L)
-//     std::string dirName = std::filesystem::path(dir).filename().string();
-// #else
-    std::string dirName = QFileInfo(QString::fromStdString(dir)).fileName().toStdString();
-// #endif /* (__cplusplus >= 201703L) */
-    // LOG_DEBUG("Dir name: {}", dirName);
+    /* 取出本层文件名称 */
+    std::string fileName = QFileInfo(QString::fromStdString(file)).fileName().toStdString();
     /* 判断是否存在 */
     bool result = false;
-    for(const std::string& str : vecDir)
+    for(const auto& fi : vecFiles)
     {
-        if(str == dirName)
+        if(fi.name == fileName && fi.type != CF_FileType::DIR)
         {
             result = true;
             break;
@@ -1449,4 +1731,159 @@ bool CurlFtp::checkSftpDirExist(const std::string& dir)
 }
 
 
+/* 进入一个路径 */
+bool CurlFtp::changeToDir(const std::string& dir)
+{
+    if(m_ftpUrl.empty())
+    {
+        LOG_ERROR("ftpUrl is empty");
+        return false;
+    }
+    // 规范化目录,确保以'/'结尾
+    std::string dirWithSlash = checkDirPath(dir);
+    std::string url = m_ftpUrl + dirWithSlash;
+
+    // 设置到该目录并发送一个不传输实体的请求,相当于验证可进入
+    if(!setCommonCurlOptions(m_curl, url, 10))
+    {
+        return false;
+    }
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+    LOG_DEBUG("Change to remote dir: " << dirWithSlash);
+    return performCurl(m_curl);
+}
+
+/* 删除一个文件,相对路径,调用这个函数,需要先调用上面那个函数进入路径 */
+bool CurlFtp::deleteFileRelative(const std::string& remoteFile)
+{
+    // 相对删除:要求已通过 changeToDir() 进入父目录
+    // 仅接受基名,不允许包含'/',且不允许以'/'结尾
+    if(remoteFile.empty())
+    {
+        LOG_ERROR("Delete relative file name is empty");
+        return false;
+    }
+    // 不允许包含路径分隔符
+    if(remoteFile.find('/') != std::string::npos)
+    {
+        LOG_ERROR("DeleteFileRelative only accepts basename without '/' : " << remoteFile);
+        return false;
+    }
+
+    // 当前 m_curl 已经被 changeToDir() 设置到父目录 URL
+    // 直接下发 DELE/rm 命令
+    struct curl_slist *headerlist = nullptr;
+    std::string deleteCmd;
+    if(m_isSftp)
+    {
+        deleteCmd = "rm " + remoteFile;   // sftp 使用 rm
+    }
+    else
+    {
+        deleteCmd = "DELE " + remoteFile; // ftp 使用 DELE
+    }
+    headerlist = curl_slist_append(headerlist, deleteCmd.c_str());
+    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+    LOG_DEBUG("Delete relative file: " << remoteFile);
+    bool ret = performCurl(m_curl);
+    curl_slist_free_all(headerlist);
+    return ret;
+}
+
+/* 删除一个文件夹,相对路径,调用这个函数,需要先调用上面那个函数进入路径 */
+bool CurlFtp::deleteDirectoryRelative(const std::string& ftpDir)
+{
+    // 相对删除:要求已通过 changeToDir() 进入父目录
+    // 仅接受基名,不允许包含'/',且不允许以'/'结尾
+    if(ftpDir.empty())
+    {
+        LOG_ERROR("Delete relative dir name is empty");
+        return false;
+    }
+    // 不允许包含路径分隔符
+    if(ftpDir.find('/') != std::string::npos)
+    {
+        LOG_ERROR("DeleteDirectoryRelative only accepts basename without '/' : " << ftpDir);
+        return false;
+    }
+
+    // 当前 m_curl 已经被 changeToDir() 设置到父目录 URL
+    // 直接下发 RMD/rmdir 命令
+    struct curl_slist *headerlist = nullptr;
+    std::string deleteCmd;
+    if(m_isSftp)
+    {
+        deleteCmd = "rmdir " + ftpDir;   // sftp 使用 rmdir
+    }
+    else
+    {
+        deleteCmd = "RMD " + ftpDir; // ftp 使用 RMD
+    }
+    headerlist = curl_slist_append(headerlist, deleteCmd.c_str());
+    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+    LOG_DEBUG("Delete relative directory: " << ftpDir);
+    bool ret = performCurl(m_curl);
+    curl_slist_free_all(headerlist);
+    return ret;
+}
+
+
+/**
+ * @brief 创建FTP文件夹,只能创建一层文件夹
+ * 
+ * @param ftpDir 文件夹路径
+ * @return true 文件夹创建成功或者文件夹存在
+ * @return false 
+ */
+bool CurlFtp::createOneDirectory(const std::string& ftpDir)
+{
+    if(m_ftpUrl.empty())
+    {
+        LOG_ERROR("ftpUrl is empty");
+        return false;
+    }
+    /* 先检查FTP文件夹是否存在,如果存在直接返回true */
+    if(existsDir(ftpDir))
+    {
+        return true;
+    }
+
+    setCommonCurlOptions(m_curl, m_ftpUrl, 30);
+
+    /* 检查传入的文件夹格式 */
+    auto dirTmp = checkDirPath(ftpDir);
+    /* 去掉末尾的“/” */
+    auto dirNoSlash = trimTrailingSlash(dirTmp);
+
+    /* 创建FTP头信息 */
+    struct curl_slist *headerlist = NULL;
+    std::string mkdir;
+    if(m_isSftp)
+    {
+        mkdir = "mkdir " + dirNoSlash;
+    }else {
+        mkdir = "MKD " + dirNoSlash;
+    }
+    headerlist = curl_slist_append(headerlist, mkdir.c_str());
+    /* 设置FTP命令行选项 */
+    curl_easy_setopt(m_curl, CURLOPT_QUOTE, headerlist);
+    /* 不包含实体 */
+    curl_easy_setopt(m_curl, CURLOPT_NOBODY, 1L);
+
+    LOG_DEBUG("Create remote dir: " << dirTmp);
+    // CURLcode res = curl_easy_perform(curl);
+    bool ret = performCurl(m_curl);
+    if(!ret)
+    {
+        LOG_ERROR("Failed to create remote Dir");
+    }
+
+    // curl_easy_cleanup(curl);
+    curl_slist_free_all(headerlist);
+    return ret;
+}
+
+
 

+ 21 - 11
module/CurlFtp/CurlFtp.h

@@ -43,20 +43,20 @@ public:
     /* 设置是否启用CURL的调试信息 */
     void enableCurlDebug(bool isPrint = false);
 
-    
+    /* 获取列表,包括文件和文件夹 */
+    bool getList(std::string dir, std::vector<CF_FileInfo>& fileInfoList);
     /* 列出文件列表 */
     bool getFileList(std::string dir, std::vector<std::string>& fileList);
     /* 获取文件夹列表 */
     bool getDirList(std::string dir, std::vector<std::string>& dirList);
     /* 判断文件夹是否存在 */
-    bool isDirExist(const std::string& dir);
-    /* 创建FTP文件夹 */
-    bool createDirectory(const std::string& ftpDir);
+    bool existsDir(const std::string& dir);
+    
     /* 创建FTP文件夹,递归创建 */
-    bool createDirectories(const std::string& ftpDir);
-    /* 删除文件 */
+    bool createDirectory(const std::string& ftpDir);
+    /* 删除文件,文件后面不能带有“/”否则会报错,这里是绝对路径 */
     bool deleteFile(const std::string& remoteFile);
-    /* 删除文件夹 */
+    /* 删除文件夹,递归删除 */
     bool deleteDirectory(const std::string& ftpDir);
 
     /* 下载文件 */
@@ -68,13 +68,13 @@ public:
     /* 上传文件,上传数据 */
     bool uploadData(char* srcData, size_t size, const std::string& remoteFile, size_t timeout = 30, bool isCreateDir = false);
 
-    
-
 private:
     /* 设置常规通用的curl设置 */
     bool setCommonCurlOptions(CURL* curl, const std::string& ftpUrl, size_t timeout);
     /* 列出所有内容 */
-    bool listAll(CURL* curl, std::string dir, std::vector<CF_FileInfo>& fileInfoList);
+    // bool listAll(CURL* curl, std::string dir, std::vector<CF_FileInfo>& fileInfoList);
+    /* 通过通配符列出文件信息 */
+    bool listByWildcard(CURL* curl, const std::string& dir, std::vector<CF_FileInfo>& out);
     /* 检查FTP文件夹路径是否合规,不合规就修改 */
     std::string checkDirPath(const std::string& dir);
     /* 检查FTP文件路径是否合规 */
@@ -94,7 +94,17 @@ private:
     /* 检查FTP文件夹是否存在 */
     bool checkFtpDirExist(const std::string& dir);
     /* 检查SFTP文件夹是否存在 */
-    bool checkSftpDirExist(const std::string& dir);
+    // bool checkSftpDirExist(const std::string& dir);
+    /* 检查FTP/SFTP文件是否存在,使用通配符获取文件列表匹配 */
+    bool checkFtpFileExist(const std::string& file);
+    /* 进入一个路径 */
+    bool changeToDir(const std::string& dir);
+    /* 删除一个文件,相对路径,调用这个函数,需要先调用上面那个函数进入路径 */
+    bool deleteFileRelative(const std::string& remoteFile);
+    /* 删除一个文件夹,相对路径,调用这个函数,需要先调用上面那个函数进入路径 */
+    bool deleteDirectoryRelative(const std::string& ftpDir);
+    /* 创建一个FTP文件夹 */
+    bool createOneDirectory(const std::string& ftpDir);
 
 private:
     std::mutex m_mutexCurl;                 /* curl互斥锁 */