【云备份】客户端实现 及 项目整体总结
文章目录
- 客户端
- 客户端实现思想
- 客户端文件操作类的设计与拷贝
- Util.hpp的设计
- data.hpp的设计
- Storage —— 持久化存储
- Initload——数据初始化加载
- cloud.hpp的设计
- GetFileIdentifier——创建文件唯一标识
- Upload—— 文件上传
- IsNeedupload —— 客户端文件是否需要上传判断
- RunModule —— 运行模块
- 项目总结
- 整体代码
- 服务器端(Linux下实现)
- util.hpp(实用文件工具模块)
- service.hpp(业务处理模块)
- makefile
- hot.hpp(热点管理模块)
- data.hpp (数据管理模块)
- config.hpp(配置加载文件模块)
- cloud.conf
- 客户端(VS2022下实现)
- util.hpp(实用文件工具模块)
- data.hpp (数据管理模块)
- cloud.hpp (文件备份模块)
- cloud.cpp
客户端
客户端实现思想
客户端实现功能:
自动对指定文件夹的文件进行备份
数据管理模块的实现思想:
内存存储 :高访问效率 使用hash表_table
持久化存储:文件存储
文件存储涉及到数据的序列化 而在VS中安装jsoncpp麻烦 直接自定义序列化格式 key value
key表示文件路径名 value表示 文件唯一标识
文件的唯一标识:用于判断上次文件上传后有没有被修改过
客户端文件操作类的设计与拷贝
以下操作都在VS中进行
Util.hpp的设计
客户端文件操作类 与 服务端的文件实用工具类 基本没有差别
所以直接复制 util.hpp
把 FileUtil类中 的 compress 压缩函数 与 uncompress 解压缩 函数 以及 json类(序列化与反序列化) 删除

data.hpp的设计
创建 data.hpp
在cloud命名空间中 设计 DataManger 类 用于设计客户端的数据管理模块

_backup_file 用于备份信息的持久化存储文件
_table 是一个 key值为string value值为string 的哈希表 用于存储数据
Storage —— 持久化存储

遍历_table哈希表,将表中的key和value值都写入到ss字符串流中
使用 _backup_file 备份信息持久化存储文件 实例化一个 FileUtil类的 对象 fu
调用 FileUtil类的 SetContent 函数 将 ss字符串流中的数据 写入到 fu中
Initload——数据初始化加载
想要实现 InitLoad函数 就需要先实现分割功能
按照换行进行分割 得到一行一行的数据
在每一行中 按照空格进行分割 即可得到 文件名和唯一标识
所以创建Split函数 实现分割功能

定义查找到分隔符的位置pos 以及 起始偏移量 idx 并初始化为0

find函数 的第一个参数为查找的字符 第二个参数为 偏移量
在while循环中 依次寻找对应的分隔符位置pos 以idx起始偏移量起始寻找

说明数据之前存在两个以上的分隔符 ,则偏移量为当前pos分隔符位置再加上分隔符个数
并重新查找分隔符位置pos

substr函数的第一个参数为 截取起始位置 第二个参数为长度
例如: abc qwe sdf
借助 substr 函数 从idx偏移量处 进行截取 pos位置为当前空格位置 pos-idx 即 截取的数据长度
再将tmp添加到 arry数组中
count 数量加1 表示 数据增添一个

若为最后一个数据 则查找不到空格直接跳出循环 还需将最后一个数据放入数组arry中


通过 _backup_file (备份信息的持久化存储文件) 实例化一个 FileUtil类的对象 fu
将_backup_file中的数据 读取到body字符串中

再将body中的数据通过 Split 函数 进行分割成一行一行的数据 并添加到arry数组中
此时arry数组中的元素 即为一行数据

想要把一行数据分割成文件名和唯一标识
就还需要借助 Split 函数 将一行数据通过空格分割开
最终在哈希表中使文件名和唯一标识 一 一对应

cloud.hpp的设计

设计Backup类 设置私有成员变量
_back_dir 要监控的文件夹
_data 数据管理类
GetFileIdentifier——创建文件唯一标识

通过filename 实例化一个对象fu
创建一个 字符串流 ss
通过调用 FileUtil类的函数 将文件名 - 文件大小 - 最后一次修改时间 写入到ss中
最后返回ss的字符串格式
Upload—— 文件上传

用 宏定义 IP地址 192.144.206.100(云服务器的公网IP) 为 SERVER_ADDR
用 宏定义 port端口号 9090 为 SERVER_PORT

通过filename 实例化一个 FileUtil类的对象 fu
再通过FileUtil类的 GetContent 函数 将 filename中的数据 传入 body字符串中
由于需要httplib,h 头文件 所以要把linux下的 该头文件 先传到桌面上 再导入 VS中

