当前位置: 首页 > news >正文

【云备份】客户端开发

目录

一. 客户端功能是什么

二. 文件工具类设计

 三.数据管理类设计

四.文件备份类设计

五.文件备份类实现

5.1.文件唯一标识的获取

5.2.文件上传接口实现

5.3.判断一个文件是否需要上传

5.4.主逻辑函数

5.5.文件备份类源码


一. 客户端功能是什么

注意:我们当前的客户端的开发,是在windows下的,我们使用的是vs2022(需要支持C++17的编译器)

客户端要实现的功能就是:自动对指定文件夹中的文件进行备份,

我们可以对上面这些功能进行模块划分:

  1. 数据管理模块:管理要备份的文件信息
  2. 目录遍历模块:获取指定文件夹中的所有文件路径名
  3. 文件备份模块:将需要备份的文件上传备份到服务器

二. 文件工具类设计

打开vs2022,搞成下面这样子

注意在VS2022中设置项目使用C++17标准:

  1. 右键项目 -> 属性。

  2. 进入“C/C++” -> “语言”。

  3. 设置“C++语言标准”为“ISO C++17 标准 (/std:c++17)”。

这个其实和服务端的实用工具类雷同,只是功能需求没有客户端那么多,复制过来即可

注意我做了一些小改动

util.hpp

#ifndef _MY_UTIL_
#define _MY_UTIL_
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <filesystem>//注意这里改了namespace cloud {//注意:我们下面这些接口的名称可能会与系统的那些接口的名字重复,所以最好创建名称空间来避免名称污染namespace fs = std::filesystem;//注意这里改了class FileUtil{public:FileUtil(const std::string& filename) :_filename(filename) {}//主要针对普通文件的接口size_t FileSize() // 获取文件大小,这里使用64位的有符号的int,防止文件过大导致文件大小显示异常{struct stat st;int re = stat(_filename.c_str(), &st);if (re < 0)//stat函数获取文件属性失败了{std::cout << "Get FileSize Failed!!" << std::endl;return -1;}return st.st_size;}time_t LastModtime() // 获取文件最后一次修改时间{struct stat st;int re = stat(_filename.c_str(), &st);if (re < 0)//stat函数获取文件属性失败了{std::cout << "Get LastModtime Failed!!" << std::endl;return -1;}return st.st_mtime;}time_t LastAcctime() // 获取文件最后一次访问时间{struct stat st;int re = stat(_filename.c_str(), &st);if (re < 0)//stat函数获取文件属性失败了{std::cout << "Get LastAcctime Failed!!" << std::endl;return -1;}return st.st_atime;}std::string FileName() // 获取文件路径中的纯文件名称 a/b/c/test.cc --> test.cc{size_t pos = _filename.find_last_of("/");//寻找最后一个/if (pos == std::string::npos)//没找到,说明没有/{return _filename;}return _filename.substr(pos + 1);//从pos截取到末尾}bool GetPosLen(std::string* body, size_t pos, size_t len){std::ifstream ifs;ifs.open(_filename, std::ios::binary);//打开文件,以二进制方式来读取数据if (ifs.is_open() == false)//打开失败{std::cout << "GetPosLen: open file failed!" << std::endl;return false;}size_t fsize = this->FileSize();//获取文件大小if (pos + len > fsize)//超过文件大小了{std::cout << "GetPosLen: get file len error" << std::endl;return false;}ifs.seekg(pos, std::ios::beg); // 将文件指针定位到pos处body->resize(len);//把存储读取的数据的载体的大小修改到够大的ifs.read(&(*body)[0], len);//读取数据if (ifs.good() == false)//上次读取出错了{std::cout << "GetPosLen: get file content failed" << std::endl;ifs.close();return false;}ifs.close();return true;}bool GetContent(std::string* body){size_t fsize = FileSize();return GetPosLen(body, 0, fsize);}bool SetContent(const std::string& body)//写入数据{std::ofstream ofs;//也就是输出ofs.open(_filename, std::ios::binary);//以二进制模式打开if (ofs.is_open() == false)//打开失败{std::cout << "SetContent: write open file failed" << std::endl;return false;}ofs.write(&body[0], body.size());if (ofs.good() == false)//上次写入文件数据出错了{std::cout << "SetContent: write open file failed" << std::endl;ofs.close();return false;}ofs.close();return true;}bool Exists(){return fs::exists(_filename);}bool Remove(){if (Exists() == false){return true;}remove(_filename.c_str());return true;}bool CreateDirectory(){if (Exists()) return true;return fs::create_directories(_filename);}bool ScanDirectory(std::vector<std::string>* array){this->CreateDirectory();//先确保目录是存在的,不在就创建一个for (auto& p : fs::directory_iterator(_filename)) // 迭代器遍历指定目录下的文件,从那个网站上面复制下来的{if (fs::is_directory(p) == true)//如果是目录,就不添加进当前目录的文件里continue;// relative_path 带有路径的文件名array->push_back(fs::path(p).relative_path().string());//添加文件//注意迭代器返回的p不是string,不能直接将p添加进array里面,我们需要使用path将其}return true;}private:std::string _filename; // 文件名--包含路径};
};
#endif

 cloud.cpp

