一、实现功能以及模块划分
客户端需要实现的功能:自动对文件夹中的文件备份,保存信息
模块划分:
- 文件操作模块
- 数据管理模块
- 文件上传模块
相较于服务端,我们的客户端只需要将文件上传即可,功能并没有这么复杂
二、数据管理模块
文件是否需要备份是通过,数据管理模块管理的信息判断的。
有两种情况需要上传:
- 没有该文件的备份信息的
- 有该文件备份信息(说明该文件已经备份过一次了)但是文件被修改过,需要更新服务端的文件
需要实现的函数:
class DataManager {
private:
std::string _back_file;//持久化储存文件
std::unordered_map<std::string, std::string> _table;
public:
//初始化、持久化、新增、更新、查询
DataManager(std::string backfile);
void InitLoad();
//用于将数据解序列化的工具
size_t Split(std::string str,std::string sep,std::vector<std::string> *arry);
bool Storage();
bool Insert(std::string key, std::string val);
bool Updata(std::string key, std::string val);
bool GetOneByKey(const std::string& key, std::string* val);
};
对象的初始化以及数据的持久化存储
通过hash表对数据进行储存,first是文件路径名——文件的唯一标识,second就是:filename-filesize-上一次修改时间
我们需要储存的信息:filename-filesize-上一次修改时间
这里涉及到一个序列化和反序列化的功能实现,我们储存起来的信息就是一个字符串,肯定不能直接拿起来用的,我们需要对其解析
我们这里实现了解序列化会用到的小工具。
持久化储存文件的格式:
filename-filesize-上一次修改时间 \nfilename-filesize-上一次修改时间 \nfilename-filesize-上一次修改时间 \n
这里就储存了三个文件的信息了,当我们开启项目时,读取持久化数据,就需要对其解序列化。将这个字符串分割成一个个filename-filesize-上一次修改时间 的形式管理。
小总结:
- 类的实例化时会读取持久化信息
- 通过Split函数对字符串进行分割,实现解序列化
- 对象销毁时(也就是程序结束时)进行持久化储存
DataManager(std::string backfile) :_back_file(backfile) {
InitLoad();
}
void InitLoad() {
//文件读写
//1.先从文件获取数据
FileUtil fu(_back_file);
std::string body;
fu.GetContent(&body);
//2.解析数据,按照我们设定的格式,分割字符串
// 格式:key val\n key val\n key val\n 一行行获取数据,然后再将每一行分割
std::vector<std::string> arry;
Split(body, "\n", &arry);
//3.储存到table中去
for (auto& a : arry) {
std::vector<std::string> tem;
Split(a, " ", &tem);
if (tem.size() != 2) {
continue;
}
_table[tem[0]] = tem[1];
}
}
size_t Split(std::string str,std::string sep,std::vector<std::string> *arry) {
//sep是分割字符,str是字符串,把分割下来的数据储存到arry
//找到分割字符
size_t pos, idx=0;//idx是偏移量,pos标识分割字符位置
size_t count=0;
while (1) {
pos=str.find(sep, idx);
//然后裁剪下来
std::string tem;
//If no matches were found, the function returns string::npos.
if (pos == std::string::npos) {
break;
}
tem = str.substr(idx, pos - idx);
arry->push_back(tem);
count++;
idx = pos + sep.size();
}
if (idx < str.size()) {
arry->push_back(str.substr(idx,str.size()-idx));
count++;
}
//aac afd asd
return count;
}
bool Storage() {
//判断一下有没有数据
if (_table.empty()) {
return false;
}
//先将数据获取完全
auto it = _table.begin();
//定义一个数据流
std::stringstream ss;
for (; it != _table.end();it++) {
//格式:key val\n
ss << it->first << " " << it->second << "\n";
}
//然后再将数据写入文件
FileUtil fu(_back_file);
fu.SetContent(ss.str());
return true;
}
~DataManager() {
Storage();
}
数据管理的增查改
bool Insert(std::string key, std::string val) {
//客户端这里是单线程模式,不涉及到线程安全问题
_table[key] = val;
Storage();
return true;
}
bool Updata(std::string key, 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;
}
客户端和服务端有一点不同的是,客户端只有一个线程,在修改数据的时候不会对产生线程安全问题,就没有使用读写锁。
为了保证持久化数据的实时性,每次修改都会进行一次持久化操作。
数据管理类的源码
/*data.hpp*/
#ifndef __MY__DATA__
#define __MY__DATA__
#include<unordered_map>
#include<sstream>
#include<vector>
#include<string>
#include<iostream>
//通过table存储key---val格式的数据,这就是我们要组织的数据
namespace cloud {
class DataManager {
private:
std::string _back_file;//持久化储存文件
std::unordered_map<std::string, std::string> _table;
public:
//初始化、持久化、新增、更新
DataManager(std::string backfile) :_back_file(backfile) {
InitLoad();
}
void InitLoad() {
//文件读写
//1.先从文件获取数据
FileUtil fu(_back_file);
std::string body;
fu.GetContent(&body);
//2.解析数据,按照我们设定的格式,分割字符串
// 格式:key val\n key val\n key val\n 一行行获取数据,然后再将每一行分割
std::vector<std::string> arry;
Split(body, "\n", &arry);
//3.储存到table中去
for (auto& a : arry) {
std::vector<std::string> tem;
Split(a, " ", &tem);
if (tem.size() != 2) {
continue;
}
_table[tem[0]] = tem[1];
}
}
size_t Split(std::string str,std::string sep,std::vector<std::string> *arry) {
//sep是分割字符,str是字符串,把分割下来的数据储存到arry
//找到分割字符
size_t pos, idx=0;//idx是偏移量,pos标识分割字符位置
size_t count=0;
while (1) {
pos=str.find(sep, idx);
//然后裁剪下来
std::string tem;
//If no matches were found, the function returns string::npos.
if (pos == std::string::npos) {
break;
}
tem = str.substr(idx, pos - idx);
arry->push_back(tem);
count++;
idx = pos + sep.size();
}
if (idx < str.size()) {
arry->push_back(str.substr(idx,str.size()-idx));
count++;
}
//aac afd asd
return count;
}
bool Storage() {
//判断一下有没有数据
if (_table.empty()) {
return false;
}
//先将数据获取完全
auto it = _table.begin();
//定义一个数据流
std::stringstream ss;
for (; it != _table.end();it++) {
//格式:key val\n
ss << it->first << " " << it->second << "\n";
}
//然后再将数据写入文件
FileUtil fu(_back_file);
fu.SetContent(ss.str());
return true;
}
bool Insert(std::string key, std::string val) {
//客户端这里是单线程模式,不涉及到线程安全问题
_table[key] = val;
Storage();
return true;
}
bool Updata(std::string key, 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;
}
~DataManager() {
Storage();
}
};
}
#endif
三、文件管理模块
与服务端不同的是:
- 客户端不需要使用文件压缩
- 客户端不需要使用序列化,因为自己实现了
这样也有效避免了json和bundle库安装的操作了,功能简单自己就能实现。
源码
/*util.hpp*/
#ifndef __MY_UTIL__
#define __MY_UTIL__
#include<iostream>
#include<fstream>
#include<string>
#include<vector>
#include<sys/stat.h>
#include<experimental/filesystem>
#include<stdio.h>
namespace cloud{
namespace fs=std::experimental::filesystem;
class FileUtil{
private:
std::string _filename;
public:
FileUtil(const std::string &filename):_filename(filename){}
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 last mtimg 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 last atimg failed!\n";
return -1;
}
return st.st_atime;
}
std::string FileName()
{
/*size_t pos;
std::string filename="/"+_filename;
pos=filename.find_last_of("/");
if(pos==std::string::npos)
{
std::cout<<"find name failed!(util.hpp)\n";
exit(1);
}*/
return fs::path(_filename).filename().string();
}
bool GetPosLen(std::string &body,size_t pos,size_t len)
{
if(this->FileSize()<pos+len)
{
std::cout<<"GetPosLen failed!\n";
return false;
}
std::ifstream ifs;
ifs.open(_filename,std::ios::binary);
if(ifs.good()==false)
{
std::cout<<"open file failed!\n";
return false;
}
ifs.seekg(pos,std::ios::beg);
body.resize(len);
ifs.read(&body[0],len);
if(!ifs.good())
{
std::cout<<"read failede!\n";
return false;
}
ifs.close();
return true;
}
bool GetContent(std::string *body)
{
size_t fsize=this->FileSize();
return GetPosLen(*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 failede!\n";
return false;
}
ofs.write(&body[0],body.size());
if(!ofs.good())
{
std::cout<<"write failede!\n";
return false;
}
ofs.close();
return true;
}
bool Remove(){
if(this->Exists()==false)
return true;
remove(_filename.c_str());
return true;
}
bool Exists()
{
return fs::exists(_filename);
}
bool CreateDirectoryy()
{
if(this->Exists()==true)
{
return true;
}
return fs::create_directories(_filename);
}
bool ScanDirectory(std::vector<std::string>* arry )
{
CreateDirectoryy();
for(auto& p:fs::directory_iterator(_filename))
{
if(fs::is_directory(p))
continue;
arry->push_back(fs::path(p).relative_path().string());
}
return true;
}
};
}
#endif
四、文件上传模块
需要实现的功能
- 创建本地服务器,实现文件上传
- 不断检索文件夹判断文件是否需要上传
- 判断文件是否需要上传
需要实现的方法:
//1监控一个文件夹,获取文件信息
//2逐一判断是否需要备份
//3将需要备份的文件上传
namespace cloud {
#define BACKUP_FILE "./backup.dat"
#define SERVER_IP "xxxxxxxxxxxxxx"
#define SERVER_PORT 9090
class Backup {
private:
std::string _back_dir;
DataManager* _data;//获取以及更新文件数据用的
std::string GetFileIdentifier(std::string& filename);//获取文件当前信息
bool Upload(std::string& filename);//搭建客户端服务器上传文件
bool IsNeedUpload(std::string filename);//判断文件是否需要上传
public:
Backup(std::string back_dir);
void RunModule();//检索文件夹
};
}
#endif
判断文件是否需要上传
文件需要上传有两种可能:
- 没有该文件的备份信息
- 有备份性息但是文件被修改了
有一个特殊情况我们需要考虑:我们要上传一个较大的文件,这个文件正在被徐徐上传,而你检测文件夹的时候发现有一个文件一直被修改,这种情况我们不能再对齐进行上传了。
bool IsNeedUpload(std::string filename) {
//有两种可能文件需要上传
//1.没有该文件的备份信息
//2.有该文件的备份信息但是文件被修改过
//先获取id
std::string old_id;
FileUtil fu(filename);
if (_data->GetOneByKey(filename, &old_id) == true) {
//有该文件备份信息
std::string new_id;
new_id = GetFileIdentifier(filename);
if (new_id == old_id) {
//该文件没被修改过
return false;
}
else {
return true;
}
}
//没有该文件的备份信息
//但是有一个例外:一个很大的文件被缓缓拷贝进来,一直重复上传同一个文件是不合理的
//通过对比上一次修改时间判断,这个文件是不是最近一直被修改
if (time(nullptr)-fu.LastMTime()<3){
//最近三秒被修改就不需要上传
return false;
}
return true;
}
检索文件夹以及文件上传
private:
//获取文件当前信息
std::string GetFileIdentifier(std::string& filename) {
std::stringstream ss;
FileUtil fu(filename);
ss<<filename<<"-";
ss<<fu.FileSize()<<"-";
ss << fu.LastMTime();
return ss.str();
}
bool Upload(std::string& filename) {
//1.获取文件数据
std::string body;
FileUtil fu(filename);
fu.GetContent(&body);
//2.创建本地客户端
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);
//3.上传
auto res = client.Post("/upload",items);
if (!res || res->status != 200) {
return false;
}
return true;
}
public:
void RunModule() {
while (1) {
//1.遍历文件夹,获取文件名
FileUtil fu(_back_dir);
std::vector<std::string> arr;
fu.ScanDirectory(&arr);//不存在目录的话会自动创建
//2.判断文件是否需要上传
for (auto& a : arr) {
if (IsNeedUpload(a) == false) {
continue;
}
//3.上传文件,同时更新备份信息
if (Upload(a) == true) {
_data->Insert(a, GetFileIdentifier(a));
}
}
Sleep(1);
}
源码
#ifndef __MY__CLOUD__
#define __MY__CLOUD__
#include"data.hpp"
#include"util.hpp"
#include"httplib.h"
#include<Windows.h>
//1监控一个文件夹,获取文件信息
//2逐一判断是否需要备份
//3将需要备份的文件上传
namespace cloud {
#define BACKUP_FILE "./backup.dat"
#define SERVER_IP "xxxxxxxxxxxxx"
#define SERVER_PORT 9090
class Backup {
private:
std::string _back_dir;
DataManager* _data;
std::string GetFileIdentifier(std::string& filename) {
std::stringstream ss;
FileUtil fu(filename);
ss<<filename<<"-";
ss<<fu.FileSize()<<"-";
ss << fu.LastMTime();
return ss.str();
}
bool Upload(std::string& filename) {
//1.获取文件数据
std::string body;
FileUtil fu(filename);
fu.GetContent(&body);
//2.创建本地客户端
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);
//3.上传
auto res = client.Post("/upload",items);
if (!res || res->status != 200) {
return false;
}
return true;
}
bool IsNeedUpload(std::string filename) {
//有两种可能文件需要上传
//1.没有该文件的备份信息
//2.有该文件的备份信息但是文件被修改过
//先获取id
std::string old_id;
FileUtil fu(filename);
if (_data->GetOneByKey(filename, &old_id) == true) {
//有该文件备份信息
std::string new_id;
new_id = GetFileIdentifier(filename);
if (new_id == old_id) {
//该文件没被修改过
return false;
}
else {
return true;
}
}
//没有该文件的备份信息
//但是有一个例外:一个很大的文件被缓缓拷贝进来,一直重复上传同一个文件是不合理的
//通过对比上一次修改时间判断,这个文件是不是最近一直被修改
if (time(nullptr)-fu.LastMTime()<3){
//最近三秒被修改就不需要上传
return false;
}
return true;
}
public:
Backup(std::string back_dir) :_back_dir(back_dir) {
_data=new DataManager(BACKUP_FILE);
}
void RunModule() {
while (1) {
//1.遍历文件夹,获取文件名
FileUtil fu(_back_dir);
std::vector<std::string> arr;
fu.ScanDirectory(&arr);//不存在目录的话会自动创建
//2.判断文件是否需要上传
for (auto& a : arr) {
if (IsNeedUpload(a) == false) {
continue;
}
//3.上传文件,同时更新备份信息
if (Upload(a) == true) {
_data->Insert(a, GetFileIdentifier(a));
}
}
Sleep(1);
}
}
};
}
#endif