项目——备份系统

项目简介

功能
  • 搭建云备份服务器与客户端,客户端针对主机上指定目录下的文件自动备份到服务器,服务端会对上传的文件进行热点文件判断,对于非热点文件进行压缩,节约磁盘空间,并且服务端支持通过浏览器进行备份文件的查看、下载,并且下载功能支持断点续传;
    在这里插入图片描述
概要设计
客户端
  • 编程环境:Windows 下的客户端程序;
  • 功能需求:监控目录,自动对指定目录下需要备份的文件进行上传备份;
  1. 目录监控:使用 C++17filesystem 遍历目录,判断文件是否需要备份,判断方式、条件如下;
    • 获取历史备份信息,获取当前监控目录下文件信息(名称、时间、大小);
    • 判断是否需要备份:当前文件信息与同名历史文件信息不符则需要备份,文件信息未在历史信息中出现需要备份;
  2. 网络通信:遍历需要上传备份的文件,获取其文件名称,使用 httplib 库中提供的接口进行文件数据上传;
  3. 数据管理:从第一步中获取到需要备份的文件,将其信息保存到下来(代码内:map,代码外:文件),保存格式为【文件名称 : 创建时间 + 文件大小】的键值对,被保存的文件也相当于历史备份信息,可作为用来判断文件是否需要备份的比对;
服务端
  • 编程环境:Linux 服务器;
  • 功能需求:对客户端上传的文件进行保存,并对文件进行检查,如果是非热点文件,那么就进行压缩存储,删除源文件,支持用户浏览器对备份文件的访问与下载;
  1. 网络通信:使用 httplib 库中提供的接口进行接收客户端上传的文件数据,并以文件形式保存;
  2. 浏览下载:用户通过浏览器可以查看当前已备份文件,可以对备份文件进行下载,并且下载还支持断点续传;
  3. 文件压缩:备份文件在一定时间内没有被访问过,那么该文件就是非热点文件,对其进行压缩存储,判断方法就是用当前时间减去文件最后一次访问时间;
  4. 数据管理:压缩存储的文件,会将其对应的原文件删除,所以我们需要保留原文件与压缩文件之间的对应关系(代码内:map,代码外:文件),保存格式为【原文件 : 压缩文件】的键值对;

技术调研

目录监控
数据管理
  • 在代码中操作时使用unordered_map来管理数据;
  • 持久化存储使用文件来管理数据,C++ 文件操作:C++的I/O流
压缩与解压缩
  • 使用 bundle 实现文件压缩与解压缩,详细介绍:c++文件压缩库bundle使用介绍
  • 获取文件时间属性:
    1. C++ 在 filesystem 中有一个接口为:last_write_time(filename),可以获取最后一次修改时间;
    2. C 语言中的struct stat结构体是一个用来描述文件属性的结构,先定义一个该结构体,然后通过stat(文件名字, 结构体)函数来获取改文件的属性,其中最后一次访问时间成员为st_atime,关于该结构体的详细介绍:struct stat结构体简介
httplib文件上传与接收
客户端
  1. 创建客户端对象:httplib::Client cli("服务端ip", 服务端port);
  2. 组织httplib::MultipartFormData结构的信息,需要填充的信息有以下四种:
httplib::MultipartFormData data;
data.name:位域信息,根据不同类型的内容设置不同的名字(双方协商),服务端通过这个字段可以判断是否有我需要的信息上传了,而且可以根据这个字段的信息来确定该文件的处理方式;
data.filename:上传文件的真实名称;
data.content:文件内容;
data.content_type:文件格式;
  1. 创建httplib::MultipartFormDataItems数组,该数组存放的元素类型为httplib::MultipartFormData,当我们组织好文件信息后,将其添加入该数组中;
  2. 使用客户端对象进行上传,上传格式为cli.Post("请求信息", 数组);
  3. 客户端通过浏览器进行访问、下载,浏览器请求格式为:ip:port/请求资源