#include"util.hpp"
int main()
{cloud::FileUtil fu("./");std::vector<std::string>arry;fu.ScanDirectory(&arry);for (auto& a : arry){std::cout << a << std::endl;}
}

这个只是测试我们的有没有问题,显然是没有问题的。 

 

 三.数据管理类设计

  • 数据管理模块

先讲讲数据管理模块,并不是所有的文件每次都需要上传,我们需要能够判断,哪些文件需要上传,哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份的判断。

这些被管理的信息将用来判断一个文件是否需要重新备份

  1. 文件是否是新增的
  2. 文件不是新增的,则上次备份后没有被修改过 

因此需要被管理的信息包含以下:

  • 文件路径名称;
  • 文件唯一标识(也就是HTTP的Etag首部字段):由文件名,最后一次修改时间,文件大小组成的一串信息;

客户端数据管理模块可直接由服务端数据管理模块改造得到,因为其只需要服务端数据管理模块代码中一小部分功能。

数据管理模块实现思想:

  • 内存存储

高访问效率,即使用哈希表(unordered_map)

  • 持久化文件存储

涉及序列化,我们服务端是使用JSON,但是我们vs2022没有jsoncpp这个库(我不想安装),所以,我们不使用JSON,我们直接自定义序列化格式:key value,每个key之间使用\n来分割

key value\nkey value\nkey value\nkey value……

 我们就安装上面这个来解析文件。


客户端数据管理模块可直接由服务端数据管理模块改造得到,因为其只需要服务端数据管理模块代码中一小部分功能。

data.hpp

#ifndef __MY_DATA__
#define __MY_DATA__
#include <unordered_map>
#include <sstream>
#include "util.hpp"namespace cloud {class DataManager {private:std::string _backup_file;//备份信息的持久化存储文件std::unordered_map<std::string, std::string> _table;//我们需要管理的数据都在这里面,因为我们只需要两条信息//文件路径名称和文件唯一标识public:DataManager(const std::string& backup_file) :_backup_file(backup_file) {//获取我们的文件名即可InitLoad();//一构造的时候就加载我们的数据}bool Storage() //持久化存储文件{//1. 获取所有的备份信息——获取_table里面的信息std::stringstream ss;auto it = _table.begin();for (; it != _table.end(); ++it) {//2. 将所有信息进行指定持久化格式的组织ss << it->first << " " << it->second << "\n";}//3. 持久化存储——将它们全部写入我们的备份信息文件FileUtil fu(_backup_file);fu.SetContent(ss.str());return true;}// 字符串分割函数// 参数说明://   str:源字符串//   sep:分隔符//   arry:存储分割结果的字符串数组指针// 返回值:分割得到的子字符串个数int Split(const std::string& str, const std::string& sep, std::vector<std::string>* arry){int count = 0;             // 记录分割出的子字符串个数size_t pos = 0;            // 分隔符所在位置size_t idx = 0;            // 查找起始位置// 循环查找分隔符while (1) {pos = str.find(sep, idx);  // 从idx位置开始查找分隔符// 未找到分隔符时退出循环if (pos == std::string::npos) {break;}// 处理零长度子字符串情况(连续分隔符或开头就是分隔符)if (pos == idx) {idx = pos + sep.size();  // 跳过当前分隔符continue;                // 继续下一次循环,避免添加空字符串}// 截取两个分隔符之间的子字符串std::string tmp = str.substr(idx, pos - idx);arry->push_back(tmp);  // 将子字符串存入结果数组count++;// 更新下一次查找的起始位置(跳过当前分隔符)idx = pos + sep.size();}// 处理最后一个分隔符之后的剩余内容if (idx < str.size()) {arry->push_back(str.substr(idx));  // 截取末尾剩余字符串count++;}return count;  // 返回分割出的子字符串总数}bool InitLoad()//初始化加载{//1. 从备份了文件信息的文件中读取备份的数据FileUtil fu(_backup_file);std::string body;fu.GetContent(&body);//2. 进行数据解析,添加到表中std::vector<std::string> arry;//注意我们的格式是key value\nkey value\nkey value\nkey value……Split(body, "\n", &arry);for (auto& a : arry) {// 分割key valuestd::vector<std::string> tmp;Split(a, " ", &tmp);if (tmp.size() != 2)//说明出问题了 {continue;}_table[tmp[0]] = tmp[1];//管理起来}return true;}bool Insert(const std::string& key, const std::string& val)//插入{_table[key] = val;Storage();//持久化存储return true;}bool Update(const std::string& key, const std::string& val) {_table[key] = val;Storage();//也是存储起来return true;}bool GetOneByKey(const std::string& key, std::string* val) {auto it = _table.find(key);if (it == _table.end()) {return false;}*val = it->second;return true;}};
}
#endif

