VC++ libcurl FTP上传客户端程序

613 阅读2分钟

最近需要在Windows下使用libcurl库实现FTP文件上传的MFC程序,最基础的一个版本的功能是定时扫描某个目录下符合文件规则(比如*.json *.xml等)的所有文件(包括子目录),然后将其上传到某个FTP目录(需要配置好上传的FTP账号信息,比如FTP目录,FTP账号、密码、),类似如下面的XML信息:

<?xml version="1.0"?>
<Settings upload-rate="5">
  <ftp-srv local-path="D:\01_Web\DataFTP\LD\SS4300001\" folder-dir="$yyyy$mm$dd" local-file="*.json" remote-url="ftp://192.168.0.123/AVORS/" remote-user="test" remote-pwd="test@123" />
</Settings>

最终的MFC FTP上传客户端效果如下图所示: FTPUpload FTP客户端

libcurl官网提供的FTP上传程序示例代码

libcul官网提供的FTP上传程序示例代码ftpupload.c如下:

/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) 1998 - 2019, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at https://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/ 
#include <stdio.h>
#include <string.h>
 
#include <curl/curl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#ifdef WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
 
/* <DESC>
 * Performs an FTP upload and renames the file just after a successful
 * transfer.
 * </DESC>
 */ 
 
#define LOCAL_FILE      "/tmp/uploadthis.txt"
#define UPLOAD_FILE_AS  "while-uploading.txt"
#define REMOTE_URL      "ftp://example.com/"  UPLOAD_FILE_AS
#define RENAME_FILE_TO  "renamed-and-fine.txt"
 
/* NOTE: if you want this example to work on Windows with libcurl as a
   DLL, you MUST also provide a read callback with CURLOPT_READFUNCTION.
   Failing to do so will give you a crash since a DLL may not use the
   variable's memory when passed in to it from an app like this. */ 
static size_t read_callback(void *ptr, size_t size, size_t nmemb, void *stream)
{
  curl_off_t nread;
  /* in real-world cases, this would probably get this data differently
     as this fread() stuff is exactly what the library already would do
     by default internally */ 
  size_t retcode = fread(ptr, size, nmemb, stream);
 
  nread = (curl_off_t)retcode;
 
  fprintf(stderr, "*** We read %" CURL_FORMAT_CURL_OFF_T
          " bytes from file\n", nread);
  return retcode;
}
 
int main(void)
{
  CURL *curl;
  CURLcode res;
  FILE *hd_src;
  struct stat file_info;
  curl_off_t fsize;
 
  struct curl_slist *headerlist = NULL;
  static const char buf_1 [] = "RNFR " UPLOAD_FILE_AS;
  static const char buf_2 [] = "RNTO " RENAME_FILE_TO;
 
  /* get the file size of the local file */ 
  if(stat(LOCAL_FILE, &file_info)) {
    printf("Couldn't open '%s': %s\n", LOCAL_FILE, strerror(errno));
    return 1;
  }
  fsize = (curl_off_t)file_info.st_size;
 
  printf("Local file size: %" CURL_FORMAT_CURL_OFF_T " bytes.\n", fsize);
 
  /* get a FILE * of the same file */ 
  hd_src = fopen(LOCAL_FILE, "rb");
 
  /* In windows, this will init the winsock stuff */ 
  curl_global_init(CURL_GLOBAL_ALL);
 
  /* get a curl handle */ 
  curl = curl_easy_init();
  if(curl) {
    /* build a list of commands to pass to libcurl */ 
    headerlist = curl_slist_append(headerlist, buf_1);
    headerlist = curl_slist_append(headerlist, buf_2);
 
    /* we want to use our own read function */ 
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);
 
    /* enable uploading */ 
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
 
    /* specify target */ 
    curl_easy_setopt(curl, CURLOPT_URL, REMOTE_URL);
 
    /* pass in that last of FTP commands to run after the transfer */ 
    curl_easy_setopt(curl, CURLOPT_POSTQUOTE, headerlist);
 
    /* now specify which file to upload */ 
    curl_easy_setopt(curl, CURLOPT_READDATA, hd_src);
 
    /* 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(curl, CURLOPT_INFILESIZE_LARGE,
                     (curl_off_t)fsize);
 
    /* Now run off and do what you've been told! */ 
    res = curl_easy_perform(curl);
    /* Check for errors */ 
    if(res != CURLE_OK)
      fprintf(stderr, "curl_easy_perform() failed: %s\n",
              curl_easy_strerror(res));
 
    /* clean up the FTP commands list */ 
    curl_slist_free_all(headerlist);
 
    /* always cleanup */ 
    curl_easy_cleanup(curl);
  }
  fclose(hd_src); /* close the local file */ 
 
  curl_global_cleanup();
  return 0;
}