将属于linux下的 httplib.h 头文件 发送到桌面上

将VS的cloud_client 客户端 的路径复制下来 并打开

将httplib.h头文件托送到当前路径下

打开ckoud_client 客户端 添加 头文件 httplib.h

通过宏定义的 IP地址和 端口号 实例化一个 Client类的对象 client

MultipartFormDataMap 内部包含四个字段
name为字段名称
content为文件内容
filename为文件名称
coneent_type 为正文类型

创建 MultipartFormDataMap 类型的对象 item
文件内容为 body字符串中的数据
文件名称为 FileName函数 返回 的文件名
辨别字段为 file(与服务器端的serice 中的 upload函数 相同)
application / octet - stream 表示二进制流数据

调用httplib库中的Client类的 Post请求
因为要与服务器端的资源路径相统一 所以使用 /upload 进行 Post请求
以及items(将文件内容 文件名称 数据类型等上传)

IsNeedupload —— 客户端文件是否需要上传判断
若 文件是新增的 则需判断是否为历史备份信息

调用Datamanger 类的 GetOneBykey 函数 判断是否为唯一标识
若返回false 则说明找到了对应的历史备份信息 并将标识信息存储到id中
调用cloud类的 GetFileIdentifier 函数 获取新的标识信息 new_id
若 id 与 new_id 相等 则说明与历史文件信息一致 就不需要再次上传了
若id与new_id不相等 则说明 备份信息被修改了 需要重新上传
但在其中需要考虑一种特殊情况
有可能一个文件比较大 所以正在一点一点拷贝到这个目录下
拷贝需要一个过程 如果每次遍历 都会判断标识不一致 需要上传
就可能上传 上百次 这样就非常不合理
因此应该判断一个文件 在一段时间都没有被修改过 才能上传

time(NULL) 表示当前系统时间
LastMtime 函数表示 最后一次修改时间
若3秒内被修改过 就认为文件还在修改中 就不需要上传

RunModule —— 运行模块

通过 _back_dir 要监控的文件夹 实例化一个 FileUtil类的对象 fu
再通过 FileUtil类的 ScanDirectory 函数 将_back_dir的数据 传入 arry数组中

遍历arry数组 将数组中的每一个元素 通过 IsNeedUpload 函数 进行判断
看是否需要上传 若不需要上传 则重新遍历到下一个数组元素
通过Upload 函数 进行上传
若上传成功 则将文件名称 和唯一标识 通过insert 进行添加 构成新的备份文件信息

