客户端负责的功能
-
指定目录的文件检测,获取文件夹里面的文件
-
判断这个文件是否需要备份,服务器备份过的文件则不需要进行备份,已经备份的文件如果修改也需要重新备份
-
若这个文件被用户打开,则不进行备份。需要每隔一段时间检测更新备份。
-
将需要备份的文件上传备份文件
客户端功能模块划分
- 数据管理模块:管理备份的文件信息
- 文件检测模块:监控指定文件夹,获取这个文件夹下所有的文件信息(通过获取到的备份文件信息)
- 文件备份模块:上传需要备份的文件数据,将数据传递给服务器
数据管理模块要管理的数据:
- . 判断一个文件是否需要重新备份
- 文件路径名,文件唯一标识符
客户端的开发环境在Windows上,使用vs2022(支持C++17即可 vs2017以上)
数据管理模块实现:
- 内存存储:使用哈希表
- 持久化存储:文件存储,自定义序列化格式(key Value型)
1. 客户端文件操作类
客户端数据管理模块设计和服务器相差不大可以直接移植
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include "log.hpp"
// #include <filesystem>
#include <experimental/filesystem>
namespace CloudBackups
{
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filepath; // 文件名称 uri格式
struct stat st; // 文件属性
public:
FileUtil() = default;
FileUtil(const std::string &filepath)
{
_filepath = filepath;
if (stat(_filepath.c_str(), &st) < 0)
{
LOG(WARNING, "get file stat failed! maybe this file not exits");
}
}
int64_t filesize() { return st.st_size; } // 获取文件大小,失败返回-1
time_t last_modify_time() { return st.st_mtime; } // 获取文件最后修改时间
time_t last_visit_time() { return st.st_atime; } // 获取文件最后访问时间
// 删除文件
bool removeFile()
{
if (this->isExit() == false)
{
return false;
}
if (remove(_filepath.c_str()) == 0)
{
return true;
}
else
{
return false;
}
}
std::string filename() // 文件名称
{
size_t pos = _filepath.find_last_of("/");
if (pos == std::string::npos)
{
return _filepath;
}
return _filepath.substr(pos + 1);
}
bool getPoslen(std::string &body, size_t pos, size_t len) // 从文件中读取len个字节,从pos位置开始读取,读取内容放到body中,为了实现断点续传
{
size_t size = this->filesize(); // 文件大小
if (pos >= size)
{
LOG(ERROR, "pos is out of range!");
return false;
}
if (pos + len > size)
{
LOG(ERROR, "pos + len is out of range!");
return false;
}
std::ifstream ifs;
ifs.open(_filepath.c_str(), std::ios::binary);
if (!ifs.is_open())
{
LOG(ERROR, "open file failed!");
return false;
}
ifs.seekg(pos, std::ios::beg);
body.resize(len);
ifs.read(&body[0], len);
if (!ifs.good())
{
// 上次读取出错
LOG(ERROR, "read file failed!");
ifs.close();
return false;
}
ifs.close();
return true;
}
bool getContent(std::string &body) // 获取整体的文件数据
{
size_t size = this->filesize();
return getPoslen(body, 0, size);
}
bool setContent(const std::string &body) // 设置文件内容
{
std::ofstream ofs;
ofs.open(_filepath.c_str(), std::ios::binary);
if (!ofs.is_open())
{
LOG(ERROR, "open file failed! file path=" + _filepath);
return false;
}
ofs.write(body.c_str(), body.size());
if (!ofs.good())
{
// 上次写入出错
LOG(ERROR, "write file failed!");
ofs.close();
return false;
}
ofs.close();
return true;
}
bool isExit() { return fs::exists(_filepath); } // 判断文件是否存在
bool mkdir() // 创建文件夹
{
if (this->isExit())
{
return true;
}
return fs::create_directories(_filepath);
}
bool mkdir(const std::string &path) // 创建文件夹
{
if (this->isExit())
{
return true;
}
return fs::create_directories(path);
}
bool ls(std::vector<std::string> &files) // 扫描文件夹,并返回里面的文件
{
for (auto &pos : fs::directory_iterator(_filepath))
{
if (fs::is_directory(pos) == true)
{
continue; // 目录不出来
}
files.push_back(fs::path(pos).relative_path().string()); // 获取文件的相对路径
}
return true;
}
};
}
2. 客户端数据管理模块设计
#pragma once
#include<string>
#include<unordered_map>
#include<sstream>
#include"fileutil.hpp"
namespace CloudBackups {
class DataManager {
private:
std::string backup_file;//备份信息持久化文件,需要在创建类时加载
std::unordered_map<std::string, std::string>backupMap;//管理文件路径和文件信息的映射
public:
// 切分字符串src,结果放到buff上
bool cutString(std::string& src, const std::string sep, std::vector<std::string>&buff)
{
size_t pos = 0;
size_t index = 0;
while (true)
{
pos = src.find(sep, index);
if (pos == std::string::npos) {
break;
}
if (pos == index) {
index = pos + sep.size();
continue;
}
std::string str = src.substr(index, pos - index);
buff.push_back(str);
index = pos + sep.size();
}
if (index < src.size()) {
//还剩数据
buff.push_back(src.substr(index));
}
return true;
}
DataManager(const std::string backup_file) {
this->backup_file = backup_file;
InitLoad();
}
bool Storage() {
//1. 获取所有备份信息
auto pos = backupMap.begin();
std::stringstream builder;
while (pos != backupMap.end()) {
//2. 将所有信息组织成特定格式
builder << pos->first << " " << pos->second << "\n";
pos++;
}
//3. 将所有信息写入文件
FileUtil tool(backup_file);
tool.setContent(builder.str());
return true;
}
bool InitLoad() {
//1. 从文件中读取所有数据
FileUtil tool(backup_file);
std::string body;
tool.getContent(body);
//2. 对数据进行解析,添加到backupMap中
//按照行进行分割,每行按照空格分割
std::vector<std::string>files;
cutString(body, "\n", files);
for (auto& file : files) {
std::vector<std::string>buff;
cutString(file, " ", buff);
if (buff.size() != 2) {
// LOG(ERROR, "cut string error!");
return false;
}
backupMap[buff[0]] = buff[1];
}
}
bool Insert(const std::string& key, const std::string& value) {
backupMap[key] = value;
Storage();
return true;
}
bool UpDate(const std::string& key, const std::string& value) {
backupMap[key] = value;
Storage();
return true;
}
bool GetByKey(const std::string& key, std::string& value) {
auto pos = backupMap.find(key);
if (pos == backupMap.end()) {
return false;
}
value = pos->second;
return true;
}
};
}
3. 文件客户端类设计
客户端实现:
- 自动将指定文件夹的备份文件备份到服务器上
流程:
- 遍历指定文件夹,获取文件信息
- 逐一判断文件是否需要备份,对需要备份的文件上传备份
需要注意的是:若文件比较大,正在拷贝过程,每次遍历文件都被修改,每次客户端都会上传不合理,所以要设置在一段时间没有被修改则上传
#pragma once
#include<string>
#include"backups.hpp"
#include<vector>
#include<sstream>
#include"httplib/httplib.h"
#include<Windows.h>
#define SEVER_IP "116.204.70.147"
#define SEVER_PORT 8081
namespace CloudBackups {
class Client {
private:
std::string back_dir;
DataManager* dataMange;
//判断文件标识符是否需要上传
bool CheckFileUpload(std::string filepath) {
//文件新增或文件修改过都需要上传 1. 文件新增:看下文件备份信息 2. 文件有历史信息,但是文件标识符不一致
std::string pre_id;
if (dataMange->GetByKey(filepath, pre_id) != false) {
//判断文件更新
std::string id = GetEtag(filepath);
if (id != pre_id) {
//前后信息不一致,需要上传
//文件比较大,正在拷贝过程,每次遍历文件都被修改,每次客户端都需要上传不合理,所以要设置在一段时间没有被修改则上传
FileUtil tool(filepath);
if (time(nullptr) - tool.last_modify_time() > 3) {
//3秒之内未修改,需要上传给服务器
return true;
}
}
return false;//前后信息一致或者客户端文件拷贝未完成,不需要上传
}
//文件不存在,需要上传
return true;
}
public:
Client(const std::string& back_dir, const std::string& back_file) {
this->back_dir = back_dir;
dataMange = new DataManager(back_file);
//创建上传目录文件夹
FileUtil tool;
tool.mkdir(back_dir);
}
//创建文件唯一标识符
std::string GetEtag(const std::string& filepath) {
//文件名-文件大小-修改时间
FileUtil tool(filepath);
std::stringstream builder;
builder << tool.filename() << "-" << tool.filesize() << "-" << tool.last_modify_time();
return builder.str();
}
//文件上传接口
bool Upload(const std::string filepath) {
//获取文件数据
FileUtil tool(filepath);
std::string body;
tool.getContent(body);
//搭建客户端发送请求
httplib::Client client(SEVER_IP, SEVER_PORT);
httplib::MultipartFormData item;
item.content = body;
//item.filename = tool.filename();
item.name = "file";//服务器上标识的是file,需要和服务器协商
item.content_type = "application/octet-stream";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto ret = client.Post("/upload", items);
if (!ret || ret->status != 200) {
LOG(ERROR, "file upload error!");
return false;
}
return true;
}
//服务器运行
bool RunModule() {
while (true) {
//1. 浏览需要备份的文件
FileUtil tool(back_dir);
std::vector<std::string>files;
tool.ls(files);
for (auto& file : files) {
//2. 逐个判断文件需要上传
if (CheckFileUpload(file)==false) {
//不需要上传
continue;
}
//需要上传服务器
if (Upload(file) == true) {
dataMange->Insert(file, GetEtag(file));//上传服务器完毕后写入文件配置文件
LOG(INFO, "upload success!");
}
else {
LOG(ERROR, "upload error! filepath: " + file);
}
}
Sleep(10);//毫秒为单位
}
return true;
}
};
}