FTP核心处理类的实现

FTP上传功能描述:

实现一个FTP客户端推送程序,定时扫描指定的目录,根据指定的目录和文件规则获取符合条件的文件列表,然后对比本地文件列表和file.xml中的文件列表,获取差异化的文件列表,遍历该列表,执行上传。流程如下图所示: FTP客户端上传流程

头文件 FTPUpload.h

#pragma once
#include <string>
#include <vector>
typedef std::string String;
#include <io.h>
typedef std::vector<_finddata_t> VectorFile;

#include "../include/xml/pugixml.hpp"
#include "pub.h"
#include "my_log.h"

class FTPUpload
{
public:
	FTPUpload();
	~FTPUpload();

	// 设置本地目录和文件规则
	void set_local_path(const String& path, const String& dirRule, const String& file);

	// 设置远程路径和验证
	void set_remote_path(const String& url, const String& user = "", const String& pwd = "");

	//// 进行查找文件,对比更新,上传FTP
	void upload();

	// 清空记录
	bool clear_files();

protected:
	// 转换文件规则
	String convert_filepath(const String& srcpath);

	// 获取本地文件列表
	void get_local_files();

	// 获取XML文件记录的文件列表
	void get_xml_files();

	// 获取需要上传的文件列表
	void get_upload_files();

	// 更新XML文件列表
	void update_xml();

	// curl的读文件函数
	static size_t read_callback(void *ptr, size_t size, size_t nmemb, void *stream);

	// 开始上传
	void upload_files();

private:
	VectorFile m_fileLocal;		// 读取的本地文件列表
	VectorFile m_fileXml;		// XML文件记录的文件列表
	VectorFile m_fileUpload;	// 需要上传的文件列表
	VectorFile m_fileFailed;	// 上传失败的列表

	String	m_LocalDirRule;	// 本地文件夹规则
	String	m_FolderDir;	// 文件夹
	String	m_LocalPath;	// 本地文件目录
	String	m_LocalFile;	// 本地文件规则
	String	m_remoteUrl;	// 远程目录
	String	m_remoteUser;	// 远程用户名
	String	m_remotePwd;	// 远程密码

	String	m_pathXml;		// XML配置文件路径

	UploadRows m_upSuccessed;	// 上传成功的记录列表
	UploadRows m_upFailed;		// 上传失败的记录列表
};

源文件 FTPUpload.cpp

#include "stdafx.h"
#include "FTPUpload.h"
#include <algorithm>

#define BUILDING_LIBCURL
#include "../include/curl/curl.h"
#pragma comment(lib, "wldap32.lib")
#ifdef _DEBUG
#pragma comment(lib, "../lib/debug/libcurld.lib")
#else
#pragma comment(lib, "../lib/release/libcurl.lib")
#endif

FTPUpload::FTPUpload()
{
	m_pathXml = "./file.xml";

	/* In windows, this will init the winsock stuff */
	curl_global_init(CURL_GLOBAL_ALL);
}


FTPUpload::~FTPUpload()
{
	curl_global_cleanup();
}

void FTPUpload::set_local_path(const String& path, const String& dirRule, const String& file)
{
	char ch =  path.at(path.size() - 1);
	if (ch == '\\' || ch == '/')
		m_LocalPath = path.substr(0, path.size() - 1);
	else
		m_LocalPath = path;
	m_LocalDirRule = dirRule;
	m_LocalFile = file;
}