项目总结
项目名称:云备份系统
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问
文件进行压缩存储。
开发环境: centos7.6/vim、g++、gdb、makefile 以及 windows10/vs2022
技术特点: http 客户端/服务器搭建, json 序列化,文件压缩,热点管理,断点续传,,读写锁(读共享 写互斥),单例模式
项目模块:
服务端:
数据管理模块: 内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
业务处理模块: 搭建 http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并支持断点续
传
热点管理模块: 对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间。
客户端
数据管理模块: 内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
文件检索模块: 基于 c++17 文件系统库,遍历获取指定文件夹下所有文件。
文件备份模块: 搭建 http 客户端上传备份文件。
整体代码
服务器端(Linux下实现)
util.hpp(实用文件工具模块)
#ifndef _MY_UTIL_
#define _MY_UTIL_
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<sys/stat.h>
#include"bundle.h"
#include<experimental/filesystem>
#include<jsoncpp/json/json.h>
#include<memory>
namespace cloud
{
namespace fs=std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename;//文件名称
public:
FileUtil(const std::string &filename):_filename(filename)//构造函数
{}
bool Remove()//删除文件
{
//若文件不存在 相当于删除成功 则返回true
if(this->Exists()==false)
{
return true;
}
remove(_filename.c_str());
return true;
}
int64_t FileSize()//文件大小
{
struct stat st;
if( stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed!\n";
return -1;
}
return st.st_size;
}
time_t LastMTime()//文件最后一次修改时间
{
struct stat st;
if( stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed!\n";
return -1;
}
return st.st_mtime;
}
time_t LastATime()//文件最后一次访问时间
{
struct stat st;
if( stat(_filename.c_str(),&st)<0)
{
std::cout<<"get file size failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName()//文件名称
{
// ./abc/test.txt
size_t pos=_filename.find_last_of("/");
if(pos==std::string::npos)
{
return _filename;
}
return _filename.substr(pos+1);
}
bool GetPostLen(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<<"read file failed"<<std::endl;
return false;
}
//打开成功,获取文件数据
size_t fsize=this->FileSize();//获取文件大小
if(pos+len>fsize)//若pos开始位置超过了文件大小
{
std::cout<<"get file len is error"<<std::endl;
return false;
}
ifs.seekg(pos,std::ios::beg);//从文件起始位置偏移到pos位置处
body->resize(len);
ifs.read(&(*body)[0], len);//读取文件所有数据到 body中
if(ifs.good()==false)//读取出错
{
std::cout<< "get file content fialed "<<std::endl;
ifs.close();
return false;
}
ifs.close();
return true;
}
bool GetContent(std::string *body) //获取整体文件数据
{
size_t fsize=this->FileSize();//获取文件大小
return GetPostLen(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<<"write open file failed"<<std::endl;
return false;
}
ofs.write(&body[0], body.size());//将body数据写入到文件中
if(ofs.good()==false)//写入失败
{
std::cout<<"write file content failed"<<std::endl;
ofs.close();
return false;
}
ofs.close();
return true;
}
bool Compress(const std::string &packname) //压缩
{
//读取文件数据
std::string body;
if(this->GetContent(&body)==false)
{
std::cout<<"compress get file content failed"<<std::endl;
return false;
}
//对数据进行压缩
std::string packed=bundle::pack(bundle::LZIP,body);
//将压缩数据存储到压缩包文件中
FileUtil fu(packname);
if(fu.SetContent(packed)==false)//写入数据失败
{
std::cout<<"compress write packed data failed"<<std::endl;
return false;
}
return true;
}
bool UnCompress(const std::string &filename)//解压缩
{
//将当前压缩包数据读取出来
std::string body;
if(this->GetContent(&body)==false)//获取文件内容
{
std::cout<<"compress get file content failed"<<std::endl;
return false;
}
//对压缩的数据进行解压缩
std::string unpacked=bundle::unpack(body);
//将解压缩的数据写入到新文件中
FileUtil fu(filename);
if(fu.SetContent(unpacked)==false)//写入数据失败
{
std::cout<<"uncompress write packed data failed"<<std::endl;
return false;
}
return true;
}
bool Exists()//判断文件是否存在
{
return fs::exists(_filename);
}
bool CreateDirectory()//创建目录
{
if(this->Exists())
{
return true;
}
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string> * arry)//浏览目录
{
for (auto & p : fs::directory_iterator(_filename))//遍历目录
{
if(fs::is_directory(p)==true)//检测遍历到的文件 是一个文件夹 就不进行操作
{
continue;
}
//普通文件才进行操作
//relative_path 表示带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
class JsonUtil
{
public:
// 序列化 将结构化数据 转化为字符串
static bool Serialize(const Json::Value &root, std::string *str)
{
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root,&ss);
*str=ss.str();
return true;
}
// 反序列化 将字符串 转化为 结构化数据
static bool UnSerialize(const std::string &str, Json::Value *root)
{
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
bool ret=cr->parse(str.c_str(),str.c_str()+str.size(),root,&err);
if(ret==false)
{
std::cout<<"parse error:"<<err<<std::endl;
return false;
}
return true;
}
};
}
#endif
service.hpp(业务处理模块)
#ifndef _MY_SERVICE_//避免头文件重复包含
#define _MY_SERVICE_
#include "data.hpp"
#include "httplib.h"
extern cloud::DataManager *_data;
namespace cloud
{
class Service
{
private:
int _server_port;
std::string _server_ip;
std::string _download_prefix;
httplib::Server _server;
private:
static void Upload(const httplib::Request &req,httplib::Response &rsp)//上传请求
{
// post /upload 文件数据在正文中
auto ret=req.has_file("file");//判断有没有上传的文件区域
if(ret==false)
{
rsp.status=400;
return;
}
//若有上传的文件区域 则获取文件包含的各项数据
const auto& file =req.get_file_value("file");
//file.filename文件名称 file.content 文件内容
std::string back_dir= Config::GetInstance()->GetBackDir();
std::string realpath=back_dir+FileUtil(file.filename).FileName();
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中
BackupInfo info;
info.NewBackupInfo(realpath);//组织备份文件信息
_data->Insert(info);//向数据管理模块添加备份文件信息
return;
}
static std::string TimetoStr(time_t t)
{
std::string tmp=std::ctime(&t);
return tmp;
}
static void ListShow(const httplib::Request &req,httplib::Response &rsp)// 获取展示页面处理
{
//获取所有文件信息
std::vector<BackupInfo> arry;
_data->GetAll(&arry);
//根据所有备份信息 组织html文件数据
std::stringstream ss;
ss << "<html><head><title>Download</title><meta charset='UTF-8'></head>";
ss<<"<body><hl>Download</hl><table>";
for(auto &a:arry)
{
ss<<"<tr>";
std::string filename=FileUtil(a.real_path).FileName();
ss<<"<td><a href =' "<<a.url<<" '>"<<filename<< "</a></td>";
ss<<"<td align='right'>"<<TimetoStr(a.mtime)<<"</td>";
ss<<"<td align ='right'>"<<a.fsize/1024<< "k</td>";
ss<<"</tr>";
}
ss<<"</table></body></html>";
rsp.body=ss.str();
rsp.set_header("Content-Type","text/html");//设置头部信息
rsp.status=200;//状态码设置为200 表示成功响应
return ;
}
static std::string GetETag(const BackupInfo & info)//获取 ETag字段
{
// etag 文件名 - filename-fsize 文件大小 - mtime 最后一次修改时间
FileUtil fu(info.real_path);
std::string etag =fu.FileName();//获取文件名
etag+= "-";
etag+= std::to_string(info.fsize);//获取文件大小(字符串格式)
etag+="-";
etag+=std::to_string(info.mtime);//获取最后一次修改时间(字符串格式)
return etag;
}
static void Download(const httplib::Request &req,httplib::Response &rsp)//文件下载请求
{
// 获取客户端请求的资源路径 req.path
// 根据资源路径 获取文件备份信息
BackupInfo info;
_data->GetOneByURL(req.path,&info);//通过URL获取单个数据
//判断文件是否被压缩 如果被压缩 需要先解压缩
if(info.pack_flag==true)//说明被压缩了
{
FileUtil fu(info.pack_path); //使用压缩包路径实例化一个对象
fu.UnCompress(info.real_path);//解压缩
// 删除压缩包 修改备份信息
fu.Remove();//删除文件
info.pack_flag=false;
_data->Update(info);//更新
}
// 读取文件数据 放入 rsp.body中
FileUtil fu(info.real_path);
fu.GetContent(&rsp.body);//将 real_path中的数据 放入 body中
bool retrans =false;//retrans表示断点续传
std::string old_etag;
if(req.has_header("If-Range"))//若存在该头部字段 则说明为断点续传
{
old_etag =req.get_header_value("If-Range");//将头部字段对应的value值传给 old_etag
if(old_etag ==GetETag(info))//若If-Range字段 的值 与请求文件的最新etag一致 则符合断点续传
{
retrans =true;
}
}
//若没有 If-Range 字段 则为正常下载
//如果有这个字段 但是它的值与当前文件的 etag 不一致 则必须返回全部数据
if(retrans==false)
{
//正常下载
fu.GetContent(&rsp.body);//将 real_path中的数据 放入 body中
//设置响应头部字段 ETag Accept-Ranges: bytes
rsp.set_header("Accept-Ranges","bytes");//建立头部字段
rsp.set_header("ETag",GetETag(info));
rsp.set_header("Content-Type","application/octet-stream");
rsp.status=200;//响应成功
}
else
{
//说明是断点续传
//httplib 内部实现对于 断点续传请求的处理
//只需要用户 将文件所有数据读取到rsp.body中
//内部会自动根据请求区间 从body中取出指定区间 数据 进行响应
fu.GetContent(&rsp.body);//将 real_path中的数据 放入 body中
rsp.set_header("Accept-Ranges","bytes");//建立头部字段
rsp.set_header("ETag",GetETag(info));
rsp.set_header("Content-Type","application/octet-stream");
rsp.status=206; //区间请求响应
}
}
public:
Service()//构造函数
{
Config * config=Config::GetInstance();//创建对象
_server_port=config->GetServerPort();
_server_ip=config->GetServerIp();
_download_prefix =config->GetDownloadPrefix();
}
bool RunModule()//运行模块
{
_server.Post("/upload",Upload);
_server.Get("/listshow", ListShow);
_server.Get("/", ListShow);
std::string download_url =_download_prefix+"(.*)";//下载请求
_server.Get(download_url,Download);
_server.listen(_server_ip.c_str(),_server_port);
return true;
}
};
}
#endif
makefile
.PHONY: cloud
cloud:cloud.cpp util.hpp
g++ -g $^ -o $@ -L./lib -lpthread -lstdc++fs -ljsoncpp -lbundle
hot.hpp(热点管理模块)
#ifndef _MY_HOT_
#define _MY_HOT_
#include<unistd.h>
#include"data.hpp"
extern cloud::DataManager *_data;
namespace cloud
{
class HotManager
{
private:
std::string _back_dir;// 备份文件路径
std::string _pack_dir;// 压缩文件路径
std::string _pack_suffix;//压缩包后缀名
int _hot_time; // 热点时间
private:
bool Hotjudge(const std::string &filename)//热点判断
{
FileUtil fu(filename);
time_t last_atime=fu.LastATime();//文件最后一次访问时间
time_t cur_time=time(NULL);//当前系统时间
if(cur_time-last_atime>_hot_time)
{
//差值超过设定的值 所以为非热点文件
return true;
}
//差值小于设定的值 所以为热点文件
return false;
}
public:
HotManager() //构造函数
{
Config* config=Config::GetInstance();//创建对象
_back_dir=config->GetBackDir();
_pack_dir=config->GetPackDir();
_pack_suffix=config->GetPackFileSuffix();
_hot_time=config->GetHotTime();
FileUtil tmp1(_back_dir);
FileUtil tmp2(_pack_dir);
//为了防止目录不存在 则创建目录
tmp1.CreateDirectory();//创建目录
tmp2.CreateDirectory();
}
bool RunModule()//运行模块
{
while(1)
{
//遍历备份目录 获取所有文件名
FileUtil fu(_back_dir);
std::vector<std::string>arry;
fu.ScanDirectory(&arry);
//遍历判断文件是否是非热点文件
for(auto &a :arry)
{
if(Hotjudge(a)==false)//说明为热点文件
{
//热点文件不需要处理 所以直接跳过
continue;
}
//获取文件备份信息
cloud::BackupInfo bi;
//GetOneByRealPat 通过realpath获取单个数据
if(_data->GetOneByRealPath(a,&bi)==false)
{
//有文件存在 但是没有备份信息
bi.NewBackupInfo(a);
}
//对非热点文件进行压缩处理
FileUtil tmp(a);
tmp.Compress(bi.pack_path);//压缩
//删除源文件 修改备份信息
tmp.Remove();
bi.pack_flag=true;//表示压缩成功
_data->Update(bi);//将bi更新到_table哈希表中
}
usleep(1000);
}
return true;
}
};
}
#endif
data.hpp (数据管理模块)
#ifndef _MY_DATA_
#define _MY_DATA_
#include<unordered_map>
#include<pthread.h>
#include"config.hpp"
namespace cloud
{
typedef struct BackupInfo
{
bool pack_flag;//压缩标志
size_t fsize; //文件大小
time_t mtime; //最后一次修改时间
time_t atime; //最后一次访问时间
std::string real_path;//文件实际存储路径
std::string pack_path;//压缩包存储路径名称
std::string url; //请求资源路径
bool NewBackupInfo(const std::string &realpath)//获取各项属性信息
{
FileUtil fu(realpath);
if(fu.Exists()==false)
{
std::cout<<"new backupinfo file not exists" <<std::endl;
return false;
}
Config* config=Config::GetInstance();//创建对象
std::string packdir=config->GetPackDir();//压缩包存放路径
std::string packsuffix=config->GetPackFileSuffix();//压缩包后缀名称
std::string download_prefix =config->GetDownloadPrefix();//URL前缀路径
this->pack_flag=false;
this->fsize=fu.FileSize();
this->mtime=fu.LastMTime();
this->atime=fu.LastATime();
this->real_path=realpath;
this->pack_path = packdir+fu.FileName()+packsuffix;
// ./backdir/a.txt -> ./packdir/a.txt.lz
this->url=download_prefix + fu.FileName();
//./backdir/a.txt -> /download/a.txt
return true;
}
}BackupInfo;
class DataManager
{
private:
std::string _backup_file;//数据持久化存储文件
pthread_rwlock_t _rwlock;//读写锁
std::unordered_map<std::string,BackupInfo> _table;//哈希表
public:
DataManager()//构造函数
{
_backup_file=Config::GetInstance()->GetBackupFile();//数据信息存放文件
pthread_rwlock_init(&_rwlock,NULL);//对读写锁初始化
InitLoad();
}
~DataManager()//析构函数
{
pthread_rwlock_destroy(&_rwlock);//对读写锁进行销毁
}
bool Insert(const BackupInfo &info)//新增
{
pthread_rwlock_wrlock(&_rwlock);//加写锁
_table[info.url]=info;
pthread_rwlock_unlock(&_rwlock);//解锁
Storage();
return true;
}
bool Update(const BackupInfo& info)//更新
{
pthread_rwlock_wrlock(&_rwlock);//加写锁
_table[info.url]=info;
pthread_rwlock_unlock(&_rwlock);//解锁
Storage();
return true;
}
bool GetOneByURL(const std::string &url,BackupInfo*info)//通过URL获取单个数据
{
pthread_rwlock_wrlock(&_rwlock);//加写锁
//因为url是key值 所以可以直接通过key值来进行查找
auto it=_table.find(url);
if(it==_table.end())
{
return false;
}
*info= it->second;//获取url对应的info
pthread_rwlock_unlock(&_rwlock);//解锁
return true;
}
bool GetOneByRealPath(const std::string &realpath ,BackupInfo*info)//通过realpath获取单个数据
{
pthread_rwlock_wrlock(&_rwlock);//加写锁
auto it=_table.begin();
for(;it!=_table.end();++it)//遍历
{
if(it->second.real_path==realpath)
{
*info=it->second;
pthread_rwlock_unlock(&_rwlock);//解锁
return true;
}
}
pthread_rwlock_unlock(&_rwlock);//解锁
return false;
}
bool GetAll(std::vector<BackupInfo>*arry) //获取所有
{
pthread_rwlock_wrlock(&_rwlock);//加写锁
auto it=_table.begin();
for(;it!=_table.end();++it)//遍历
{
arry->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);//解锁
return true;
}
bool Storage()//持久化存储实现
{
//获取所有数据
std::vector<BackupInfo> arry;
this->GetAll(&arry);//获取所有数据放入arry中
//添加到json::value中
Json::Value root;
for(int i=0;i<arry.size();i++)
{
Json::Value item;
item["pack_flag"]= arry[i].pack_flag;
item["fsize"]= (Json::Int64)arry[i].fsize;
item["atime"]= (Json::Int64)arry[i].atime;
item["mtime"]= (Json::Int64)arry[i].mtime;
item["real_path"]= arry[i].real_path;
item["pack_path"]= arry[i].pack_path;
item["url"]= arry[i].url;
root.append(item); //添加数组元素item
}
// 对json::value 序列化
std::string body;
JsonUtil::Serialize(root,&body);//序列化
//写文件
FileUtil fu(_backup_file);//数据持久化存储文件
fu.SetContent(body);
return true;
}
bool InitLoad()//初始化加载
{
//将数据文件中的数据读取出来
FileUtil fu(_backup_file);//数据持久化存储文件
if(fu.Exists()==false)//若没有数据 则直接返回true 就不用进行加载了
{
return true;
}
std::string body;
fu.GetContent(&body);//将_backup_file文件中的数据 全部读取到body中
//反序列化
Json::Value root;
JsonUtil::UnSerialize(body,&root);//反序列化 将body字符串转化为 root结构化数据
//将反序列化得到的Json::Value中的数据添加到table中
for(int i=0;i<root.size();i++)
{
BackupInfo info;
info.pack_flag =root[i]["pack_flag"].asBool();
info.fsize =root[i]["fsize"].asInt64();
info.atime =root[i]["atime"].asInt64();
info.mtime =root[i]["mtime"].asInt64();
info.pack_path =root[i]["pack_path"].asString();
info.real_path =root[i]["real_path"].asString();
info.url =root[i]["url"].asString();
Insert(info);//插入
}
return true;
}
};
}
#endif
config.hpp(配置加载文件模块)
//防止头文件被重复包含
#ifndef _MY_CONFIG_
#define _MY_CONFIG_
#include"util.hpp"
#include<mutex>
namespace cloud
{
#define CONFIG_FILE "./cloud.conf"
class Config
{
private:
Config()
{
ReadConfigFile();//读取配置文件信息
}
static Config* _instance;
static std::mutex _mutex;
private:
int _hot_time; //热点判断时间
int _server_port; //服务器的监听端口
std::string _server_ip; //下载的url前缀路径
std::string _download_prefix; // 压缩包后缀名称
std::string _packfile_suffix; //备份文件存放目录
std::string _pack_dir; // 压缩包存放目录
std::string _back_dir; // 服务器IP地址
std::string _backup_file; // 数据信息存放文件
bool ReadConfigFile()//读取配置文件
{
FileUtil fu(CONFIG_FILE);
std::string body;
//获取文件内容到body中
if(fu.GetContent(&body) ==false)//读取失败
{
std::cout<<"load config file failed"<<std::endl;
return false;
}
Json::Value root;
if(JsonUtil::UnSerialize(body,&root)==false)//反序列化 字符串转化为结构化数据
{
std::cout<<"parse config file failed"<<std::endl;
return false;
}
_hot_time=root["hot_time"].asInt();
_server_port=root["server_port"].asInt();
_server_ip=root["server_ip"].asString();
_download_prefix=root["download_prefix"].asString();
_packfile_suffix=root["packfile_suffix"].asString();
_pack_dir =root["pack_dir"].asString();
_back_dir =root["back_dir"].asString();
_backup_file=root["backup_file"].asString();
return true;
}
public:
static Config *GetInstance() //创建对象
{
if(_instance==NULL)
{
_mutex.lock();//加锁
//若指针为空 则创建对象
if(_instance==NULL)
{
_instance= new Config();//实例化对象
}
_mutex.unlock();
}
return _instance;
}
int GetHotTime()//获取热点时间
{
return _hot_time;
}
int GetServerPort() //端口号
{
return _server_port;
}
std::string GetServerIp() //IP地址
{
return _server_ip;
}
std::string GetDownloadPrefix()//URL前缀路径
{
return _download_prefix;
}
std::string GetPackFileSuffix()//压缩包后缀名称
{
return _packfile_suffix;
}
std::string GetPackDir() //压缩包存放路径
{
return _pack_dir;
}
std::string GetBackDir()//备份文件存放目录
{
return _back_dir;
}
std::string GetBackupFile()//数据信息存放文件
{
return _backup_file;
}
};
Config* Config::_instance=NULL;
std::mutex Config::_mutex;
}
#endif
cloud.conf
{
"hot_time": 30,
"server_port":9090,
"server_ip":"10.0.16.6",
"download_prefix":"/download/",
"packfile_suffix":".lz",
"pack_dir": "./packdir/",
"back_dir": "./backdir/",
"backup_file":"./cloud.dat"
}
客户端(VS2022下实现)
util.hpp(实用文件工具模块)
#ifndef _MY_UTIL_
#define _MY_UTIL_
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<sys/stat.h>
#include<experimental/filesystem>
#include<memory>
namespace cloud
{
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename;//文件名称
public:
FileUtil(const std::string& filename) :_filename(filename)//构造函数
{}
bool Remove()//删除文件
{
//若文件不存在 相当于删除成功 则返回true
if (this->Exists() == false)
{
return true;
}
remove(_filename.c_str());
return true;
}
size_t FileSize()//文件大小
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed!\n";
return 0;
}
return st.st_size;
}
time_t LastMTime()//文件最后一次修改时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed!\n";
return -1;
}
return st.st_mtime;
}
time_t LastATime()//文件最后一次访问时间
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName()//文件名称
{
// ./abc/test.txt
size_t pos = _filename.find_last_of("\\");
if (pos == std::string::npos)
{
return _filename;
}
// return fs::path(_filename).filename().string();
return _filename.substr(pos + 1);
}
bool GetPostLen(std::string* body, size_t pos, size_t len)//获取文件数据
{
//打开成功,获取文件数据
size_t fsize = this->FileSize();//获取文件大小
if (pos + len > fsize)//若pos开始位置超过了文件大小
{
std::cout << "get file len is error" << std::endl;
return false;
}
std::ifstream ifs;
ifs.open(_filename, std::ios::binary);//以二进制方式打开文件
if (ifs.is_open() == false)
{
std::cout << "read file failed" << std::endl;
return false;
}
ifs.seekg(pos, std::ios::beg);//从文件起始位置偏移到pos位置处
body->resize(len);
ifs.read(&(*body)[0], len);//读取文件所有数据到 body中
if (ifs.good() == false)//读取出错
{
std::cout << "get file content fialed " << std::endl;
ifs.close();
return false;
}
ifs.close();
return true;
}
bool GetContent(std::string* body) //获取整体文件数据
{
size_t fsize = this->FileSize();//获取文件大小
return GetPostLen(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 << "write open file failed" << std::endl;
return false;
}
ofs.write(&body[0], body.size());//将body数据写入到文件中
if (ofs.good() == false)//写入失败
{
std::cout << "write file content failed" << std::endl;
ofs.close();
return false;
}
ofs.close();
return true;
}
bool Exists()//判断文件是否存在
{
return fs::exists(_filename);
}
bool CreateDirectory()//创建目录
{
if (this->Exists())
{
return true;
}
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string>* arry)//浏览目录
{
this->CreateDirectory();//创建目录
for (auto& p : fs::directory_iterator(_filename))//遍历目录
{
if (fs::is_directory(p) == true)//检测遍历到的文件 是一个文件夹 就不进行操作
{
continue;
}
//普通文件才进行操作
//relative_path 表示带有路径的文件名
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
}
#endif
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()//持久化存储
{
//获取所有的备份信息
std::stringstream ss;//字符串流
auto it = _table.begin();
for (; it != _table.end(); it++)//遍历_table
{
//将所有信息进行指定持久化格式的组织
ss << it->first << " " << it->second << "\n";
}
//持久化存储
FileUtil fu(_backup_file);
fu.SetContent(ss.str());//将ss流中的数据写入到fu中
return true;
}
//分割字符串数据
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
pos = str.find(sep, idx);
if (pos == std::string::npos)//找不到分隔符就退出循环
{
break;
}
if (pos == idx)//若有两个以上分隔符 则跳过分隔符则为起始偏移量
{
idx = pos + sep.size();
continue;
}
std::string tmp = str.substr(idx, pos - idx);//tmp为获取数据大小
arry->push_back(tmp);
idx += pos + sep.size();
count++;
}
//最后一个数据也需放入arry数组中
if (idx < str.size())
{
arry->push_back(str.substr(idx));
count++;
}
return count;
}
bool InitLoad()//初始化加载
{
//从文件中读取所有数据
FileUtil fu(_backup_file);
std::string body;
fu.GetContent(&body);//将数据读取到body中
//进行数据解析 添加到表中
std::vector<std::string>arry;
Split(body,"\n",&arry);//分割字符串数据 放入arry数组中
for (auto& a : arry)
{
//在arry数组中 的每一个元素 分为 文件名 唯一标识
std::vector<std::string>tmp;
Split(a, " ", &tmp);//分割出文件名和唯一标识
if (tmp.size() != 2)
{
continue;
}
//tmp[0]文件名 tmp[1]唯一标识
_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.hpp (文件备份模块)
#ifndef _MY_CLOUD_ //防止头文件重复包含
#define _MY_CLOUD_
#include"data.hpp"
#include"httplib.h"
#include<windows.h>
namespace cloud
{
#define SERVER_ADDR "192.144.206.100"
#define SERVER_PORT 9090
class Backup
{
private:
std::string _back_dir;
DataManager* _data;
public:
Backup(const std::string& back_dir, const std::string& back_file)//构造函数
:_back_dir(back_dir)
{
_data = new DataManager(back_file);
}
std::string GetFileIdentifier(const std::string &filename)//创建文件唯一标识
{
//a.txt -fsize(文件大小)-mtime(最后一次修改时间)
FileUtil fu(filename);
std::stringstream ss;
ss << fu.FileName() << "-" << fu.FileSize() << "-" << fu.LastMTime();
return ss.str();
}
bool Upload(const std::string &filename)//文件上传
{
//获取文件数据
FileUtil fu(filename);
std::string body;
fu.GetContent(&body);//读取文件数据到body中
//搭建http客户端上传 文件数据
httplib::Client client(SERVER_ADDR,SERVER_PORT);
httplib::MultipartFormData item;
item.content = body; //文件内容
item.filename = fu.FileName(); //文件名称
item.name = "file"; //辨识字段
item.content_type = "application/octet-stream"; //文件数据类型
httplib::MultipartFormDataItems items;
items.push_back(item);//将信息添加进去
auto res = client.Post("/upload", items);//发送post请求 URL资源路径upload
if (!res || res->status != 200)//res不存在 或者状态码不为200
{
//失败
return false;
}
return true;
}
bool IsNeedUpload(const std::string &filename)//判断一个文件是否需要上传
{
//需要上传的文件的判断条件 1.文件是新增的 2.不是新增的但是被修改过
//若文件是新增的: 则只需看有没有历史备份信息 若有则为新增的
std::string id;
if (_data->GetOneByKey(filename, &id) != false)//找到了对应的历史信息
{
std::string new_id = GetFileIdentifier(filename);//获取新的标识信息
if (new_id == id)//与历史文件信息一致 则不需要上传
{
return false;
}
}
//不是新增的但是被修改过:有历史信息 但是历史的唯一标识与当前最新的唯一标识不一致
FileUtil fu(filename);
//若3秒钟内被修改过 就认为文件还在修改中
if (time(NULL) - fu.LastMTime() < 3)
{
return false;
}
std::cout << filename << " need upload! \n";
return true;
}
bool RunModule()//运行模块
{
while (1)
{
//1.遍历指定文件夹中的所有文件
FileUtil fu(_back_dir);
std::vector<std::string>arry;
fu.ScanDirectory(&arry);//将_back_dir中的数据传入arry数组中
//2.逐个判断文件是否需要上传
for (auto& a : arry)
{
if (IsNeedUpload(a) == false)//不需要上传
{
continue;
}
//3.如果需要上传 则上传文件
if (Upload(a) == true)
{
//若上传成功 则新增文件备份信息
// 备份信息: 文件名称 唯一标识
_data->Insert(a, GetFileIdentifier(a));
std::cout << a << "upload success!\n";
}
}
Sleep(1);
std::cout << "------loop end -----\n";
}
}
};
}
#endif
cloud.cpp
#include"util.hpp"
#include"data.hpp"
#include"cloud.hpp"
#define BACKUP_FILE "./backup.dat"
#define BACKUP_DIR "./backup/"
int main()
{
cloud::Backup backup(BACKUP_DIR, BACKUP_FILE); \
backup.RunModule();
return 0;
}