我们来测试一下

cloud.cpp

#include"util.hpp"
#include"data.hpp"
#define BACKUP_FILE "./backup.dat"
int main()
{cloud::FileUtil fu("./");std::vector<std::string>arry;fu.ScanDirectory(&arry);cloud::DataManager data(BACKUP_FILE);for (auto& a : arry){data.Insert(a, "sassdsajdjsa");}
}

编译运行一下,我们发现当前路径下面生成了这么一个文件

打开一看,那很好了

接着我们进行测试

cloud.cpp

#include"util.hpp"
#include"data.hpp"
#define BACKUP_FILE "./backup.dat"
int main()
{cloud::DataManager data(BACKUP_FILE);std::string str;data.GetOneByKey("./cloud.cpp", &str);std::cout << str << std::endl;}

这功能基本可以了!!

四.文件备份类设计

我们客户端的功能就是自动将指定文件夹里的文件备份到服务器上面去

那基本就是下面这个

  • 遍历指定文件夹
  • 逐一判断文件是否需要备份
  • 需要备份的文件进行上传备份

客户端文件备份类主要包含以下成员:

#define SERVER_ADDR "47.108.25.253" // 服务器的IP
#define SERVER_PORT 9090 // 服务器端口
class Backup
{
public:Backup(const std::string& back_dir, const std::string& back_file);// 生成文件的唯一标识,用于判断该文件是否需要上次std::string GetFileIdentifier(std::string filename);// 上传文件函数bool Upload(const std::string& filename); // 判断是否需要上传bool IsNeedUpload(const std::string& filename);// 主逻辑执行函数bool RunMoudle();
private:std::string _back_dir; // 要监控的文件目录的名字DataManager* _data;   //数据管理模块
};

五.文件备份类实现

5.1.文件唯一标识的获取

我们创建一个cloud.hpp

cloud.hpp

