项目简介
功能
- 搭建云备份服务器与客户端,客户端针对主机上指定目录下的文件自动备份到服务器,服务端会对上传的文件进行热点文件判断,对于非热点文件进行压缩,节约磁盘空间,并且服务端支持通过浏览器进行备份文件的查看、下载,并且下载功能支持断点续传;
概要设计
客户端
- 编程环境:Windows 下的客户端程序;
- 功能需求:监控目录,自动对指定目录下需要备份的文件进行上传备份;
- 目录监控:使用 C++17filesystem 遍历目录,判断文件是否需要备份,判断方式、条件如下;
- 获取历史备份信息,获取当前监控目录下文件信息(名称、时间、大小);
- 判断是否需要备份:当前文件信息与同名历史文件信息不符则需要备份,文件信息未在历史信息中出现需要备份;
- 网络通信:遍历需要上传备份的文件,获取其文件名称,使用 httplib 库中提供的接口进行文件数据上传;
- 数据管理:从第一步中获取到需要备份的文件,将其信息保存到下来(代码内:map,代码外:文件),保存格式为【文件名称 : 创建时间 + 文件大小】的键值对,被保存的文件也相当于历史备份信息,可作为用来判断文件是否需要备份的比对;
服务端
- 编程环境:Linux 服务器;
- 功能需求:对客户端上传的文件进行保存,并对文件进行检查,如果是非热点文件,那么就进行压缩存储,删除源文件,支持用户浏览器对备份文件的访问与下载;
- 网络通信:使用 httplib 库中提供的接口进行接收客户端上传的文件数据,并以文件形式保存;
- 浏览下载:用户通过浏览器可以查看当前已备份文件,可以对备份文件进行下载,并且下载还支持断点续传;
- 文件压缩:备份文件在一定时间内没有被访问过,那么该文件就是非热点文件,对其进行压缩存储,判断方法就是用当前时间减去文件最后一次访问时间;
- 数据管理:压缩存储的文件,会将其对应的原文件删除,所以我们需要保留原文件与压缩文件之间的对应关系(代码内:map,代码外:文件),保存格式为【原文件 : 压缩文件】的键值对;
技术调研
目录监控
数据管理
- 在代码中操作时使用
unordered_map
来管理数据; - 持久化存储使用文件来管理数据,C++ 文件操作:C++的I/O流
压缩与解压缩
- 使用 bundle 实现文件压缩与解压缩,详细介绍:c++文件压缩库bundle使用介绍
- 获取文件时间属性:
- C++ 在 filesystem 中有一个接口为:
last_write_time(filename)
,可以获取最后一次修改时间; - C 语言中的
struct stat
结构体是一个用来描述文件属性的结构,先定义一个该结构体,然后通过stat(文件名字, 结构体)
函数来获取改文件的属性,其中最后一次访问时间成员为st_atime
,关于该结构体的详细介绍:struct stat结构体简介
httplib文件上传与接收
客户端
- 创建客户端对象:
httplib::Client cli("服务端ip", 服务端port);
; - 组织
httplib::MultipartFormData
结构的信息,需要填充的信息有以下四种:
httplib::MultipartFormData data;
data.name:位域信息,根据不同类型的内容设置不同的名字(双方协商),服务端通过这个字段可以判断是否有我需要的信息上传了,而且可以根据这个字段的信息来确定该文件的处理方式;
data.filename:上传文件的真实名称;
data.content:文件内容;
data.content_type:文件格式;
- 创建
httplib::MultipartFormDataItems
数组,该数组存放的元素类型为httplib::MultipartFormData
,当我们组织好文件信息后,将其添加入该数组中; - 使用客户端对象进行上传,上传格式为
cli.Post("请求信息", 数组);
; - 客户端通过浏览器进行访问、下载,浏览器请求格式为:
ip:port/请求资源
;
服务端
- 创建服务端对象:
httplib::Server ser;
; - 为客户端不同的请求信息,创建不同的
void(*fun)(const httplib::Request&, httplib::Response&);
回调函数; - 对于文件上传请求,回调函数如下:
static void Upload(const httplib::Request& req, httplib::Response& res){
if(!req.has_file("上传文件的位域信息,也就是name字段")){
}
const httplib::MultipartFormData& file = req.get_file_value("上传文件的位域信息,也就是name字段");
return;
}
- 对于访问、下载等请求,则是组织相应格式的响应信息即可,需要注意的是,用户访问时展示的是所有备份文件的名字,但是可能有一部分文件已经被压缩存储了,所以下载时需要进行判断,对压缩文件进行解压缩才能下载;
- 开始监听,格式为:
ser.listen("0.0.0.0", 监听port)
,将监听 IP 设置为 “0.0.0.0” 的目的是,可以监听该主机上任意一个网卡设备的请求,这样做更加便捷可靠;
断点续传
- 假设现在用户通过浏览器要下载 xuexi.mp4 文件;
- 服务端收到请求后,检查是否为从头开始下载该文件,如果是从头开始下载,则响应信息设置为以下内容:
res.set_header("Accept-Ranges", "bytes");
res.set_header("ETag", newflag);
res.set_header("Content-Type", "application/octet-stream");
res.body = str;
res.status = 200;
- 客户端在下载的过程中出现了一些意外情况,导致下载中断,此时选择继续下载,因为服务端支持断点续传功能,所以进行断点续传;
- 客户端会通过会通过
ranges
字段向服务端请求需要断点续传的区间是哪些,ranges
字段的每一个元素都是pair
类型,first
-代表了起始位置,second
代表了结束位置; - 服务端在收到请求后,判断此次下载为断点续传,则会进行以下操作:
1. 判断该文件是否为断点续传,如果是断点续传,则获取之前传给客户端的文件唯一标识
std::string oldflag;
if(req.has_header("If-Range")){
oldflag = req.get_header_value("If-Range");
}
2. 然后拿当前文件唯一标识和旧的文件唯一表示进行比较,如果不相等,则说明备份文件被修改过了,那么就不能断点续传了,因为内容会接不上,需要全部下载,如果相等,则断点续传
if(req.has_header("If-Range") && newflag == oldflag){
begin = req.ranges[0].first;
end = req.ranges[0].second;
if(end == -1){
end = filesize - 1;
}
3. 设置头部
3.1 ("Content-Range", "bytes 起始-结束/文件大小")
std::stringstream ss;
ss << "bytes " << begin << '-' << end << '/' << filesize;
res.set_header("Content-Range", ss.str());
3.2 将备份文件的唯一标识返回给客户端,用于后续比较
res.set_header("ETag", newflag);
3.3 下载则设置为二进制传输
res.set_header("Content-Type", "application/octet-stream");
res.body = str;
res.status = 206;
}
项目代码
#pragma once
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include<vector>
#include<unordered_map>
#include<filesystem>
#include"httplib.h"
#include<Windows.h>
namespace cloud_sys {
namespace fs = std::filesystem;
class ScanDir {
private:
std::string _path;
public:
ScanDir(const std::string& path)
:_path(path)
{
if (!fs::exists(_path))
fs::create_directories(_path);
if (_path.back() != '/')
_path += '/';
}
bool Scan(std::vector<std::string>* array) {
for (auto& file : fs::directory_iterator(_path)) {
std::string name = file.path().string();
if (fs::is_directory(name))
continue;
array->push_back(name);
}
return true;
}
};
class Util {
public:
static bool FileRead(const std::string& file, std::string* body) {
body->clear();
std::ifstream infile;
infile.open(file, std::ios::binary);
if (!infile.is_open()) {
std::cout << "open file failed\n";
return false;
}
uint64_t size = fs::file_size(file);
body->resize(size);
infile.read(&(*body)[0], size);
if (!infile.good()) {
std::cout << "read file failed\n";
return false;
}
infile.close();
return true;
}
static bool FileWrite(const std::string& file, std::string body) {
std::ofstream outfile;
outfile.open(file, std::ios::binary);
if (!outfile.is_open()) {
std::cout << "open file failed\n";
return false;
}
outfile.write(&body[0], body.size());
if (!outfile.good()) {
std::cout << "write file failed\n";
return false;
}
outfile.close();
return true;
}
static int Split(const std::string& str, const std::string& ch, std::vector<std::string>* array) {
int count = 0;
int begin = 0, end = 0;
while (str.find(ch, begin) != std::string::npos) {
end = str.find(ch, begin);
std::string tmp = str.substr(begin, end - begin);
array->push_back(tmp);
begin = end + ch.size();
count++;
}
if (begin < str.size()) {
std::string tmp = str.substr(begin);
array->push_back(tmp);
count++;
}
return count;
}
};
class DataManage {
private:
std::string _path;
std::unordered_map<std::string, std::string> _map;
public:
DataManage(const std::string& path)
:_path(path)
{}
bool Read() {
std::string body, ch1, ch2;
if (!Util::FileRead(_path, &body)) {
std::cout << "FileRead failed\n";
return false;
}
std::vector<std::string> array;
ch1 += '\n';
Util::Split(body, ch1, &array);
ch2 += '=';
for (auto& e : array) {
std::vector<std::string> tmp;
Util::Split(e, ch2, &tmp);
_map[tmp[0]] = tmp[1];
}
return true;
}
bool Write() {
std::stringstream ss;
for (auto& it : _map) {
ss << it.first << '=' << it.second << '\n';
}
if (!Util::FileWrite(_path, ss.str())) {
std::cout << "FileWrite failed\n";
return false;
}
return true;
}
bool Exists(const std::string& file) {
auto it = _map.find(file);
if (it == _map.end()) {
std::cout << "cannot find file\n";
return false;
}
return true;
}
bool AddOrMod(const std::string& file, const std::string& str) {
_map[file] = str;
return true;
}
bool Del(const std::string& file) {
if (!Exists(file)) {
std::cout << "file not exists\n";
return false;
}
_map.erase(file);
return true;
}
bool Get(const std::string& file, std::string* str) {
if (!Exists(file)) {
std::cout << "not find file\n";
return false;
}
*str += _map[file];
return true;
}
};
class Client {
private:
const std::string _scandir = "./scandir";
const std::string _datafile = "./data.conf";
ScanDir _scan;
DataManage _data;
httplib::Client* _client;
public:
Client(const std::string& host, const int port)
:_scan(_scandir)
, _data(_datafile)
, _client(new httplib::Client(host, port))
{}
std::string GetFlag(const std::string& file) {
uint64_t file_size = fs::file_size(file);
auto time_type = fs::last_write_time(file).time_since_epoch().count();
uint64_t last_time = (time_type - 116444736000000000) / 10000000;
std::stringstream ss;
ss << file_size << last_time;
return ss.str();
}
bool Scan(std::vector <std::pair<std::string, std::string>>* array) {
std::vector<std::string> arr;
_scan.Scan(&arr);
for (auto& file : arr) {
std::string newid = GetFlag(file);
std::string oldid;
_data.Get(file, &oldid);
if (!_data.Exists(file) || oldid != newid) {
array->push_back(std::make_pair(file, newid));
}
}
return true;
}
bool Upload(const std::string& file) {
httplib::MultipartFormDataItems items;
httplib::MultipartFormData data;
data.name = "file";
fs::path path(file);
data.filename = path.filename().string();
data.content_type = "application/octet-stream";
Util::FileRead(file, &data.content);
items.push_back(data);
auto rsp = _client->Post("/upload", items);
if (rsp->status != 200) {
std::cout << "Upload failed\n";
return false;
}
return true;
}
bool Start() {
_data.Read();
while (1) {
std::vector <std::pair<std::string, std::string>> array;
Scan(&array);
for (auto& pair : array) {
if (!Upload(pair.first))
continue;
std::cout << pair.first << " = " << pair.second << " 上传成功\n";
_data.AddOrMod(pair.first, pair.second);
_data.Write();
}
Sleep(1000);
}
return true;
}
};
}
#pragma once
#include<iostream>
#include<fstream>
#include<sstream>
#include<string>
#include<vector>
#include<unordered_map>
#include<thread>
#include<experimental/filesystem>
#include<unistd.h>
#include<pthread.h>
#include<ctime>
#include"httplib.h"
#include"bundle.h"
namespace fs = std::experimental::filesystem;
namespace cloud_sys{
class ScanDir{
private:
std::string _path;
public:
ScanDir(const std::string& path)
:_path(path)
{
if(!fs::exists(_path))
fs::create_directories(_path);
if(_path.back() != '/')
_path += '/';
}
bool Scan(std::vector<std::string>* array){
for(auto& file : fs::directory_iterator(_path)){
array->push_back(_path + file.path().filename().string());
}
return true;
}
};
class Util{
public:
static bool FileRead(const std::string& file, std::string* body, int begin = 0, int end = -1){
body->clear();
std::ifstream infile;
infile.open(file, std::ios::binary);
if(!infile.is_open()){
std::cout << "open file failed\n";
return false;
}
uint64_t size = fs::file_size(file);
infile.seekg(begin, std::ios::beg);
if(end == -1){
end = size - 1;
}
size = end - begin + 1;
body->resize(size);
infile.read(&(*body)[0], size);
if(!infile.good()){
std::cout << "read file failed\n";
return false;
}
infile.close();
return true;
}
static bool FileWrite(const std::string& file, std::string body){
std::ofstream outfile;
outfile.open(file, std::ios::binary);
if(!outfile.is_open()){
std::cout << "open file failed\n";
return false;
}
outfile.write(&body[0], body.size());
if(!outfile.good()){
std::cout << "write file failed\n";
return false;
}
outfile.close();
return true;
}
static int Split(const std::string& str, const std::string& ch, std::vector<std::string>* array){
int count = 0;
int begin = 0, end = 0;
while(str.find(ch, begin) != std::string::npos){
end = str.find(ch, begin);
std::string tmp = str.substr(begin, end - begin);
array->push_back(tmp);
begin = end + ch.size();
count++;
}
if(begin < str.size()){
std::string tmp = str.substr(begin);
array->push_back(tmp);
count++;
}
return count;
}
static bool Compress(const std::string& file, const std::string& pack){
std::string body;
if(!FileRead(file, &body)){
std::cout << "Compress FileRead failed\n";
return false;
}
body = bundle::pack(bundle::LZIP, body);
if(!FileWrite(pack, body)){
std::cout << "Compress FileWrite failed\n";
return false;
}
unlink(file.c_str());
return true;
}
static bool UnCompress(const std::string& pack, const std::string& file){
std::string body;
if(!FileRead(pack, &body)){
std::cout << "UnCompress FileRead failed\n";
return false;
}
body = bundle::unpack(body);
if(!FileWrite(file, body)){
std::cout << "UnCompress FileWrite failed\n";
return false;
}
unlink(pack.c_str());
return true;
}
static std::string GetFlag(const std::string& file){
uint64_t file_size = fs::file_size(file);
auto time_type = fs::last_write_time(file).time_since_epoch().count();
uint64_t last_time = (time_type - 116444736000000000) / 10000000;
std::stringstream ss;
ss << file_size << last_time;
return ss.str();
}
};
class DataManage{
private:
std::string _path;
std::unordered_map<std::string, std::string> _map;
pthread_rwlock_t _rwlock;
public:
DataManage(const std::string& path)
:_path(path)
{
Read();
pthread_rwlock_init(&_rwlock, NULL);
}
~DataManage(){
pthread_rwlock_destroy(&_rwlock);
}
bool Read(){
std::string body, ch1, ch2;
if(!Util::FileRead(_path, &body)){
std::cout << "FileRead failed\n";
return false;
}
std::vector<std::string> array;
ch1 += '\n';
Util::Split(body, ch1, &array);
ch2 += '=';
for(auto& e : array){
std::vector<std::string> tmp;
Util::Split(e, ch2, &tmp);
pthread_rwlock_wrlock(&_rwlock);
_map[tmp[0]] = tmp[1];
pthread_rwlock_unlock(&_rwlock);
}
return true;
}
bool Write(){
std::stringstream ss;
pthread_rwlock_rdlock(&_rwlock);
for(auto& it : _map){
ss << it.first << '=' << it.second << '\n';
}
pthread_rwlock_unlock(&_rwlock);
if(!Util::FileWrite(_path, ss.str())){
std::cout << "FileWrite failed\n";
return false;
}
return true;
}
bool Exists(const std::string& file){
pthread_rwlock_rdlock(&_rwlock);
const auto& it = _map.find(file);
if(it == _map.end()){
std::cout << "cannot find file\n";
pthread_rwlock_unlock(&_rwlock);
return false;
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool AddOrMod(const std::string& file, const std::string& str){
pthread_rwlock_wrlock(&_rwlock);
_map[file] = str;
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool Del(const std::string& file){
if(!Exists(file)){
std::cout << "file not exists\n";
return false;
}
pthread_rwlock_wrlock(&_rwlock);
_map.erase(file);
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool Get(const std::string& file, std::string* str){
if(!Exists(file)){
std::cout << file << "not find file\n";
return false;
}
pthread_rwlock_rdlock(&_rwlock);
*str += _map[file];
pthread_rwlock_unlock(&_rwlock);
return true;
}
bool GetAll(std::vector<std::string>* array){
pthread_rwlock_rdlock(&_rwlock);
for(auto& e : _map){
array->push_back(e.first);
}
pthread_rwlock_unlock(&_rwlock);
return true;
}
};
const std::string _file = "./data.conf";
DataManage _data(_file);
class Server{
private:
static std::string _path;
httplib::Server* _server;
private:
static void Upload(const httplib::Request& req, httplib::Response& res){
if(!req.has_file("file")){
std::cout << "Upload have no file\n";
res.status = 400;
return;
}
const httplib::MultipartFormData& file = req.get_file_value("file");
std::string name = _path + file.filename;
std::string content = file.content;
if(!Util::FileWrite(name, content)){
std::cout << "FileWrite failed\n";
res.status = 500;
return;
}
_data.AddOrMod(name, name);
_data.Write();
return;
}
static void List(const httplib::Request& req, httplib::Response& res){
std::vector<std::string> array;
_data.GetAll(&array);
std::stringstream ss;
ss << "<html><head><meta http-equiv='content-type' content='text/html;charset=utf-8'></head><body>";
for(auto& file : array){
ss << "<hr />";
fs::path pth(file);
std::string name = pth.filename().string();
ss << "<a href='/download/" << name << "'><strong>" << name << "</strong></a>";
}
ss << "<hr /></body><html>";
res.body = ss.str();
res.set_header("Content-Type", "text/html");
return;
}
static void Download(const httplib::Request& req, httplib::Response& res){
std::string tmp = req.matches[1];
std::string file = _path + tmp;
std::string pack;
_data.Get(file, &pack);
if(file != pack){
if(!Util::UnCompress(pack, file)){
std::cout << "UnCompress failed\n";
res.status = 500;
return;
}
_data.AddOrMod(file, file);
_data.Write();
}
int begin = 0, end = -1;
uint64_t filesize = fs::file_size(file);
std::string newflag = Util::GetFlag(file);
std::string oldflag;
if(req.has_header("If-Range")){
oldflag = req.get_header_value("If-Range");
}
if(req.has_header("If-Range") && newflag == oldflag){
begin = req.ranges[0].first;
end = req.ranges[0].second;
if(end == -1){
end = filesize - 1;
}
std::stringstream ss;
ss << "bytes " << begin << '-' << end << '/' << filesize;
res.set_header("Content-Range", ss.str());
res.status = 206;
}
else{
res.set_header("Accept-Ranges", "bytes");
res.status = 200;
}
res.set_header("Content-Type", "application/octet-stream");
res.set_header("ETag", newflag);
if(!Util::FileRead(file, &res.body, begin, end)){
std::cout << "Download FileRead failed\n";
res.status = 500;
return;
}
return;
}
public:
Server()
:_server(new httplib::Server())
{
if(!fs::exists(_path))
fs::create_directories(_path);
if(_path.back() != '/')
_path += '/';
}
bool Start(const int port = 9000){
_server->Post("/upload", Upload);
_server->Get("/list", List);
_server->Get(R"(/download/(.*))", Download);
_server->listen("0.0.0.0", port);
}
};
std::string Server::_path = "./backup";
class IsHotFile{
private:
std::string _pack = "./packdir";
std::string _path = "./backup";
const time_t _time = 500;
ScanDir _scan;
private:
time_t LastAccess(const std::string& file){
struct stat st;
stat(file.c_str(), &st);
return st.st_atime;
}
public:
IsHotFile()
:_scan(_path)
{
if(!fs::exists(_pack))
fs::create_directories(_pack);
if(_pack.back() != '/')
_pack += '/';
}
bool Start(){
while(1){
std::vector<std::string> array;
_scan.Scan(&array);
for(auto& file : array){
time_t filetime = LastAccess(file);
time_t nowtime = time(NULL);
if(nowtime - filetime > _time){
fs::path fp(file);
std::string pack = _pack + fp.filename().string() + ".pack";
if(!Util::Compress(file, pack)){
std::cout << "Compress failed\n";
return false;
}
_data.AddOrMod(file, pack);
_data.Write();
}
}
}
return true;
}
};
}