void FTPUpload::set_remote_path(const String& url, const String& user /*= ""*/, const String& pwd /*= ""*/)
{
	char ch = url.at(url.size() - 1);
	if (ch == '\\' || ch == '/')
		m_remoteUrl = url.substr(0, url.size() - 1);
	else
		m_remoteUrl = url;
	
	m_remoteUser = user;
	m_remotePwd = pwd;
}

void FTPUpload::upload()
{
	// 读取本地文件列表
	get_local_files();
	// 读取上次文件列表
	get_xml_files();
	// 对比变化的文件
	get_upload_files();
	// 上传文件列表
	upload_files();
	// 更新XML列表
	update_xml();
}

// 清空记录
bool FTPUpload::clear_files()
{
	pugi::xml_document doc;
	return doc.save_file(m_pathXml.c_str());
}

#include <boost/algorithm/string.hpp>
String FTPUpload::convert_filepath(const String& srcpath)
{
	time_t tt = time(0);
	tm* now = localtime(&tt);
	auto it = [](int val, int count, const char* cc) {
		std::string fmt1 = "%$b$cd";
		std::string fmt = boost::replace_all_copy(boost::replace_all_copy(fmt1, "$b", cc), "$c", std::to_string(count));
		char str[256];
		std::sprintf(str, fmt.c_str(), val);
		return std::string(str);
	};
	return boost::replace_all_copy(
		boost::replace_all_copy(
			boost::replace_all_copy(
				boost::replace_all_copy(srcpath, "$yyyy", it(now->tm_year + 1900, 4, "0")),
				"$mm", it(now->tm_mon + 1, 2, "0")),
			"$dd", it(now->tm_mday, 2, "0")),
		"$HH", it(now->tm_hour, 2, "0"));
}

// 获取本地文件列表
void FTPUpload::get_local_files()
{
	// 路径
	String local_path = m_LocalPath;
	// 规则文件夹
	m_FolderDir = convert_filepath(m_LocalDirRule);
	if (m_FolderDir.size())
	{
		local_path += "\\" + m_FolderDir;
	}

#ifdef WIN32
	local_path += "\\";
#else
	local_path += "/";
#endif
	local_path += m_LocalFile;

	// 清空列表
	m_fileLocal.clear();
	// 读取文件目录
	_finddata_t fdd;
	intptr_t fd = _findfirst(local_path.c_str(), &fdd);
	int rc = fd;
	while (rc != -1)
	{
		m_fileLocal.push_back(fdd);
		rc = _findnext(fd, &fdd);
	}
	_findclose(fd);
}

// 获取XML文件记录的文件列表
void FTPUpload::get_xml_files()
{
	// 清空
	m_fileXml.clear();
	// 读取xml配置
	pugi::xml_document doc;
	pugi::xml_parse_result rc = doc.load_file(m_pathXml.c_str());
	if (rc.status == pugi::status_ok)
	{
		auto fl = doc.child("file-list");
		for (auto it = fl.child("file"); it; it = it.next_sibling("file"))
		{
			_finddata_t dd;
			strcpy_s(dd.name, it.attribute("name").value());
			dd.size = it.attribute("size").as_uint();
			dd.time_write = it.attribute("last_time").as_uint();
			m_fileXml.push_back(dd);
		}
	}
}

// 获取需要上传的文件列表
void FTPUpload::get_upload_files()
{
	// 清空上传列表
	m_fileUpload.clear();
	// 对比文件信息
	for (auto i : m_fileLocal)
	{
		// xml中没有的就加入
		auto it1 = std::find_if(m_fileXml.begin(), m_fileXml.end(), [i](const _finddata_t& oth){
			return strcmp(oth.name, i.name) == 0;
		});
		if (it1 == m_fileXml.end())
		{
			m_fileUpload.push_back(i);
		}
		// xml中有的,对比日期,日期变化的就加入
		auto it2 = std::find_if(m_fileXml.begin(), m_fileXml.end(), [i](const _finddata_t& oth){
			return strcmp(oth.name, i.name) == 0 && i.time_write != oth.time_write;
		});
		if (it2 != m_fileXml.end())
		{
			m_fileUpload.push_back(i);
		}
	}
}