服务端
  1. 创建服务端对象:httplib::Server ser;
  2. 为客户端不同的请求信息,创建不同的void(*fun)(const httplib::Request&, httplib::Response&);回调函数;
  3. 对于文件上传请求,回调函数如下:
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;    
}    
  1. 对于访问、下载等请求,则是组织相应格式的响应信息即可,需要注意的是,用户访问时展示的是所有备份文件的名字,但是可能有一部分文件已经被压缩存储了,所以下载时需要进行判断,对压缩文件进行解压缩才能下载;
  2. 开始监听,格式为:ser.listen("0.0.0.0", 监听port),将监听 IP 设置为 “0.0.0.0” 的目的是,可以监听该主机上任意一个网卡设备的请求,这样做更加便捷可靠;
断点续传
  1. 假设现在用户通过浏览器要下载 xuexi.mp4 文件;
  2. 服务端收到请求后,检查是否为从头开始下载该文件,如果是从头开始下载,则响应信息设置为以下内容:
//回复响应信息的对象为:res
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;//成功响应返回200
  1. 客户端在下载的过程中出现了一些意外情况,导致下载中断,此时选择继续下载,因为服务端支持断点续传功能,所以进行断点续传;
  2. 客户端会通过会通过ranges字段向服务端请求需要断点续传的区间是哪些,ranges字段的每一个元素都是pair类型,first-代表了起始位置,second代表了结束位置;
  3. 服务端在收到请求后,判断此次下载为断点续传,则会进行以下操作:
//请求对象为req,响应对象为res
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;    
    //如果end==-1,则说明是从begin位置到文件结束    
    if(end == -1){    
        //修改end为文件末尾,文件大小减一    
        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;//断点续传成功响应码为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);
			//打开失败则返回false
			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);
			//如果读取失败,则返回false
			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());
			//如果写入失败,则返回false
			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;
			//从首位开始向后查找ch
			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;
		//数据在内存中的保存方式unordered_map
		std::unordered_map<std::string, std::string> _map;
	public:
		//构造函数
		DataManage(const std::string& path)
			:_path(path)
		{}
		//从文件读取数据存入map中
		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);
			//将分割好的数据按照first、second元素格式写入map
			ch2 += '=';
			for (auto& e : array) {
				std::vector<std::string> tmp;
				Util::Split(e, ch2, &tmp);
				_map[tmp[0]] = tmp[1];
			}
			return true;
		}
		//将map中的数据写入文件中
		bool Write() {
			//将map中的所有数据存入空间,一并写入文件,此处使用流操作
			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) {
			//数组结构,每个元素都是需要上传信息的数据结构MultipartFormData
			httplib::MultipartFormDataItems items;
			//组织上传信息数据结构MultipartFormData
			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;
					//将上传过的文件写入_map中记录
					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)){
					//我们需要拿到的是文件路径,所以文件名是后缀在_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);
				//打开失败则返回false
				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);
				//如果读取失败,则返回false
				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());
				//如果写入失败,则返回false
				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;
				//从首位开始向后查找ch
				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;
			//数据在内存中的保存方式unordered_map
			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);
			}
			//从文件读取数据存入map中
			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);
				//将分割好的数据按照first、second元素格式写入map
				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;
			}
			//将map中的数据写入文件中
			bool Write(){
				//将map中的所有数据存入空间,一并写入文件,此处使用流操作
				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){
				//循环遍历_map
				pthread_rwlock_rdlock(&_rwlock);
				for(auto& e : _map){
					array->push_back(e.first);
				}
				pthread_rwlock_unlock(&_rwlock);
				return true;
			}
	};
	//_data:所有文件的情况信息,因为在服务类和热点检测类中都会用到,所以设为全局变量
	const std::string _file = "./data.conf"; // 文件备份配置信息
	DataManage _data(_file); // 文件备份信息
	//服务端
	class Server{
		private:
			static std::string _path; // 备份文件存放路径
			httplib::Server* _server; // httplib服务端
		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);
				//组织正文HTML格式
				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){
				//通过matches数组取出对应下载文件名,然后拼接路径
				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;
					//如果end==-1,则说明是从begin位置到文件结束
					if(end == -1){
						//修改end为文件末尾,文件大小减一
						end = filesize - 1;
					}
					std::stringstream ss;
					ss << "bytes " << begin << '-' << end << '/' << filesize;
					res.set_header("Content-Range", ss.str());
					//断点续传成功响应码为206
					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;
			}
	};
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值