#ifndef __MY_CLOUD__
#define __MY_CLOUD__
#include "data.hpp"
#include "util.hpp"
#include <Windows.h>
namespace cloud
{class Backup{public:Backup(const std::string& back_dir, const std::string& back_file):_back_dir(back_dir){_data = new DataManager(back_file);//数据管理类的对象}std::string GetFileIdentifier(std::string filename)//创建文件的唯一标识{FileUtil fu(filename);std::stringstream ss;ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastModtime();return ss.str();}bool RunMoudle()//主逻辑函数{while (1){FileUtil fu(_back_dir);std::vector<std::string> array;fu.ScanDirectory(&array);for (auto& a : array){_data->Insert(a, GetFileIdentifier(a));//将文件名 唯一标识添加进数据管理模块}Sleep(1);//避免服务器过载}}private:std::string _back_dir;DataManager* _data;};
}
#endif

 cloud.cpp

#include"util.hpp"
#include"cloud.hpp"
#include"data.hpp"
#define BACKUP_FILE "./backup.dat"
#define BACK_DIR "./backup/"
int main()
{cloud::Backup backup(BACK_DIR, BACKUP_FILE);backup.RunMoudle();
}

我们编译看看效果

当前目录下生成了一个backup的目录

我们把util.hpp复制到这个目录里面去

我们看最后一行,我们可以在试一下,把data.hpp拷贝进去

 

5.2.文件上传接口实现

cloud.hpp

// 文件上传函数
// 参数 filename: 需要上传的本地文件路径
// 返回值: true表示上传成功,false表示失败
bool Upload(const std::string& filename)
{// 步骤1: 读取文件内容到内存FileUtil fu(filename);        // 创建文件工具对象std::string body;              // 存储文件二进制内容fu.GetContent(&body);          // 将文件内容读取到body变量// 步骤2: 创建HTTP客户端并配置上传参数httplib::Client client(SERVER_ADDR, SERVER_PORT); // 创建连接服务器的客户端// SERVER_ADDR: 服务器IP地址// SERVER_PORT: 服务器端口号// 配置多部分表单数据(符合HTTP文件上传规范)httplib::MultipartFormData item;item.content = body;           // 设置文件二进制内容item.filename = fu.FileName(); // 设置上传后的文件名item.name = "file";            // 设置表单字段名(与服务端约定)item.content_type = "application/octet-stream"; // 通用二进制流类型// 步骤3: 构建请求并执行上传httplib::MultipartFormDataItems items; // 可以包含多个上传项items.push_back(item);       // 添加当前文件项// 发送POST请求到服务器的/upload端点auto res = client.Post("/upload", items);// 步骤4: 处理响应结果// 检查请求是否成功:网络通信成功且HTTP状态码为200if (!res || res->status != 200) {// 失败情况:// !res 表示网络错误(连接失败、超时等)// status != 200 表示服务端处理失败return false;}// 上传成功return true;
}

注意:我们这里需要把httplib库的httplib.h拷贝到当前目录来

然后在头文件那里,添加现有项,选择这个即可

5.3.判断一个文件是否需要上传

cloud.hpp

bool IsNeedUpload(const std::string& filename)
{// 需要上传的文件判断条件:文件是新增的,不是新增的但是被修改过// 1.文件是新增的:看一下有没有备份信息,没有备份信息,就说明是新增的,有备份信息,则说明不是新增的// 2.不是新增的但是被修改过:有历史信息,但是历史信息的唯一标识符与当前最新的唯一标识符不一致std::string id;if (_data->GetOneByKey(filename, &id) ==true)//有备份信息,文件不是新增的{// 看看这个文件有没有被修改过std::string new_id = GetFileIdentifier(filename);if (new_id == id)//没有修改过,不需要上传{return false;}}//一个文件比较大,正在慢慢的拷贝到那个备份目录下,// 那他在这个目录里面的大小在外面客户端每一次检测的时候都不一样,那是不是都要上传啊?// 显然是不要的// 如果是因为每次遍历都会判断标识不一致然后就因为这个进行上传,那么上传一个几十G的文件会上传上百次//判断一个文件是否有一段时间没有被修改过了FileUtil fu(filename);if (time(NULL) - fu.LastModtime() < 3)//3秒内修改过,不上传{return false;}return true;
}

5.4.主逻辑函数

cloud.hpp

bool RunMoudle()
{while (1){// 1.遍历获取指定文件夹中所有文件FileUtil fu(_back_dir);std::vector<std::string> array;fu.ScanDirectory(&array);// 2.逐个判断是否需要上传for (auto& a : array){if (IsNeedUpload(a) == false)continue;// 3.如果需要,则上传文件if (Upload(a) == true){_data->Insert(a, GetFileIdentifier(a));std::cout << a << " upload success!" << std::endl;}}Sleep(1);}
}

5.5.文件备份类源码

cloud.hpp

#ifndef __MY_CLOUD__
#define __MY_CLOUD__
#include "data.hpp"
#include "httplib.h"
#include "util.hpp"
#include <Windows.h>
#define SERVER_ADDR "47.108.25.253"
#define SERVER_PORT 9090
namespace cloud
{class Backup{public:Backup(const std::string& back_dir, const std::string& back_file):_back_dir(back_dir){_data = new DataManager(back_file);}std::string GetFileIdentifier(std::string filename){FileUtil fu(filename);std::stringstream ss;ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastModtime();return ss.str();}// 文件上传函数// 参数 filename: 需要上传的本地文件路径// 返回值: true表示上传成功,false表示失败bool Upload(const std::string& filename){// 步骤1: 读取文件内容到内存FileUtil fu(filename);        // 创建文件工具对象std::string body;              // 存储文件二进制内容fu.GetContent(&body);          // 将文件内容读取到body变量// 步骤2: 创建HTTP客户端并配置上传参数httplib::Client client(SERVER_ADDR, SERVER_PORT); // 创建连接服务器的客户端// SERVER_ADDR: 服务器IP地址// SERVER_PORT: 服务器端口号// 配置多部分表单数据(符合HTTP文件上传规范)httplib::MultipartFormData item;item.content = body;           // 设置文件二进制内容item.filename = fu.FileName(); // 设置上传后的文件名item.name = "file";            // 设置表单字段名(与服务端约定)item.content_type = "application/octet-stream"; // 通用二进制流类型// 步骤3: 构建请求并执行上传httplib::MultipartFormDataItems items; // 可以包含多个上传项items.push_back(item);       // 添加当前文件项// 发送POST请求到服务器的/upload端点auto res = client.Post("/upload", items);// 步骤4: 处理响应结果// 检查请求是否成功:网络通信成功且HTTP状态码为200if (!res || res->status != 200) {// 失败情况:// !res 表示网络错误(连接失败、超时等)// status != 200 表示服务端处理失败return false;}// 上传成功return true;}bool IsNeedUpload(const std::string& filename){// 需要上传的文件判断条件:文件是新增的,不是新增的但是被修改过// 1.文件是新增的:看一下有没有备份信息,没有备份信息,就说明是新增的,有备份信息,则说明不是新增的// 2.不是新增的但是被修改过:有历史信息,但是历史信息的唯一标识符与当前最新的唯一标识符不一致std::string id;if (_data->GetOneByKey(filename, &id) ==true)//有备份信息,文件不是新增的{// 看看这个文件有没有被修改过std::string new_id = GetFileIdentifier(filename);if (new_id == id)//没有修改过,不需要上传{return false;}}//一个文件比较大,正在慢慢的拷贝到那个备份目录下,// 那他在这个目录里面的大小在外面客户端每一次检测的时候都不一样,那是不是都要上传啊?// 显然是不要的// 如果是因为每次遍历都会判断标识不一致然后就因为这个进行上传,那么上传一个几十G的文件会上传上百次//判断一个文件是否有一段时间没有被修改过了FileUtil fu(filename);if (time(NULL) - fu.LastModtime() < 3)//3秒内修改过,不上传{return false;}return true;}bool RunMoudle(){while (1){// 1.遍历获取指定文件夹中所有文件FileUtil fu(_back_dir);std::vector<std::string> array;fu.ScanDirectory(&array);// 2.逐个判断是否需要上传for (auto& a : array){if (IsNeedUpload(a) == false)continue;// 3.如果需要,则上传文件if (Upload(a) == true)//上传成功了{_data->Insert(a, GetFileIdentifier(a));//新增文件备份信息std::cout << a << " upload success!" << std::endl;}}Sleep(1);}}private:std::string _back_dir;DataManager* _data;};
}
#endif

cloud.cpp 

#include"util.hpp"
#include"cloud.hpp"
#include"data.hpp"
#define BACKUP_FILE "./backup.dat"
#define BACK_DIR "./backup/"
int main()
{cloud::Backup backup(BACK_DIR, BACKUP_FILE);backup.RunMoudle();
}

http://www.xdnf.cn/news/331903.html

相关文章:

  • 百胜企业管理咨询:助力企业快速获得ecovadis认证
  • SecureCRT SFTP命令详解与实战
  • S32K3 HSE模块安装
  • 屏蔽力 | 在复杂世界中从内耗到成长的转变之道
  • STM32开发printf函数支持
  • LeetCode:二叉树的最大深度
  • React Native主题切换、字号调整:不用styled-components也能玩出花
  • 查询nvidia边缘设备的软硬件版本jetson_release
  • 【软件设计师:程序语言】4.程序语言基础知识
  • Unity-Socket通信实例详解
  • 【面试 · 二】JS个别重点整理
  • leetcode hot100 技巧
  • C++函数栈帧详解
  • Ultralytics中的YOLODataset和BaseDataset
  • comfyui 实现中文提示词翻译英文进行图像生成
  • 低成本监控IPC模组概述
  • D盘出现不知名文件
  • int (*)[3]和int (*arr_ptr)[3]区别
  • Spark应用部署模式实例
  • 个人网站versionI正式上线了!Personal Website for Jing Liu
  • ✍️【TS类型体操进阶】挑战类型极限,成为类型魔法师!♂️✨
  • JAVA八股文
  • CI/CD与DevOps流程流程简述(提供思路)
  • 使用pdm管理python项目时去哪里找nuitka
  • 如何通过复盘提升团队能力?
  • 数组和集合
  • 【C++的类型转换】
  • 【漏洞预警】:致远OA V8.1 SP2 data.htm DOM型XSS漏洞
  • 使用 `detach()` 断开与共享特征层的连接
  • (已完结)完美解决C盘拓展卷是灰色的无法扩容的问题以及如何正确地在WINDOS上从一个盘扩容到C盘