// 更新XML文件列表
void FTPUpload::update_xml()
{
	if (m_fileUpload.empty())
	{
		return;
	}
	// 将所有文件保存
	pugi::xml_document doc;
	auto all = doc.append_child("file-list");
	for (auto i : m_fileLocal)
	{
		// 检查传输失败的文件列表
		auto it = std::find_if(m_fileFailed.begin(), m_fileFailed.end(), [i](const _finddata_t& oth){
			return strcmp(oth.name, i.name) == 0;
		});
		// 记录传输成功的文件
		if (it != m_fileFailed.end())
		{
			continue;
		}
		auto item = all.append_child("file");
		item.append_attribute("name").set_value(i.name);
		item.append_attribute("size").set_value(i.size);
		item.append_attribute("last_time").set_value(i.time_write);
	}
	doc.save_file(m_pathXml.c_str());
}

size_t FTPUpload::read_callback(void *ptr, size_t size, size_t nmemb, void *stream)
{
	curl_off_t nread;
	/* in real-world cases, this would probably get this data differently
	as this fread() stuff is exactly what the library already would do
	by default internally */
	size_t retcode = fread(ptr, size, nmemb, (FILE*)stream);

	nread = (curl_off_t)retcode;

	// 	fprintf(stderr, "*** We read %" CURL_FORMAT_CURL_OFF_T
	// 		" bytes from file\n", nread);
	return retcode;
}

// 进行查找文件,对比更新,上传FTP
void FTPUpload::upload_files()
{
	m_fileFailed.clear();
	m_upFailed.clear();
	m_upSuccessed.clear();

	/* get a curl handle */
	CURL* curl = curl_easy_init();
	if (!curl)
	{
		m_fileFailed = m_fileUpload;
		return;
	}

	/* enable uploading */
	curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);

	/* we want to use our own read function */
	curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);

	// 设置远程访问的用户名和密码
	if (m_remoteUser.size())
	{
		String verify = m_remoteUser + ":" + m_remotePwd;
		curl_easy_setopt(curl, CURLOPT_USERPWD, verify.c_str());
	}

	for (auto it : m_fileUpload)
	{
		// 本地文件路径
		String local_file = m_LocalPath;

		// 组合文件路径
		local_file += "\\";
		if (m_FolderDir.size())
		{
			local_file += m_FolderDir + "\\";
		}
		local_file += it.name;

		// 打开上传的文件流
		FILE* fd = fopen(local_file.c_str(), "rb");

		// 设置上传的文件流
		curl_easy_setopt(curl, CURLOPT_READDATA, fd);

		// 设置上传的文件大小
		curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)it.size);

		// 设置文件夹
		curl_easy_setopt(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, 1);

		// 设置远程路径
		String remote_file = m_remoteUrl;
		remote_file += "/";
		if (m_FolderDir.size())
		{
			remote_file += m_FolderDir + "/";
		}
		remote_file += it.name;
		curl_easy_setopt(curl, CURLOPT_URL, remote_file.c_str());

		// 开始上传文件
		CURLcode res = curl_easy_perform(curl);
		if (res != CURLE_OK)
		{
			fprintf(stderr, "curl_easy_perform() failed: %s\n",
				curl_easy_strerror(res));
		}
		
		// 关闭本地文件流
		if (fd > 0) fclose(fd);
	}

	// 释放网络
	curl_easy_cleanup(curl);
}

源代码

源代码我已经上传到Github和Gitee上面了:

  • FTPUpload-Github地址
  • FTPUpload-Gitee地址 FTPUpload是一款基于MFC的FTP推送客户端程序,使用了libcurl实现FTP推送,使用pugixml实现xml配置文件的读写,还使用了Boost库用于目录规则的转换(涉及到日期的)。

参考资料: