云备份:自动将本地计算机上指定文件夹中需要备份的文件上传备份到服务器中。并且能够随时通过浏览器进行查看并且下载,其中下载过程支持断点续传功能,而服务器也会对上传文件进行热点管理,将非热点文件进行压缩存储,节省磁盘空间。
目标:们实现两端程序,其中包括部署在用户机的客户端程序,上传需要备份的文件,以及运行在服务器上的服务端程序,实现备份文件的存储和管理,两端合作实现总体的自动云备份功能。
服务端程序
负责功能
- 客户端上传的文件进行备份存储
- 能够对文件进行热点文件管理,对非热点文件进行压缩存储,节省磁盘空间
- 支持客户端浏览器查看访问文件列表。
- 支持客户端浏览器下载文件,并且下载支持断点续传。
模块划分
- 数据管理模块:负责服务器上备份文件的信息管理。
- 网络通信模块:搭建网络通信服务器,实现与客户端通信。
- 业务处理模块:针对客户端的各个请求进行对应业务处理并响应结果。
- 热点管理模块:负责文件的热点判断,以及非热点文件的压缩存储。
客户端程序
负责功能
- 能够自动检测客户机指定文件夹中的文件,并判断是否需要备份
- 将需要备份的文件逐个上传到服务器
模块划分
- 数据管理模块:负责客户端备份的文件信息管理,通过这些数据可以确定一个文件是否需要备份。
- 文件检测模块:遍历获取指定文件夹中所有文件路径名称。
- 网络通信模块:搭建网络通信客户端,实现将文件数据备份上传到服务器。
环境搭建
gcc7.3版本
sudo yum install centos-release-scl-rh centos-release-scl
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
source /opt/rh/devtoolset-7/enable
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
安装jsoncpp库
sudo yum install epel-release
sudo yum install jsoncpp-devel
ls /usr/include/jsoncpp/json/
下载bundle数据压缩库
sudo yum install git
git clone https://github.com/r-lyeh-archived/bundle.git
下载 httplib 库
git clone https://github.com/yhirose/cpp-httplib.git
json
json 是一种数据交换格式,采用完全独立于编程语言的文本格式来存储和表示数据。
举例:
char name = "jack";
int age = 18;
float score[3] = {88.5, 99, 58};
//json这种数据交换格式是将这多种数据对象组织成为一个字符串:
[
{
"name" : "jack",
"age" : 18,
"score" : [88.5, 99, 58]
},
{
"name" : "rose",
"age" : 20,
"score" : [88.5, 99, 58]
}
]
json 数据类型:对象,数组,字符串,数字
对象:使用花括号 {} 括起来的表示一个对象。
数组:使用中括号 [] 括起来的表示一个数组。
字符串:使用常规双引号 “” 括起来的表示一个字符串
数字:包括整形和浮点型,直接使用。
jsoncpp
jsoncpp 库用于实现 json 格式的序列化和反序列化,完成将多个数据对象组织成为 json 格式字符串,以及将 json格式字符串解析得到多个数据对象的功能。
这其中主要借助三个类以及其对应的少量成员函数完成:
//Json数据对象类
class Json::Value{
Value &operator=(const Value &other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过
Value& operator[](const std::string& key);//简单的方式完成 val["姓名"] = "小明";
Value& operator[](const char* key);
Value removeMember(const char* key);//移除元素
const Value& operator[](ArrayIndex index) const; //val["成绩"][0]
Value& append(const Value& value);//添加数组元素val["成绩"].append(88);
ArrayIndex size() const;//获取数组元素个数 val["成绩"].size();
std::string asString() const;//转string string name = val["name"].asString();
const char* asCString() const;//转char* char *name = val["name"].asCString();
Int asInt() const;//转int int age = val["age"].asInt();
float asFloat() const;//转float
bool asBool() const;//转 bool
};
//json序列化类,低版本用这个更简单
class JSON_API Writer {
virtual std::string write(const Value& root) = 0;
}
class JSON_API FastWriter : public Writer {
virtual std::string write(const Value& root);
}
class JSON_API StyledWriter : public Writer {
virtual std::string write(const Value& root);
}
//json序列化类,高版本推荐,如果用低版本的接口可能会有警告
class JSON_API StreamWriter {
virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory {
virtual StreamWriter* newStreamWriter() const;
}
//json反序列化类,低版本用起来更简单
class JSON_API Reader {
bool parse(const std::string& document, Value& root, bool collectComments = true);
}
//json反序列化类,高版本更推荐
class JSON_API CharReader {
virtual bool parse(char const* beginDoc, char const* endDoc,
Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory {
virtual CharReader* newCharReader() const;
}
jsoncpp 实现序列化
#include <iostream>
#include <sstream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
const char *name = "jack";
int age = 18;
float score[] = {70,80,90};
Json::Value root;
root["name"] =name;
root["age"] = age;
root["score"].append(score[0]);
root["score"].append(score[1]);
root["score"].append(score[2]);
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
sw->write(root,&ss);
std::cout << ss.str() << std::endl;
return 0;
}
g++ json_example.cpp -o json_example -ljsoncpp
jsoncpp 实现反序列化
#include <iostream>
#include <string>
#include <memory>
#include <jsoncpp/json/json.h>
int main()
{
std::string str = R"({"name":"lily","age":20,"score":[10,30,20]})";
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"<<std::endl;
return -1;
}
std::cout<<root["name"].asString()<<std::endl;
std::cout<<root["age"].asString()<<std::endl;
int sz = root["score"].size();
for(int i = 0;i < sz;i++){
std::cout<<root["score"][i]<<std::endl;
}
return 0;
}
bundle文件压缩库
BundleBundle 是一个嵌入式压缩库,支持23种压缩算法和2种存档格式。使用的时候只需要加入两个文件bundle.h 和 bundle.cpp 即可。
namespace bundle
{
// low level API (raw pointers)
bool is_packed( *ptr, len );
bool is_unpacked( *ptr, len );
unsigned type_of( *ptr, len );
size_t len( *ptr, len );
size_t zlen( *ptr, len );
const void *zptr( *ptr, len );
bool pack( unsigned Q, *in, len, *out, &zlen );
bool unpack( unsigned Q, *in, len, *out, &zlen );
// medium level API, templates (in-place)
bool is_packed( T );
bool is_unpacked( T );
unsigned type_of( T );
size_t len( T );
size_t zlen( T );
const void *zptr( T );
bool unpack( T &, T );
bool pack( unsigned Q, T &, T );
// high level API, templates (copy)
T pack( unsigned Q, T );
T unpack( T );
}
bundle库实现文件压缩
cp bundle/bundle.cpp bundle/bundle.h Linux/cloude/
把bundle.cpp,bundle.h拷贝到当前操作的文件夹中
#include <iostream>
#include <string>
#include <fstream>
#include "bundle.h"
int main(int argc,char* argv[])
{
std::cout<<"argv[1]是原始路径名称"<<std::endl;
std::cout<<"argv[2]是压缩包名称"<<std::endl;
if(argc < 3)
return -1;
std::string ifilename = argv[1];
std::string ofilename = argv[2];
std::ifstream ifs;
ifs.open(ifilename,std::ios::binary);//打开原始文件
ifs.seekg(0,std::ios::end);//跳转读写位置到末尾
size_t fsize = ifs.tellg();//获取末尾偏移量--文件长度
ifs.seekg(0,std::ios::beg);//跳转到文件起始
std::string body;
body.resize(fsize);//调整body大小为文件大小
ifs.read(&body[0],fsize);//读取文件所有数据到body
std::string packed = bundle::pack(bundle::LZIP,body);//以lzip格式压缩文件数据
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);//打开压缩包文件
ofs.write(&packed[0],packed.size());//将压缩后的数据写入压缩包文件
ifs.close();
ofs.close();
return 0;
}
g++ compress.cpp bundle.cpp -o compress -lpthread
bundle库实现文件解压缩
#include <iostream>
#include <fstream>
#include <string>
#include "bundle.h"
int main(int argc,char* argv[])
{
if(argc < 3){
std::cout<<"argv[1]是压缩åŒ
åç§°"<<std::endl;
std::cout<<"argv[2]是解压åŽçš„æ–‡ä»¶åç§°"<<std::endl;
return -1;
}
std::string ifilename = argv[1];//压缩åŒ
å
std::string ofilename = argv[2];//解压åŽçš„æ–‡ä»¶å
std::ifstream ifs;
ifs.open(ifilename,std::ios::binary);
ifs.seekg(0,std::ios::end);
size_t fsize = ifs.tellg();
ifs.seekg(0,std::ios::beg);
std::string body;
body.resize(fsize);
ifs.read(&body[0],fsize);
ifs.close();
std::string unpacked = bundle::unpack(body);
std::ofstream ofs;
ofs.open(ofilename,std::ios::binary);
ofs.write(&unpacked[0],unpacked.size());
ofs.close();
return 0;
}
g++ uncompress.cpp bundle.cpp -o uncompress -lpthread
httplib 库
httplib 库,一个 C++11 单文件头的跨平台 HTTP/HTTPS 库。安装起来非常容易。只需包含 httplib.h 在你的代码中即可。 httplib 库实际上是用于搭建一个简单的 http
服务器或者客户端的库,这种第三方网络库,可以让我们免去搭建服务器或客户端的时间,把更多的精力投入到具体的业务处理中,提高开发效率
namespace httplib{
struct MultipartFormData {
std::string name;
std::string content;
std::string filename;
std::string content_type;
};
using MultipartFormDataItems = std::vector<MultipartFormData>;
struct Request {
std::string method;
std::string path;
Headers headers;
std::string body;
// for server
std::string version;
Params params;
MultipartFormDataMap files;
Ranges ranges;
bool has_header(const char *key) const;
std::string get_header_value(const char *key, size_t id = 0) const;
void set_header(const char *key, const char *val);
bool has_file(const char *key) const;
MultipartFormData get_file_value(const char *key) const;
};
struct Response {
std::string version;
int status = -1;
std::string reason;
Headers headers;
std::string body;
std::string location; // Redirect location
void set_header(const char *key, const char *val);
void set_content(const std::string &s, const char *content_type);
};
class Server {
using Handler = std::function<void(const Request &, Response &)>;//函数指针类型
using Handlers = std::vector<std::pair<std::regex, Handler>>;//请求与处理函数映射表
std::function<TaskQueue *(void)> new_task_queue;//线程池-用于处理请求
Server &Get(const std::string &pattern, Handler handler);
Server &Post(const std::string &pattern, Handler handler);
Server &Put(const std::string &pattern, Handler handler);
Server &Patch(const std::string &pattern, Handler handler);
Server &Delete(const std::string &pattern, Handler handler);
Server &Options(const std::string &pattern, Handler handler);
bool listen(const char *host, int port, int socket_flags = 0);
};
class Client {
Client(const std::string &host, int port);
Result Get(const char *path, const Headers &headers);
Result Post(const char *path, const char *body, size_t content_length,
const char *content_type);
Result Post(const char *path, const MultipartFormDataItems &items);
}
}
- handler:函数指针类型,定义了一个http请求处理回调函数格式。httplib搭建的服务器收到请求后,进行解析,得到一个Request结构体其中包含了请求数据,根据请求数据就可以处理请求,这个处理函数定义的格式就是Handler格式
- Request参数:保存请求数据,让用户根据请求数据进行业务处理
- Response参数:需要用户在业务处理中,填充数据,最终要响应给客户端
- regex:正则表达式-用于匹配http请求资源
- httplib收到一个新建链接,则将新的客户端连接到线程池中
handlers可以理解为一张表,映射了一个客户端请求的资源路径和一个处理函数(用户自己定义的函数),当服务器收到请求解析得到Request就会根号资源路径以及请求方法到这张表中查看有没有对应的函数,如果有则调用这个函数处理请求,如果没有,则响应404
线程池中的线程工作:1、收到请求,解析请求,得到Request结构体也就是请求的数据2、在handlers映射表中根据请求信息查找处理函数
httplib 库搭建简单服务器
#include "httplib.h"
void hello(const httplib::Request &req,httplib::Response &rsp)
{
rsp.set_content("hello world!","text/plain");
rsp.status = 200;
}
void Numbers(const httplib::Request &req,httplib::Response &rsp)
{
auto num = req.matches[1];//0里保存的是整体path,1中以后下标保存的是捕捉的数据
rsp.set_content(num,"text/plain");
rsp.status = 200;
}
void Mutipart(const httplib::Request &req,httplib::Response &rsp)
{
auto ret = req.has_file("file");
if(ret == false)
{
std::cout<<"not file upload"<<std::endl;
rsp.status = 404;
return ;
}
const auto& file = req.get_file_value("file");
rsp.body.clear();
rsp.body = file.filename;//文件名称
rsp.body += "\n";
rsp.body += file.content;//文件内容
rsp.set_header("Content-Type","text/plain");
rsp.status = 200;
}
int main()
{
httplib::Server server;//实例化一个server对象用于搭建服务器
server.Get("/hi",hello);//注册一个针对 /hi 的Get请求的处理函数的映射关系
//server.Get("/numbers/(\\d+)")
server.Get(R"(/numbers/(\d+))",Numbers);
server.Post("/mutipart",Mutipart);
server.listen("0.0.0.0",8081);//匹配服务器上的所有地址
return 0;
}
httplib库搭建简单客户端
#include "httplib.h"
#define SERVER_IP "152.136.155.244"
#define SERVER_PORT 8081
int main()
{
httplib::Client client(SERVER_IP,SERVER_PORT);
httplib::MultipartFormData item;
item.name = "file";
item.filename = "hello.txt";
item.content = "Hello World";
item.content_type = "text/plain";
httplib::MultipartFormDataItems items;
items.push_back(item);
auto res = client.Post("/mutipart",items);
std::cout<<res->status<<std::endl;
std::cout<<res->body<<std::endl;
return 0;
}
服务端
服务端工具类实现
文件实用工具类设计
不管是客户端还是服务端,文件的传输备份都涉及到文件的读写,包括数据管理信息的持久化也是如此,因此首先设计封装文件操作类,这个类封装完毕之后,则在任意模块中对文件进行操作时都将变的简单化。
class FileUtil{
private:
std::string _name;
public:
FileUtil(const std::string &name);
bool Remove();//删除
size_t FileSize();//文件大小
time_t LastATime();//文件最后一次访问时间(热点管理)
time_t LastMTime();//文件最后一次修改时间
std::string FileName();//获取文件路径名中的文件名称
bool GetPosLen(std::string *content, size_t pos, size_t len);//获取文件指定位置指定长度的数据
bool GetContent(std::string *content);//获取文件数据
bool SetContent(std::strint *content);//向文件写入
bool Compress(const std::string &packname);
bool UnCompress(const std::string &filename);
bool Exists();//判断文件是否存在
bool CreateDirectory();
bool ScanDirectory(std::vector<std::string> *arry);
};
具体实现
util.hpp
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <experimental/filesystem>
#include <jsoncpp/json/json.h>
#include "bundle.h"
namespace cloud
{
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename;
public:
FileUtil(const std::string &filename):_filename(filename){}
bool Remove()
{
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 false"<<std::endl;
return -1;
}
return st.st_size;
}
time_t LastMTime()
{
struct stat st;
if(stat(_filename.c_str(),&st) < 0)
{
std::cout<<"get file size false"<<std::endl;
return -1;
}
return st.st_mtime;
}
time_t LastATime()
{
struct stat st;
if(stat(_filename.c_str(),&st) < 0)
{
std::cout<<"get file size false"<<std::endl;
return -1;
}
return st.st_atime;
}
std::string FileName()
{
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)
{
//获取文件指定位置,指定长度的数据
size_t fsize = this->FileSize();
if(pos + len > fsize)
{
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 open file false"<<std::endl;
return false;
}
ifs.seekg(pos,std::ios::beg);
body->resize(len);
ifs.read(&(*body)[0],len);
if(ifs.good() == false)
{
std::cout<<"get file content fail"<<std::endl;
ifs.close();
}
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 file content fail"<<std::endl;
return false;
}
ofs.write(&body[0],body.size());
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)
{
//1、获取源文件数据
std::string body;
if(this->GetContent(&body) == false)
{
std::cout<<"compress get file fail"<<std::endl;
return false;
}
//2、对数据进行压缩
std::string packed = bundle::pack(bundle::LZIP,body);
//将压缩的数据存储到压缩包文件
FileUtil fu(packname);
if(fu.SetContent(packed) == false)
{
std::cout<<"compress write packed data fail"<<std::endl;
return false;
}
return true;
}
bool UnCompress(const std::string &filename)
{
//将当前压缩包数据读取
std::string body;
if(this->GetContent(&body))
{
std::cout<<"uncompress get file fail"<<std::endl;
return false;
}
//对压缩的数据解压缩
std::string unpacked = bundle::unpack(body);
FileUtil fu(filename);
if(fu.SetContent(unpacked) == false)
{
std::cout<<"uncompress write packed data fail"<<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> *array)
{
for(auto& p: fs::directory_iterator(_filename))
{
if(fs::is_directory(p) == true)
continue;
//relativa_patj 带有路径的文件名
array->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
}
cloud.cpp
#include "util.hpp"
void FileUtilTest(const std::string &filename)
{
cloud::FileUtil fu(filename);
std::cout<<fu.FileSize()<<std::endl;
std::cout<<fu.LastMTime()<<std::endl;
std::cout<<fu.LastATime()<<std::endl;
std::cout<<fu.FileName()<<std::endl;
}
void test2(const std::string &filename)
{
cloud::FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
cloud::FileUtil nuf("./hello.txt");
nuf.SetContent(body);
}
void test3(const std::string &filename)
{
std::string packname = filename + ".lz";
cloud::FileUtil fu(filename);
fu.Compress(packname);
cloud::FileUtil ufu(packname);
ufu.UnCompress("./hello.txt");
}
void test4(const std::string &filename)
{
cloud::FileUtil fu(filename);
fu.CreateDirectory();
std::vector<std::string> array;
fu.ScanDirectory(&array);
for(auto& a: array)
{
std::cout<<a <<std::endl;
}
}
int main(int argc,char* argv[])
{
FileUtilTest(argv[1]);
test2(argv[1]);
test3(argv[1]);
test4(argv[1]);
return 0;
}
Json 实用工具类设计
class JsonUtil{
public:
static bool Serialize(const Json::Value &root, std::string *str);
static bool UnSerialize(const std::string &str, Json::Value *root);
};
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;
if(sw->write(root,&ss) != 0)
{
std::cout<<"json write fail"<<std::endl;
return false;
}
*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;
}
};
void jsontest()
{
const char *name = "jack";
int age = 18;
float score[] = {70,80,90};
Json::Value root;
root["name"] =name;
root["age"] = age;
root["score"].append(score[0]);
root["score"].append(score[1]);
root["score"].append(score[2]);
std::string json_str;
cloud::JsonUtil::Serialize(root,&json_str);
std::cout<<json_str<<std::endl;
Json::Value val;
cloud::JsonUtil::UnSerialize(json_str,&val);
std::cout<< val["name"].asString()<<std::endl;
std::cout<<val["age"].asInt()<<std::endl;
for(int i = 0;i < val["score"].size();i++)
{
std::cout<<val["score"][i].asFloat() <<std::endl;
}
}
int main(int argc,char* argv[])
{
jsontest();
return 0;
}
服务端配置信息模块
系统配置信息
配置信息:
- 热点判断时间
- 文件下载URL前缀路径
- 压缩包后缀名称 :订立的压缩包命名规则,就是在文件原名称之后加上后缀
- 上传文件存放路径 :决定了文件上传之后实际存储在服务器的位置
- 压缩文件存放路径 :决定非热点文件压缩后存放的路径
- 服务端备份信息存放文件 :服务器端记录的备份文件信息的持久化存储
- 服务器访问 IP 地址:当程序要运行在其他主机上,则不需要修改程序
- 服务器访问端口
单例文件配置类设计
#define CONFIG_FILE "./cloud.conf"
class Config{
private:
time_t _hot_time;//热点判断时间
int _server_port;//服务器监听端口
std::string _server_ip;
std::string _download_prefix;//下载的url前缀路径
std::string packfile_suffix;//压缩包后缀名称
std::string _pack_dir;//压缩包存放目录
std::string _back_dir;//备份文件存放目录
std::string _backup_file;//备份信息的配置管理
private:
static std::mutex _mutex;
static Config *_instance;
Config();
};
cloud.conf
注:配置信息里的ip是内网ip
查询内网IP :ifconfig
{
{
"hot_time" : 30,
"server_port" : 8081,
"server_ip" : "10.0.8.17",
"download_prefix" : "/download/",
"packfile_suffix" : ".lz",
"pack_dir" : "./packdir/",
"back_dir" : "./backdir/",
"backup_file" : "./back.dat"
}
}
config.hpp
#pragma once
#include <mutex>
#include "util.hpp"
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;
std::string _download_prefix;
std::string _packfile_suffix;
std::string _pack_dir;
std::string _back_dir;
std::string _backup_file;
bool ReadConfigFile()
{
FileUtil fu(CONFIG_FILE);
std::string body;
if(fu.GetContent(&body) == false)
{
std::cout << "load config file fail"<<std::endl;
return false;
}
Json::Value root;
if(JsonUtil::UnSerialize(body,&root) == false)
{
std::cout<<"parse config file fail"<<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 == nullptr)
{
_mutex.lock();
if(_instance == nullptr)
{
_instance = new Config();
}
_mutex.unlock();
}
return _instance;
}
int GetHotTime()
{
return _hot_time;
}
int GetServerPort()
{
return _server_port;
}
std::string GetServerIp()
{
return _server_ip;
}
std::string GetDownLoadPrefix()
{
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 = nullptr;
std::mutex Config::_mutex;
}
cloud.cpp
void configtest()
{
cloud::Config *config = cloud::Config::GetInstance();
std::cout<<config->GetHotTime()<<std::endl;
std::cout<<config->GetServerPort()<<std::endl;
std::cout << config->GetServerIp()<<std::endl;
std::cout<<config->GetDownLoadPrefix()<<std::endl;
std::cout<<config->GetPackFileSuffix()<<std::endl;
std::cout<<config->GetPackDir()<<std::endl;
std::cout<<config->GetBackDir()<<std::endl;
std::cout<<config->GetBackUpFile()<<std::endl;
}
int main(int argc,char* argv[])
{
configtest();
return 0;
}
管理数据
内存中以文件访问URL为key,数据信息结构为val,使用哈希表进行管理,查询速度快。使用url作为key是因为往后客户端浏览器下载文件的时候总是以 url 作为请求。 采用文件形式对数据进行持久化存储(序列化方式采用 json 格式或者自定义方式)
数据信息
- 文件实际存储路径:当客户端下载文件时,从此文件中读取数据进行响应
- 文件是否压缩标志:判断文件是否已经被压缩
- 压缩包存储路径:非热点文件会被压缩,如果客户端需要下载文件,则需先解压然后读取
- 文件访问URL
- 文件最后一次修改时间
- 文件最后一次访问时间
- 文件大小
数据管理类
typedef struct BackupInfo{
int pack_flag;//是否压缩标志
time_t mtime;//最后一次修改时间
time_t atime;//最后一次访问时间
size_t fsize;//文件大小
std::string realpath;//实际存储路径
std::string url_path;//
std::string packpath;//压缩包存储路径
bool NewBackupInfo(const std::string &realpath);
}BackupInfo;
class DataManager{
private:
FileUtil _backup_file;//持久化存储文件
pthread_rwlock_t _rwlock;//读写锁-读共享,写互斥
std::unordered_map<std::string, BackupInfo> _table;//内存以hash表存储
public:
DataManager();
bool InitLoad();//初始化程序,每次重启都要加载以前的信息
bool Storage(); //每次有信息改变则需要持久化存储一次,避免数据丢失
bool Insert(const std::string &key, const BackupInfo &val);
bool Update(const std::string &key, const BackupInfo &val);
bool GetOneByURL(const std::string &key, BackupInfo *info);
bool GetOneByRealPath(const std::string &key, BackupInfo *info);
bool GetAll(std::vector<BackupInfo> *arry);
};
data.hpp
#pragma once
#include "util.hpp"
#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 back faile"<<std::endl;
return false;
}
Config *config = Config::GetInstance();
std::string packdir = config->GetPackDir();
std::string packsuffix = config->GetPackFileSuffix();
std::string download = config->GetDownLoadPrefix();
this->pack_flag = false;
this->fsize = fu.FileSize();
this->atime = fu.LastATime();
this->mtime = fu.LastMTime();
this->real_path = realpath;
this->pack_path = packdir + fu.FileName() + packsuffix;
this->url = download + fu.FileName();
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,nullptr);//初始化读写锁
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)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.find(url);
if(it == _table.end())
{
pthread_rwlock_unlock(&_rwlock);
return false;
}
*info = it->second;
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool GetOneByRealPath(const std::string &realpath,BackupInfo *info)
{
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> *array)
{
pthread_rwlock_wrlock(&_rwlock);
auto it = _table.begin();
for(;it != _table.end();++it)
{
array->push_back(it->second);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool Storage()
{
//1、获取所有数据
std::vector<BackupInfo> arry;
this->GetAll(&arry);
//2、添加到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);
}
//3、对Json::Value序列化
std::string body;
JsonUtil::Serialize(root,&body);
//4、写文件
FileUtil fu(_backup_file);
fu.SetContent(body);
return true;
}
bool InitLoad()
{
//1、将文件中的数据读取
FileUtil fu(_backup_file);
if(fu.Exists() == false)
{
return false;
}
std::string body;
fu.GetContent(&body);
//2、反序列化
Json::Value root;
JsonUtil::UnSerialize(body,&root);
//3、将反序得到的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"].asInt();
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;
}
};
}
cloud.cpp
void test5(const std::string &filename)
{
cloud::BackupInfo info;
info.NewBackupInfo(filename);
cloud::DataManager data;
data.Insert(info);
cloud::BackupInfo tmp;
data.GetOneByURL("/download/bundle.h",&tmp);
std::cout<<tmp.pack_flag<<std::endl;
std::cout<<tmp.fsize<<std::endl;
std::cout<<tmp.atime<<std::endl;
std::cout<<tmp.mtime<<std::endl;
std::cout<<tmp.real_path<<std::endl;
std::cout<<tmp.pack_path<<std::endl;
std::cout<<tmp.url<<std::endl;
std::cout<<"-----------------------------------------"<<std::endl;
info.pack_flag = true;
data.Update(info);
std::vector<cloud::BackupInfo> arry;
data.GetAll(&arry);
for(auto &a : arry)
{
std::cout<<a.pack_flag<<std::endl;
std::cout<<a.fsize<<std::endl;
std::cout<<a.atime<<std::endl;
std::cout<<a.mtime<<std::endl;
std::cout<<a.real_path<<std::endl;
std::cout<<a.pack_path<<std::endl;
std::cout<<a.url<<std::endl;
}
std::cout<<"-----------------------------"<<std::endl;
data.GetOneByRealPath(filename,&tmp);
std::cout<<tmp.pack_flag<<std::endl;
std::cout<<tmp.fsize<<std::endl;
std::cout<<tmp.atime<<std::endl;
std::cout<<tmp.mtime<<std::endl;
std::cout<<tmp.real_path<<std::endl;
std::cout<<tmp.pack_path<<std::endl;
std::cout<<tmp.url<<std::endl;
}
int main(int argc,char* argv[])
{
test5(argv[1]);
return 0;
}
热点管理
服务器端的热点文件管理是对上传的非热点文件进行压缩存储,节省磁盘空间。
而热点文件的判断在于上传的文件的最后一次访问时间是否在热点判断时间之内,如果一个文件一天都没有被访问过,就认为这是一个非热点文件,其实就是当前系统时间,与文件最后一次访问时间之间的时间差是否在一天之内的判断。
而我们需要对上传的文件每隔一段时间进行热点检测,相当于遍历上传文件的存储文件夹,找出所有的文件,然后通过对逐个文件进行时间差的判断,来逐个进行热点处理。
基于这个思想,我们需要将上传的文件存储位置与压缩后压缩文件的存储位置分开。这样在遍历上传文件夹的时候不至于将压缩过的文件又进行非热点处理了。
遍历所有的文件:
- 从数据管理模块中遍历所有的备份文件信息
- 遍历备份文件,获取所有的文件进行属性获取,最终判断
遍历备份文件夹:
- 遍历文件夹,每次获取文件的最新数据进行判断,并且还可以解决信息缺漏的问题
1、遍历备份目录,获取所有文件路径名称
2、逐个文件获取最后一次访问时间与当前系统时间进行比较判断
3、对非热点文件进行压缩处理,删除源文件
4\修改数据管理模块对应的文件信息(压缩标志->true)
热点管理类的设计
//因为数据管理是要在多个模块中访问的,因此将其作为全局数据定义,在此处声明使用即可
extern DataManager *_data;
class HotManager{
private:
std::string _back_dir;//备份文件路径
std::string _pack_dir;//压缩文件路径
std::string _pack_suffix;//压缩包后缀名
time_t _hot_time;//热点判断时间
public:
HotManager();
bool HotJudge(const std::string &file);
bool RunModule();
};
hot.hpp
#pragma once
#include "data.hpp"
#include <unistd.h>
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(nullptr);//当前时间
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 tmp_back(_back_dir);//如果没有目录就创建
FileUtil tmp_pack(_pack_dir);
tmp_back.CreateDirectory();
tmp_pack.CreateDirectory();
}
bool RunMoudle()
{
while(1)
{
//1、遍历备份目录,获取所有文件名
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
//2、遍历判断文件是否是非热点文件
for(auto &a :arry)
{
if(HotJudge(a) == false)
{
//热点文件
continue;
}
//3、获取文件的备份信息
BackupInfo bi;
if(_data->GetOneByRealPath(a,&bi) == false)
{
//有文件存在,但是没有备份信息
bi.NewBackupInfo(a);//设置一个新的备份信息
}
//4、非热点文件压缩处理
FileUtil tmp(a);
tmp.Compress(bi.pack_path);
//4、删除源文件,修改备份信息
tmp.Remove();
bi.pack_flag = true;
_data->Update(bi);
}
usleep(1000);//避免空目录循环遍历消耗cpu过高
}
return true;
}
};
}
cloud.cpp
cloud::DataManager *_data;
void test7()
{
_data = new cloud::DataManager();
cloud::HotManager hot;
hot.RunMoudle();
}
int main(int argc,char* argv[])
{
test7();
return 0;
}
运行 ./cloud 多了backdir packdir文件夹
设置热点时间是30秒,给backdir 添加一个文件,修改cloud.conf ,30秒后删除非热点文件,并压缩文件到packdir
业务处理
云备份项目中 ,业务处理模块是针对客户端的业务请求进行处理,并最终给与响应。将网络通信和业务处理进行合并
实现的功能:
- 借助网络通信模块httplib库搭建http服务器与客户端进行网络通信
- 针对收到的请求进行对应的业务处理并进行响应(文件上传,列表查看,文件下载(包含断点续传))
网络通信
约定好客户端发送什么样的请求,服务端给与什么样的响应
请求:文件上传,展示页面,文件下载
HTTP文件上传
POST /upload HTTP/1.1
Content-Length:11
Content-Type:multipart/form-data;boundary= ----WebKitFormBoundary+16字节随机字符
------WebKitFormBoundary
Content-Disposition:form-data;filename="a.txt";
hello world
------WebKitFormBoundary--
当服务器收到了一个POST方法的/upload请求,则认为是一个文件上传请求,解析请求,得到文件数据,将数据写入到文件中
HTTP/1.1 200 OK
Content-Length: 0
HTTP文件列表获取
//展示页面
GET /list HTTP/1.1
Content-Length: 0
HTTP/1.1 200 OK
Content-Length:
Content-Type: text/html
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Page of Download</title>
</head>
<body>
<h1>Download</h1>
<table>
<tr>
<td><a href="/download/a.txt"> a.txt </a></td>
<td align="right"> 10:20:44 2022 </td>
<td align="right"> 27K </td>
</tr>
</table>
</body>
</html>
HTTP文件下载:
GET /download/a.txt http/1.1
Content-Length: 0
HTTP/1.1 200 OK
Content-Length: 100000
ETags: "filename-size-mtime一个能够唯一标识文件的数据"
Accept-Ranges: bytes
ETag:存储了一个资源的唯一标识,客户端第一次下载文件的时候,会收到这个响应信息,第二次下载就会将信息发给服务器,让服务器根据唯一标识判断该资源有无修改,如果没有,直接使用原理缓存的数据,不用重新下载
ETag : 文件名-文件大小-最后一次修改时间
ETag字段不仅缓存时会用到,断电传续也会用到,因为断电传续要保证文件没有被修改过
HTTP断点续传
功能:文件下载过程中,因为某种异常而中断,如果再次从头下载,效率极低,
因此断电传续就是从上次下载断开的位置,重新下载即可
GET /download/a.txt http/1.1
Content-Length: 0
If-Range: "文件唯一标识"
Range: bytes=89-999
HTTP/1.1 206 Partial Content
Content-Length:
Content-Range: bytes 89-999/100000
Content-Type: application/octet-stream
ETag: "inode-size-mtime一个能够唯一标识文件的数据"
Accept-Ranges: bytes
对应文件从89到999字节的数据。
在http协议中断点续传的实现
- 告诉服务器下载区间范围服务器上能够检测上一次下载之后的文件是否被修改过
业务处理类设计
//因为业务处理的回调函数没有传入参数的地方,因此无法直接访问外部的数据管理模块数据
//可以使用lamda表达式解决,但是所有的业务功能都要在一个函数内实现,于功能划分上模块不够清晰
//因此将数据管理模块的对象定义为全局数据,在这里声明一下,就可以在任意位置访问了
extern cloud::DataManager *_data;
class Service{
private:
int _server_port;
std::string _server_ip;
std::string _download_prefix;
httplib::Server _srv;
public:
static void Upload(const httplib::Request &req, httplib::Response &rsp);//上传
static void ListShow(const httplib::Request &req, httplib::Response &rsp);//展示页面
static void Download(const httplib::Request &req,httplib::Response &rsp);//文件下载
public:
Service();
bool RunModule();
}
server.hpp
#pragma once
#include "data.hpp"
#include "httplib.h"
#include "config.hpp"
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");
std::string back_dir = Config::GetInstance()->GetBackDir();
std::string realpath = back_dir + FileUtil(file.filename).FileName();
FileUtil fu(realpath);
fu.SetContent(file.content);//将数据写入文件中
//file.filename;//文件名称
//file.content;//文件数据
BackupInfo info;
info.NewBackupInfo(realpath);//组织备份文件的信息
_data->Insert(info);//向数据管理模块添加文件的备份信息
return ;
}
static std::string TimeStr(time_t t)
{
std::string tmp = std::ctime(&t);
return tmp;
}
static void ListShow(const httplib::Request &req,httplib::Response &rsp)
{
//1、获取所有的文件备份信息
std::vector<BackupInfo> arry;
_data->GetAll(&arry);
//2、根据所有备份信息组织html文件数据
std::stringstream ss;
ss << "<html><head><title><Download></title></head>";
ss << "<body><h1>Download</h1><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'> " << TimeStr(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;
return ;
}
static std::string GetETag(const BackupInfo &info)
{
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)
{
//1、获取客户端请求的资源路径path req.path
//2、根据资源路径,获取文件备份信息
BackupInfo info ;
_data->GetOneByURL(req.path,&info);
//3、判断文件是否被压缩,如果被压缩,解压
if(info.pack_flag == true)
{
//4、删除压缩包,修改备份信息
FileUtil fu(info.pack_path);
fu.UnCompress(info.real_path);//将文件解压到备份目录下
fu.Remove();
info.pack_flag = false;
_data->Update(info);
}
//5、读取文件数据,放入rsp.body中
FileUtil fu(info.real_path);
bool retrans = false;
std::string old_etag;
if(req.has_header("If-Range"))
{
old_etag = req.get_header_value("If-Range");
//有If-Range字段,且值与请求文件的最新etag一致,符合断点续传
if(old_etag == GetETag(info))
{
retrans = true;
}
}
//如果没有If-Range字段则是正常下载,如果有而且值与当前文件的etag不一致,也比学重新返回全部数据
if(retrans == false)
{
//6、设置响应头部字段: ETag Accept-Ranges: bytes;
fu.GetContent(&rsp.body);
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中取出指定区间数据进行响应
//std::string range = req.get_header_val("Range"); bytes = start-end
fu.GetContent(&rsp.body);
rsp.set_header("Accept-Ranges","bytes");
rsp.set_header("ETag",GetETag(info));
rsp.status = 206;//区间请求响应的是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);
std::cout<<_server_ip.c_str()<<std::endl;
std::cout<<_server_port<<std::endl;
return true;
}
};
}
cloud.cpp
cloud::DataManager *_data;
void test7()
{
cloud::HotManager hot;
hot.RunMoudle();
}
void test8()
{
cloud::Service srv;
srv.RunModule();
}
int main(int argc,char* argv[])
{
_data = new cloud::DataManager();
std::thread thread_hot_manager(test7);
std::thread thread_service(test8);
thread_hot_manager.join();
thread_service.join();
return 0;
}
网页
网页中的网址是外网地址
<html>
<head>
<title>Page of Download</title>
</head>
<body>
<form action = "http://152.136.155.244:8081/upload" method="post" enctype="multipart/form-data">
<div> <input type="file" name="file"> </div>
<div> <input type="submit" name="上传"> </div>
</form>
</body>
</html>
Upload
ListShow
Download
back.dat
[
{
"atime" : 1653533820,
"fsize" : 61539448,
"mtime" : 1653533820,
"pack_flag" : true,
"pack_path" : "./packdir/ToDesk_Setup.exe.lz",
"real_path" : "./backdir/ToDesk_Setup.exe",
"url" : "/download/ToDesk_Setup.exe"
},
{
"atime" : 1653531586,
"fsize" : 0,
"mtime" : 1653532484,
"pack_flag" : true,
"pack_path" : "./packdir/a.txt.lz",
"real_path" : "./backdir/a.txt",
"url" : "/download/a.txt"
},
{
"atime" : 1653532512,
"fsize" : 292,
"mtime" : 1653532524,
"pack_flag" : true,
"pack_path" : "./packdir/b.html.lz",
"real_path" : "./backdir/b.html",
"url" : "/download/b.html"
},
{
"atime" : 1653532542,
"fsize" : 385,
"mtime" : 1653532542,
"pack_flag" : true,
"pack_path" : "./packdir/test2.py.lz",
"real_path" : "./backdir/test2.py",
"url" : "/download/test2.py"
}
客户端
用vs2019编译,需要在linux下把httplib添加到vs的项目中
sz httplib.h
头文件单击右键-》添加-》现有项 添加httplib.h
数据管理模块
管理备份的文件信息
数据信息设计
客户端要实现的功能是对指定文件夹中的文件自动进行备份上传。但是并不是所有的文件每次都需要上传,我们需要能够判断,哪些文件需要上传,哪些不需要,因此需要将备份的文件信息给管理起来,作为下一次文件是否需要备份
的判断。
理想实现:
- 内存存储:高访问效率–hash表
- 持久化存储:文件存储
因此需要被管理的信息:
- 文件路径名称
- 文件唯一标识:由文件名,最后一次修改时间,文件大小组成的一串信息
文件操作实用类
namespace fs = std::experimental::filesystem;
class FileUtil {
private:
std::string _name;
public:
FileUtil(const std::string &name) :_name(name) {}
size_t FileSize();
time_t LastATime();
time_t LastMTime();
std::string FileName();
bool GetPosLen(std::string *content, size_t pos, size_t len);
bool GetContent(std::string *content);
bool SetContent(const std::string &content);
bool Exists();
bool CreateDirectory();
bool ScanDirectory(std::vector<std::string> *arry);
};
util.hpp
注:experimental/filesystem在vs2017以上版本,C++17才能运行成功
如果出现一下问题:
需要在项目单机右键-》属性-》C/C+±》预处理器-》预处理器定义添加:
_SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <sys/stat.h>
#include <experimental/filesystem>
namespace cloud
{
namespace fs = std::experimental::filesystem;
class FileUtil
{
private:
std::string _filename;
public:
FileUtil(const std::string& filename) :_filename(filename) {}
bool Remove()
{
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 false" << std::endl;
return 0;
}
return st.st_size;
}
time_t LastMTime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size false" << std::endl;
return -1;
}
return st.st_mtime;
}
time_t LastATime()
{
struct stat st;
if (stat(_filename.c_str(), &st) < 0)
{
std::cout << "get file size false" << std::endl;
return -1;
}
//return fs::path(_filename).filename().string();
return st.st_atime;
}
std::string FileName()
{
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)
{
//获取文件指定位置,指定长度的数据
size_t fsize = this->FileSize();
if (pos + len > fsize)
{
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 open file false" << std::endl;
return false;
}
ifs.seekg(pos, std::ios::beg);
body->resize(len);
ifs.read(&(*body)[0], len);
if (ifs.good() == false)
{
std::cout << "get file content fail" << std::endl;
ifs.close();
}
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 file content fail" << std::endl;
return false;
}
ofs.write(&body[0], body.size());
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>* array)
{
this->CreateDirectory();
for (auto& p : fs::directory_iterator(_filename))
{
if (fs::is_directory(p) == true)
continue;
//relativa_patj 带有路径的文件名
array->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
}
#endif // !__MY_UTIL
数据管理类
class DataManager{
private:
std::unordered_map<std::string, std::string> _table;
std::string _back_file;
public:
DataManager(const std::string back_file);
bool InitLoad();//程序运行时加载以前的数据
bool Storage();//持久化存储
bool Insert(const std::string &key, const std::string &val);
bool Update(const std::string &key, const std::string &val);
bool GetOneByKey(const std::string &key, std::string *val);
};
data.hpp
#ifndef _MY_DATA__
#define _MY_DATA__
#include "util.hpp"
#include <unordered_map>
#include <sstream>
namespace cloud
{
class DadaManager
{
private:
std::string _backup_file;//备份信息的持久化存储文件
std::unordered_map<std::string, std::string> _table;
public:
DadaManager(const std::string &backup_file) :_backup_file(backup_file)
{
InitLoad();
}
bool Storage()
{
//1、获取所有的备份信息
auto it = _table.begin();
std::stringstream ss;
for (; it != _table.end(); it++)
{
//2、将所有信息进行特定持久化格式的组织
ss << it->first << " " << it->second << "\n";
}
//3、持久化存储
FileUtil fu(_backup_file);
fu.SetContent(ss.str());
return true;
}
int Split(const std::string &str, const std::string &sep, std::vector<std::string> *arry)
{
int count = 0;
size_t pos = 0, idx = 0;
while (1)
{
//find(要查找的分隔符,从那开始找的偏移量)
pos = str.find(sep, idx);
if (pos == std::string::npos)
{
break;
}
if (pos == idx)
{
idx = pos + sep.size();
continue;
}
//substr(截取起始位置,长度)
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;
Split(body,"\n", &arry);
for (auto &a : arry)
{
//file.txt file.txt-3458-9876
std::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 // !_MY_DATA__
文件备份模块
将需要备份的文件上传备份到服务器
1、遍历指定文件夹,获取文件信息
2、逐一判断文件是否需要备份
3、需要备份的文件进行上传
#define SERVER_IP "152.136.155.244"//虚拟机/服务器的地址
#define SERVER_PORT 8081
class BackUp {
private:
DataManager *_data;
std::string _back_dir;//要监控的文件夹
std::string _back_file;
public:
BackUp(const std::string &back_dir, const std::string &back_file)
: _back_dir(back_dir)
, _back_file(back_file){}
std::string GetFileIdentifier(const std::string &filename);//计算获取文件的唯一标识
bool Upload(const std::string &filename);
bool IsCanBeUpload(const std::string &filename);
bool RunModule();
};
cloud.hpp
#pragma once
#include "data.hpp"
#include "util.hpp"
#include "httplib.h"
#include <windows.h>
namespace cloud
{
#define SERVER_IP "152.136.155.244"
#define SERVER_PORT 8081
class BackUp
{
private:
std::string _back_dir;
DadaManager *_data;
public:
BackUp(const std::string &back_dir, const std::string &back_file) :
_back_dir(back_dir)
{
_data = new DadaManager(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)
{
//1、获取文件数据
FileUtil fu(filename);
std::string body;
fu.GetContent(&body);
//2、搭建http客户端上传文件数据
httplib::Client client(SERVER_IP, 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);
if (!res || res->status != 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);
if (time(nullptr) - fu.LastMTime() < 3)
{
//3s之内修改,认为文件还在修改中
return false;
}
std::cout << filename << "need backup" << std::endl;
return true;
}
bool Runmodule()
{
while (1)
{//1、遍历获取指定文件夹中的所有文件
FileUtil fu(_back_dir);
std::vector<std::string> arry;
fu.ScanDirectory(&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" << std::endl;
}
}
Sleep(1);
std::cout << "------------------------loop end-----------------------------"<< std::endl;
}
}
};
}
cloud.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include "util.hpp"
//#include "data.hpp"
#include "cloud.hpp"
#define BACKUP_FILE "./backup_dat"
#define BACKUP_DIR "./backup/"
int main()
{
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include "util.hpp"
//#include "data.hpp"
#include "cloud.hpp"
#define BACKUP_FILE "./backup_dat"
#define BACKUP_DIR "./backup/"
int main()
{
/*
cloud::FileUtil fu("./");
std::vector<std::string> arry;
fu.ScanDirectory(&arry);
cloud::DadaManager data(BACKUP_FILE);
for (auto &a : arry)
{
data.Insert(a,"slkjendhh");
std::cout << a << std::endl;
}
*/
/*
cloud::DadaManager data(BACKUP_FILE);
std::string str;
data.GetOneByKey(".\\cloud.cpp",&str);
std::cout << str << std::endl;
*/
cloud::BackUp backup(BACKUP_DIR, BACKUP_FILE);
backup.Runmodule();
return 0;
}
back.dat
在backup文件夹里拷贝文件
总结
项目功能:搭建云备份服务器与客户端,客户端程序运行在客户机上自动将指定目录下的文件备份到服务器,并且能够支持浏览器查看与下载,其中下载支持断点续传功能,并且服务器端对备份的文件进行热点管理,将长时间无访问的文件进行压缩存储。
服务器:
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 热点管理模块:对备份的文件进行热点管理,将长时间无访问文件进行压缩存储,节省磁盘空间
- 业务处理模块:搭建 http 服务器与客户端进行通信处理客户端的上传,下载,查看请求,并支持断点续传
客户端:
- 数据管理模块:内存中使用hash表存储提高访问效率,持久化使用文件存储管理备份数据
- 文件检索模块:基于 c++17 文件系统库,遍历获取指定文件夹下所有文件。
- 文件备份模块:搭建 http 客户端上传备份文件。