图床项目详解

文章目录

  • 一、图床项目介绍
  • 二、图床项目架构
  • 三、图床功能实现
    • 3.1 注册功能
    • 3.2 登录功能
    • 3.3 用户文件列表
    • 3.4 上传文件
    • 3.5 上传文件之秒传
    • 3.6 获取共享文件列表或下载榜
    • 3.7 分享/ 删除文件/ 更新下载数
    • 3.8 取消分享/ 转存/ 更新下载计数
    • 3.9 图床分享图片

一、图床项目介绍

实现一个能够上传、存储、分享图片的后端项目。

1)上传:上传文件,并且如果上传的文件在数据库中有记录,即md5匹配,则实现秒传效果。
2)分享(共享)文件:共享文件给其他已注册的用户。其他注册用户可以在 “共享文件–>文件列表” 中看到共享的文件,并且可转存到自己的文件列表或者下载。同样在自己的 “共享文件–>文件列表”中,可以查看共享文件的信息,也可以取消共享若取消共享,除非其他用户已经转存,否则就看不到。
3)分享图片:生成链接,其他未注册用户可以根据链接查看已分享的图片可在 “共享文件 -->我的共享图片” 中看到相关浏览信息也可以取消分享。
在这里插入图片描述
在这里插入图片描述

二、图床项目架构

文件上传逻辑:
客户端上传图片 ⟹ \Longrightarrow nginx代理 ⟹ \Longrightarrow 通过nginx-upload-module,上传到某个临时目录 ⟹ \Longrightarrow 透传到后端服务程序tc-http-server ⟹ \Longrightarrow reactor网络模型监听到任务,解析http请求,然后将任务交由线程池处理 ⟹ \Longrightarrow 把文件信息存储到数据库,同时把文件上传到fastdfs
在这里插入图片描述
主要的http api接口
在这里插入图片描述
reactor网络模型
在这里插入图片描述

三、图床功能实现

3.1 注册功能

在这里插入图片描述

// 回发信息给前端的格式
#define HTTP_RESPONSE_HTML                                                     \"HTTP/1.1 200 OK\r\n"                                                      \"Connection:close\r\n"                                                     \"Content-Length:%d\r\n"                                                    \"Content-Type:application/json;charset=utf-8\r\n\r\n%s"// 注册函数
int ApiRegisterUser(uint32_t conn_uuid, string &url, string &post_data) {string str_json;UNUSED(url);int ret = 0;string user_name;string nick_name;string pwd;string phone;string email;LogInfo("uuid: {}, url: {}, post_data: {}", conn_uuid, url, post_data);// 1、判断数据是否为空if (post_data.empty()) {LogError("decodeRegisterJson failed");encodeRegisterJson(1, str_json);ret = -1;goto END;}// 2、解析jsonif (decodeRegisterJson(post_data, user_name, nick_name, pwd, phone, email) <0) {LogError("decodeRegisterJson failed");encodeRegisterJson(1, str_json);ret = -1;goto END;}// 3、注册账号ret = registerUser(user_name, nick_name, pwd, phone, email);ret = encodeRegisterJson(ret, str_json);// 这里是裸数据// 发送到回发队列里
END:// 3、把状态结果按回复消息的格式打包char *str_content = new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen = str_json.length();snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,str_json.c_str());str_json = str_content;// 4、添加到回发队列CHttpConn::AddResponseData(conn_uuid, str_json);delete str_content;return ret;
}

重点看一下registerUser(user_name, nick_name, pwd, phone, email);的处理过程:先查看用户是否存在,存在就返回,不存在需要就把用户信息添加到数据库,完成注册。

int registerUser(string &user_name, string &nick_name, string &pwd,string &phone, string &email) {int ret = 0;uint32_t user_id;// 1、获取数据库连接池CDBManager *db_manager = CDBManager::getInstance();CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");AUTO_REL_DBCONN(db_manager, db_conn);// 2、先查看用户是否存在string str_sql;str_sql = formatString2("select * from user_info where user_name='%s'",user_name.c_str());CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str());    // 执行sql语句if (result_set && result_set->Next()) { // 2.1 存在用户记录,返回LogWarn("id: {}, user_name: {}  已经存在", result_set->GetInt("id"), result_set->GetString("user_name"));delete result_set;ret = 2;} else { // 2.2 如果不存在,注册time_t now;char create_time[TIME_STRING_LEN];//获取当前时间now = time(NULL);strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S",localtime(&now));// 向数据库插入信息的语句str_sql = "insert into user_info ""(`user_name`,`nick_name`,`password`,`phone`,`email`,`create_""time`) values(?,?,?,?,?,?)";LogInfo("执行: {}", str_sql);CPrepareStatement *stmt = new CPrepareStatement();if (stmt->Init(db_conn->GetMysql(), str_sql)) {uint32_t index = 0;string c_time = create_time;stmt->SetParam(index++, user_name);stmt->SetParam(index++, nick_name);stmt->SetParam(index++, pwd);stmt->SetParam(index++, phone);stmt->SetParam(index++, email);stmt->SetParam(index++, c_time);bool bRet = stmt->ExecuteUpdate();if (bRet) {ret = 0;user_id = db_conn->GetInsertId();LogInfo("insert user_id: {}", user_id);} else {LogError("insert user_info failed. {}", str_sql);ret = 1;}}delete stmt;}return ret;
}

3.2 登录功能

在这里插入图片描述

int ApiUserLogin(u_int32_t conn_uuid, std::string &url, std::string &post_data)
{UNUSED(url);string user_name;string pwd;string token;string str_json;// 1、判断数据是否为空if (post_data.empty()) {encodeLoginJson(1, token, str_json);goto END;}// 2、解析jsonif (decodeLoginJson(post_data, user_name, pwd) < 0) {LogError("decodeRegisterJson failed");encodeLoginJson(1, token, str_json);goto END;}// 3、验证账号和密码是否匹配if (verifyUserPassword(user_name, pwd) < 0) {LogError("verifyUserPassword failed");encodeLoginJson(1, token, str_json);goto END;}// 4、生成token,并存储到redis中if (setToken(user_name, token) < 0) {LogError("setToken failed");encodeLoginJson(1, token, str_json);goto END;}// 5、加载 我的文件数量  我的分享图片数量if (loadMyfilesCountAndSharepictureCount(user_name) < 0) {LogError("loadMyfilesCountAndSharepictureCount failed");encodeLoginJson(1, token, str_json);goto END;}encodeLoginJson(0, token, str_json);
END:char *str_content = new char[HTTP_RESPONSE_HTML_MAX];uint32_t ulen = str_json.length();snprintf(str_content, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, ulen,str_json.c_str());str_json = str_content;CHttpConn::AddResponseData(conn_uuid, str_json);delete str_content;return 0;
}

关注一下三个过程:
verifyUserPassword(user_name, pwd),验证账号密码是否匹配。

setToken(user_name, token),生成token,并存储到redis中。所谓token相当于令牌,前面账号密码验证过后,说明你是有户口的人,放你进来。但是你在访问其他功能的时候,需要有个通关令牌,一个只有服务器和客户端前端知道这个字符串,来再次验证你的身份而不用每次都通过账号密码。于是 Token 就成了这两者之间的密钥,它可以让服务器确认请求是来自客户端还是恶意的第三方。

int setToken(string &user_name, string &token) {int ret = 0;CacheManager *cache_manager = CacheManager::getInstance();CacheConn *cache_conn = cache_manager->GetCacheConn("token");AUTO_REL_CACHECONN(cache_manager, cache_conn);token = RandomString(32); // 随机32个字母if (cache_conn) {//用户名:token, 86400有效时间为24小时cache_conn->SetEx(user_name, 86400, token); // redis做超时} else {ret = -1;}return ret;
}

loadMyfilesCountAndSharepictureCount(user_name) :加载 我的文件数量 和 我的分享图片数量

int loadMyfilesCountAndSharepictureCount(string &user_name) {int64_t redis_file_count = 0;int mysq_file_count = 0;// 1. 获取mysql 连接池CDBManager *db_manager = CDBManager::getInstance();CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");AUTO_REL_DBCONN(db_manager, db_conn);// 2. 获取redis 连接池CacheManager *cache_manager = CacheManager::getInstance();CacheConn *cache_conn = cache_manager->GetCacheConn("token");AUTO_REL_CACHECONN(cache_manager, cache_conn);// 3. 从mysql加载 用户文件个数if (DBGetUserFilesCountByUsername(db_conn, user_name, mysq_file_count) <0) {LogError("DBGetUserFilesCountByUsername failed");return -1;}//  4. 存储到redisredis_file_count = (int64_t)mysq_file_count;if (CacheSetCount(cache_conn, FILE_USER_COUNT + user_name,redis_file_count) < 0) // 失败了那下次继续从mysql加载{LogError("DBGetUserFilesCountByUsername failed");return -1;}LogInfo("FILE_USER_COUNT: {}", redis_file_count);// 5. 从mysql加载 我的分享图片数量if (DBGetSharePictureCountByUsername(db_conn, user_name, mysq_file_count) <0) {LogError("DBGetUserFilesCountByUsername failed");return -1;}// 6. 存储到redisredis_file_count = (int64_t)mysq_file_count;if (CacheSetCount(cache_conn, SHARE_PIC_COUNT + user_name,redis_file_count) < 0) // 失败了那下次继续从mysql加载{LogError("DBGetUserFilesCountByUsername failed");return -1;}LogInfo("SHARE_PIC_COUNT: {}", redis_file_count);return 0;
}

3.3 用户文件列表

查看我的文件时候,显示的是图片信息。从浏览器的抓包直观看到,我们请求的两个命令
myfiles?cmd=count:文件数量
myfiles?cmd=normal:文件列表
在这里插入图片描述
当然,我们还有按排序
/api/myfiles&cmd=pvasc 文件列表( ( 按下载量升序) )
/api/myfiles&cmd= pvdesc 文件列表( ( 按下载量降序) )
在这里插入图片描述

int ApiMyfiles(string &url, string &post_data, string &str_json) {// 解析url有没有命令// count 获取用户文件个数// display 获取用户文件信息,展示到前端char cmd[20];string user_name;string token;int ret = 0;int start = 0; //文件起点int count = 0; //文件个数//1、解析命令 解析url获取自定义参数QueryParseKeyValue(url.c_str(), "cmd", cmd, NULL);LogInfo("url: {}, cmd: {} ",url, cmd);if (strcmp(cmd, "count") == 0) {    // 2. cmd == 'count' 获取文件数量// 2.1 解析jsonif (decodeCountJson(post_data, user_name, token) < 0) {encodeCountJson(1, 0, str_json);LogError("decodeCountJson failed");return -1;}//2.2 验证登陆token,成功返回0,失败-1ret = VerifyToken(user_name, token); // util_cgi.hif (ret == 0) {// 2.3 获取文件数量if (handleUserFilesCount(user_name, count) < 0) { //获取用户文件个数LogError("handleUserFilesCount failed");encodeCountJson(1, 0, str_json);} else {LogInfo("handleUserFilesCount ok, count: {}", count);encodeCountJson(0, count, str_json);}} else {LogError("VerifyToken failed");encodeCountJson(1, 0, str_json);}return 0;} else {    // 3. cmd == 'normal' 或者 ‘pvdesc’ 或者 ‘pvasc’ 获取文件列表if ((strcmp(cmd, "normal") != 0) && (strcmp(cmd, "pvasc") != 0) &&(strcmp(cmd, "pvdesc") != 0)) {LogError("unknow cmd: {}", cmd);encodeCountJson(1, 0, str_json);}// 3.1 通过json包获取信息ret = decodeFileslistJson(post_data, user_name, token, start,count);LogInfo("user_name: {}, token:{}, start: {}, count:", user_name,token, start, count);if (ret == 0) {// 3.2 验证登陆token,成功返回0,失败-1ret = VerifyToken(user_name, token); // util_cgi.hif (ret == 0) {string str_cmd = cmd;// 3.3 获取用户文件列表if (getUserFileList(str_cmd, user_name, start, count,str_json) < 0) { LogError("getUserFileList failed");encodeCountJson(1, 0, str_json);}} else {LogError("VerifyToken failed");encodeCountJson(1, 0, str_json);}} else {LogError("decodeFileslistJson failed");encodeCountJson(1, 0, str_json);}}return 0;
}

1、myfiles?cmd=count:文件数量
在这里插入图片描述
需要注意获取文件数量,我们是先从redis获取,如果redis没有,再从MySQL获取。如果MySQL有,从MySQL获取,并把数据写入redis。如果MySQL也没有,就报错。

int handleUserFilesCount(string &user_name, int &count) {CDBManager *db_manager = CDBManager::getInstance();CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");AUTO_REL_DBCONN(db_manager, db_conn);CacheManager *cache_manager = CacheManager::getInstance();CacheConn *cache_conn = cache_manager->GetCacheConn("token");AUTO_REL_CACHECONN(cache_manager, cache_conn);int ret = getUserFilesCount(db_conn, cache_conn, user_name, count);return ret;
}int getUserFilesCount(CDBConn *db_conn, CacheConn *cache_conn,string &user_name, int &count) {int ret = 0;int64_t file_count = 0;// 先查看用户是否存在string str_sql;// 1. 先从redis里面获取if (CacheGetCount(cache_conn, FILE_USER_COUNT + user_name, file_count) < 0) {LogWarn("CacheGetCount failed"); // 有可能是因为没有key,不要急于判断为错误file_count = 0;ret = -1;}// 2. redis没有,从mysql获取。若MySQL获取到,再写入redisif (file_count == 0) {// 2.1 从mysql加载count = 0;if (DBGetUserFilesCountByUsername(db_conn, user_name, count) < 0) { // 如果MySQL也没有就报错LogError("DBGetUserFilesCountByUsername failed");return -1;}// 2.2 将获取的数据写入redisfile_count = (int64_t)count;if (CacheSetCount(cache_conn, FILE_USER_COUNT + user_name, file_count) <0) {LogError("CacheSetCount failed");return -1;}}count = file_count;return ret;
}

2、myfiles?cmd=normal:文件列表
这是我们后端程序返回的结果,前端根据这些字段解析展现

{"code": 0,"count": 3,"files": [{"create_time": "2023-08-29 06:45:34","file_name": "黄山景区高清地图.jpg","md5": "825a70d2c0132eca6afe84694c984120","pv": 1,"share_status": 1,"size": 875885,"type": "jpg","url": "http://192.168.3.128:80/group1/M00/00/00/wKgDgGTtlA6AYQyyAA1dbSSfUFk261.jpg","user": "handsome1"}],"total": 1
}

“code”: 0 正常,1 失败
“count”: 返回的当前文件数量,比如 2
“total”: 个人文件总共的数量
“user”: 用户名称,
“md5”: md5 值,
“create_time”: 创建时间,
“file_name”: 文件名,
“share_status”: 共享状态, 0 为没有共享, 1 为共享
“pv”: 文件下载量,下载一次加 1
“url”: URL,
“size”: 文件大小,
“type”: 文件类型

在这里插入图片描述
/api/myfiles&cmd=pvasc 文件列表( ( 按下载量升序) ) ------ 升序:order by pv asc
/api/myfiles&cmd= pvdesc 文件列表( ( 按下载量降序) ) ----- 降序:order by pv desc
这两个和normal一样,只是sql语句中的查询方式不一样。

getUserFileList()函数的大概就是,获取连接池,然后编写sql执行语句,然后交由连接池db_conn->ExecuteQuery(str_sql.c_str())执行,最后根据结果解析。这就不再赘述,都差不多。主要看看解析成json打包的过程

    LogInfo("执行: {}", str_sql);CResultSet *result_set = db_conn->ExecuteQuery(str_sql.c_str());if (result_set) {// 遍历所有的内容// 获取大小int file_index = 0;Json::Value root, files;root["code"] = 0;while (result_set->Next()) {Json::Value file;file["user"] = result_set->GetString("user");file["md5"] = result_set->GetString("md5");file["create_time"] = result_set->GetString("create_time");file["file_name"] = result_set->GetString("file_name");file["share_status"] = result_set->GetInt("shared_status");file["pv"] = result_set->GetInt("pv");file["url"] = result_set->GetString("url");file["size"] = result_set->GetInt("size");file["type"] = result_set->GetString("type");files[file_index] = file;file_index++;}root["files"] = files;root["count"] = file_index;root["total"] = total;Json::FastWriter writer;str_json = writer.write(root);delete result_set;return 0;} else {LogError("{} 操作失败", str_sql);return -1;}

对于result_set->GetString("user")。在数据库连接池设计中,我们获取到一行数据,我们将<列名和列数>插入到map中。后续我们可以根据要获取的字段名,得到列数,再到结果集查找具体数据。
举个例子,map里面有<user,1>、<md5,2>。通过_GetIndex(user)可知道user字段的数据在结果集的第一列,然后通过row_[1],获取结果集row_的第一个,也就是user对应的数据。

char *CResultSet::GetString(const char *key) {int idx = _GetIndex(key);if (idx == -1) {return NULL;} else {return row_[idx]; // 列}
}

3.4 上传文件

先介绍一下md5,每个文件都有一个唯一的 MD5 值(比如2bf8170b42cc7124b04a8886c83a9c6f),就好比每个人的指纹都是唯一的一样,效验 MD5 就是用来确保文件在传输过程中未被修改过。也就是说,如果要上传文件的MD5和数据库的某个文件的MD5匹配,意味着这两个文件一样。那么就无需重复上传。

1)客户端在上传文件之前将文件的 MD5 码上传到服务器。
2)服务器端判断是否已存在此 MD5 码,如果存在,说明该文件已存在,则此文件无需再上传,在此文件的计数器加 1,说明此文件多了一个用户共用。
3)如果服务器没有此 MD5 码,说明上传的文件是新文件,则真正上传此文件。

我们先将处理上传新文件的逻辑。
1)先通过 nginx-upload-module 模块上传文件到临时目录
2)nginx-upload-module 模块上传完文件后,通知/api/upload 后端处理程序:
3)后端处理程序 ApiUpload 函数解析文件信息,然后将临时文件上传到 fastdfs

在这里插入图片描述
1、解析客户端的post请求

------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; name="file"; filename="牛牛.png"
Content-Type: image/png------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; name="user"handsome1
------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; name="md5"aa3a04152a85412779357dc008d67ae7
------WebKitFormBoundaryLheXCMpLubcS8BsC
Content-Disposition: form-data; name="size"2292609
------WebKitFormBoundaryLheXCMpLubcS8BsC--

post请求通过nginx-upload-module加工后到达后台server,后台server逐步从post请求中解析出相应的文件信息

	//===============> 1. 解析post请求 <============// boundary=----WebKitFormBoundaryjWE3qXXORSg2hZiB 找到起始位置p1 = strstr(begin, "\r\n"); // 作用是返回字符串中首次出现子串的地址if (p1 == NULL) {LogError("wrong no boundary!");ret = -1;goto END;}//拷贝分界线strncpy(boundary, begin, p1 - begin); // 缓存分界线, 比如:WebKitFormBoundary88asdgewtgewxboundary[p1 - begin] = '\0'; //字符串结束符LogInfo("boundary: {}", boundary); //打印出来// 查找文件名file_namebegin = p1 + 2;  // 2->\r\n p2 = strstr(begin, "name=\"file_name\""); //找到file_name字段if (!p2) {LogError("wrong no file_name!");ret = -1;goto END;}p2 = strstr(begin, "\r\n"); // 找到file_name下一行p2 += 4;                    //下一行起始begin = p2;                 //  p2 = strstr(begin, "\r\n");strncpy(file_name, begin, p2 - begin);LogInfo("file_name: {}", file_name);// 其他的类似// 查找文件类型file_content_type// ……// 查找文件file_path// ……// 查找文件file_md5// ……// 查找文件file_size// ……// 查找user// ……

2、根据文件后缀对临时文件做重命名

	//===============> 2. 根据文件后缀对临时文件做重命名 <============// 获取文件名后缀GetFileSuffix(file_name, suffix); //  20230720-2.txt -> txt  mp4, jpg, pngstrcat(new_file_path, file_path); // /root/tmp/1/0045118901strcat(new_file_path, ".");  // /root/tmp/1/0045118901.strcat(new_file_path, suffix); // /root/tmp/1/0045118901.txt// 重命名 修改文件名ret = rename(file_path, new_file_path); /// /root/tmp/1/0045118901 ->  /root/tmp/1/0045118901.txtif (ret < 0) {LogError("rename {} to {} failed", file_path, new_file_path);ret = -1;goto END;}  

3、将该文件存入fastDFS中,并得到文件的file_id

    //===============> 3. 将该文件存入fastDFS中,并得到文件的file_id <============// file_id 例如 group1/M00/00/00/ctepQmIWLzWAHzHrAAAAKTIQHvk745.txtLogInfo("uploadFileToFastDfs, file_name:{}, new_file_path:{}", file_name, new_file_path);if (uploadFileToFastDfs(new_file_path, fileid) < 0) {LogError("uploadFileToFastDfs failed, unlink: {}", new_file_path);ret = unlink(new_file_path);if (ret != 0) {LogError("unlink: {} failed", new_file_path); // 删除失败则需要有个监控重新清除过期的临时文件,比如过期两天的都删除}ret = -1;goto END;}

将这个本地文件上传到 后台分布式文件系统(fastdfs)中,具体来说通过多进程的方式,子进程通过execlp()进程替换执行fastdfs写的的客户端上传文件的程序

//fdfs_upload_file 客户端的配置文件(/etc/fdfs/client.conf) 要上传的文件
fdfs_upload_file 	/etc/fdfs/client.conf 		zxm.txt

在这里插入图片描述

/* -------------------------------------------*/
/*** @brief  将一个本地文件上传到 后台分布式文件系统中* 对应 fdfs_upload_file /etc/fdfs/client.conf  完整文件路径** @param file_path  (in) 本地文件的路径* @param fileid    (out)得到上传之后的文件ID路径** @returns*      0 succ, -1 fail*/
/* -------------------------------------------*/
int uploadFileToFastDfs(char *file_path, char *fileid) {int ret = 0;pid_t pid;int fd[2];//无名管道的创建if (pipe(fd) < 0) // fd[0] → r; fd[1] → w  获取上传后返回的信息 fileid{LogError("pipe error");ret = -1;goto END;}//创建进程pid = fork(); // if (pid < 0)  //进程创建失败{LogError("fork error");ret = -1;goto END;}if (pid == 0) //子进程{//关闭读端close(fd[0]);//将标准输出 重定向 写管道dup2(fd[1],STDOUT_FILENO); // 往标准输出写的东西都会重定向到fd所指向的文件,// 当fileid产生时输出到管道fd[1]// fdfs_upload_file  /etc/fdfs/client.conf  123.txt//通过execlp执行fdfs_upload_file//如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了.execlp("fdfs_upload_file", "fdfs_upload_file",s_dfs_path_client.c_str(), file_path, NULL); //// 执行正常不会跑下面的代码//执行失败LogError("execlp fdfs_upload_file error");close(fd[1]);} else //父进程{//关闭写端close(fd[1]);//从管道中去读数据read(fd[0], fileid, TEMP_BUF_MAX_LEN); // 等待管道写入然后读取LogInfo("fileid1: {}", fileid);//去掉一个字符串两边的空白字符TrimSpace(fileid);if (strlen(fileid) == 0) {LogError("upload failed");ret = -1;goto END;}LogInfo("fileid2: {}", fileid);wait(NULL); //等待子进程结束,回收其资源close(fd[0]);}END:return ret;
}

4、删除本地临时存放的上传文件

    //================> 4. 删除本地临时存放的上传文件 <===============LogInfo("unlink: {}", new_file_path);ret = unlink(new_file_path);if (ret != 0) {LogWarn("unlink: {} failed", new_file_path); // 删除失败则需要有个监控重新清除过期的临时文件,比如过期两天的都删除}

5、得到文件所存放storage的host_name,拼接出完整的http地址

    //================> 5. 得到文件所存放storage的host_name <=================// 拼接出完整的http地址LogInfo("getFullurlByFileid, fileid: {}", fileid);if (getFullurlByFileid(fileid, fdfs_file_url) < 0) {LogError("getFullurlByFileid failed ");ret = -1;goto END;}

和把文件上传到fastdfs系统一样,都是多进程加管道通信

// 子进程
//将标准输出 重定向 写管道
dup2(fd[1], STDOUT_FILENO);
/*读取存储文件的信息文件,利用fastdfs自带的fdfs_file_info进程*/
//使用“fdfs_file_info”可以查看到文件的详细存储信息,也是跟上客户端的配置文件以及储服务器返回给我们的文件的路径execlp("fdfs_file_info", "fdfs_file_info", fdfs_cli_conf_path, fileid, NULL);// 父进程
//从管道中去读数据
read(fd[0], fdfs_file_stat_buf, TEMP_BUF_MAX_LEN);
//拼接上传文件的完整url地址--->http://host_name/group1/M00/00/00/D12313123232312.png

6、将该文件的FastDFS相关信息存入mysql中

    //===============> 将该文件的FastDFS相关信息存入mysql中 <======LogInfo("storeFileinfo, url: {}", fdfs_file_url);// 把文件写入file_infoif (storeFileinfo(db_conn, cache_conn, user, file_name, file_md5,long_file_size, fileid, fdfs_file_url) < 0) {LogError("storeFileinfo failed ");ret = -1;// 严谨而言,这里需要删除 已经上传的文件goto END;}ret = 0;value["code"] = 0;str_json = value.toStyledString(); // json序列化,  直接用writer是紧凑方式,这里toStyledString是格式化更可读方式

3.5 上传文件之秒传

上节提到,文件上传时会先校验MD5,如果匹配,则说明服务器已经存在该文件,客户端不需要再去调用 upload 接口上传文件。达到秒传效果。本节介绍的就是秒传。

在这里插入图片描述
1、sql 语句,从文件信息表file_info获取此md5值文件的文件计数器 count(表示有多少个用户拥有这个MD5值的文件)

sprintf(sql_cmd, "select count from file_info where md5 = '%s'", md5);

2、若查询不到,秒传失败

3、若查询到,再查询此用户是否已经有此文件
◼ 如果存在,说明此用户已经保存此文件,不能能重复上传
◼ 如果不存在,修改file_info对应MD5文件的count字段,进行+1,表示多一个用户拥有。同时向用户文件列表user_file_list插入一条数据。

"insert into user_file_list(user, md5, create_time, file_name, ""shared_status, pv) values ('%s', '%s', '%s', '%s', %d, %d)",user, md5, time_str, filename, 0, 0);
//秒传处理
void handleDealMd5(const char *user, const char *md5, const char *filename,string &str_json) {Md5State md5_state = Md5Failed;int ret = 0;int file_ref_count = 0;char sql_cmd[SQL_MAX_LEN] = {0};CDBManager *db_manager = CDBManager::getInstance();CDBConn *db_conn = db_manager->GetDBConn("tuchuang_slave");AUTO_REL_DBCONN(db_manager, db_conn);CacheManager *cache_manager = CacheManager::getInstance();CacheConn *cache_conn = cache_manager->GetCacheConn("token");AUTO_REL_CACHECONN(cache_manager, cache_conn);// 1、sql 语句,获取此md5值文件的文件计数器 countsprintf(sql_cmd, "select count from file_info where md5 = '%s'", md5);LogInfo("执行: {}", sql_cmd);//返回值: 0成功并保存记录集,1没有记录集,2有记录集但是没有保存,-1失败file_ref_count = 0;ret = GetResultOneCount(db_conn, sql_cmd, file_ref_count); //执行sql语句LogInfo("ret: {}, file_ref_count: {}", ret, file_ref_count);if (ret == 0) //2、有结果, 并且返回 file_info被引用的计数 file_ref_count{//2.1 查看此用户是否已经有此文件,如果存在说明此文件已上传,无需再上传sprintf(sql_cmd,"select * from user_file_list where user = '%s' and md5 = '%s' ""and file_name = '%s'",user, md5, filename);LogInfo("执行: {}", sql_cmd);//返回值: 1: 表示已经存储了,有这个文件记录ret = CheckwhetherHaveRecord(db_conn, sql_cmd); // 检测个人是否有记录if (ret == 1) //如果有结果,说明此用户已经保存此文件{LogWarn("user: {}->  filename: {}, md5: {}已存在", user, filename, md5);md5_state = Md5FileExit; // 此用户已经有该文件了,不能重复上传goto END;}// 2.2 此用户没有此文件,修改file_info中的count字段,+1 (count文件引用计数),表示多了一个用户拥有该文件sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",file_ref_count + 1, md5);LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecutePassQuery(sql_cmd)) {LogError("{} 操作失败", sql_cmd);md5_state =Md5Failed; // 更新文件引用计数失败这里也认为秒传失败,宁愿他再次上传文件goto END;}// 2.3 同时向user_file_list用户文件列表插入一条数据//当前时间戳struct timeval tv;struct tm *ptm;char time_str[128];//使用函数gettimeofday()函数来得到时间。它的精度可以达到微妙gettimeofday(&tv, NULL);ptm = localtime(&tv.tv_sec); //把从1970-1-1零点零分到当前时间系统所偏移的秒数时间转换为本地时间// strftime()// 函数根据区域设置格式化本地时间/日期,函数的功能将时间格式化,或者说格式化一个时间字符串strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", ptm);// 用户列表增加一个文件记录sprintf(sql_cmd,"insert into user_file_list(user, md5, create_time, file_name, ""shared_status, pv) values ('%s', '%s', '%s', '%s', %d, %d)",user, md5, time_str, filename, 0, 0);LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteCreate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);md5_state = Md5Failed;// 恢复引用计数sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",file_ref_count, md5);LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecutePassQuery(sql_cmd)) {LogError("{} 操作失败", sql_cmd);}goto END;}//查询用户文件数量, 用户数量+1if (CacheIncrCount(cache_conn, FILE_USER_COUNT + string(user)) < 0) {LogWarn("CacheIncrCount failed"); // 这个可以在login的时候从mysql加载}md5_state = Md5Ok;} else //3、没有结果,秒传失败{LogInfo("秒传失败");md5_state = Md5Failed;goto END;}END:/*秒传文件:秒传成功:  {"code": 0}秒传失败:  {"code":1}文件已存在:{"code": 5}*/int code = (int)md5_state;encodeMd5Json(code, str_json);
}

3.6 获取共享文件列表或下载榜

分 3 个接口:
◼ 获取共享文件个数 /api/sharefiles?cmd=count
◼ 获取共享文件列表 /api/sharefiles?cmd=normal
◼ 获取共享文件下载排行榜 /api/sharefiles?cmd=pvdesc

1、共享文件个数 /api/sharefiles?cmd=count
在这里插入图片描述
获取共享文件数量,我们是先查redis,若有直接返回即可。若没有再查MySQL,并且把数据同步到redis。

int getShareFilesCount(CDBConn *db_conn, CacheConn *cache_conn, int &count) {int ret = 0;int64_t file_count = 0;// 先查看用户是否存在string str_sql;// 1. 先从redis里面获取if (CacheGetCount(cache_conn, FILE_PUBLIC_COUNT, file_count) < 0) {LogWarn("CacheGetCount FILE_PUBLIC_COUNT failed");ret = -1;}// 2. 若数量为0,从mysql查询确定是否为0if (file_count == 0) {// 2.1 从mysql加载if (DBGetShareFilesCount(db_conn, count) < 0) {LogError("DBGetShareFilesCount failed");return -1;}file_count = (int64_t)count;// 2.2 同步数据到redisif (CacheSetCount(cache_conn, FILE_PUBLIC_COUNT, file_count) < 0) // 失败了那下次继续从mysql加载{LogError("CacheSetCount FILE_PUBLIC_COUNT failed");return -1;}ret = 0;}// 3. 若数量不为0,直接返回count = file_count;return ret;
}

2、 获取共享文件列表 /api/sharefiles?cmd=normal
在这里插入图片描述
核心就是执行sql语句,然后把返回的数据解析成json打包。

    str_sql = FormatString("select share_file_list.*, file_info.url, file_info.size, file_info.type from file_info, \share_file_list where file_info.md5 = share_file_list.md5 limit %d, %d",start, count);LogInfo("执行: {}", str_sql);result_set = db_conn->ExecuteQuery(str_sql.c_str());if (result_set) {// 遍历所有的内容// 获取大小file_count = 0;while (result_set->Next()) {Json::Value file;file["user"] = result_set->GetString("user");file["md5"] = result_set->GetString("md5");file["file_name"] = result_set->GetString("file_name");file["share_status"] = result_set->GetInt("share_status");file["pv"] = result_set->GetInt("pv");file["create_time"] = result_set->GetString("create_time");file["url"] = result_set->GetString("url");file["size"] = result_set->GetInt("size");file["type"] = result_set->GetString("type");files[file_count] = file;file_count++;}if (file_count > 0)root["files"] = files;ret = 0;delete result_set;} else {ret = -1;}

3、获取共享文件下载排行榜 /api/sharefiles?cmd=pvdesc
排行榜的逻辑比较简单,就是使用 redis 的 ZSET 做排行榜。
这里涉及到 mysql 和 redis,获取返回的是文件名和下载量。这里文件名可能重名,所以这里用了文件 md5+文件名作为唯一 ID。

1)先从 ZSET 获取排行榜,此时的 member 是 md5+文件名,score 是下载量 pv
2)然后将 member 的 md5+文件名 通过 HASH 查找对应的文件名 filename
3)将文件名 filename 和下载量 pv 返回给前端展示。
4)下载文件后,需要更新排行榜。
在这里插入图片描述
具体步骤:
a) mysql共享文件数量和redis共享文件数量对比,判断是否相等
b) 如果不相等,清空redis数据,从mysql中导入数据到redis (mysql和redis交互)

    //===3、mysql共享文件数量和redis共享文件数量对比,判断是否相等if (redis_num != sql_num) { //===4、如果不相等,清空redis数据,重新从mysql中导入数据到redis//(mysql和redis交互)// a) 清空redis有序数据cache_conn->Del(FILE_PUBLIC_ZSET); // 删除集合cache_conn->Del(FILE_NAME_HASH); // 删除hash, 理解 这里hash和集合的关系// b) 从mysql中导入数据到redis// sql语句strcpy( sql_cmd, "select md5, file_name, pv from share_file_list order by pv desc");LogInfo("执行: {}", sql_cmd);pCResultSet = db_conn->ExecuteQuery(sql_cmd);if (!pCResultSet) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// mysql_fetch_row从使用mysql_store_result得到的结果结构中提取一行,并把它放到一个行结构中。// 当数据用完或发生错误时返回NULL.while (pCResultSet->Next()) {char field[1024] = {0};string md5 = pCResultSet->GetString("md5"); // 文件的MD5string file_name = pCResultSet->GetString("file_name"); // 文件名int pv = pCResultSet->GetInt("pv");sprintf(field, "%s%s", md5.c_str(),file_name.c_str()); //文件标示,md5+文件名//增加有序集合成员cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, pv, field);//增加hash记录cache_conn->Hset(FILE_NAME_HASH, field, file_name);}}

c) 从redis读取数据,给前端反馈相应信息

3.7 分享/ 删除文件/ 更新下载数

1、/api/dealfile?cmd=share 分享文件
在这里插入图片描述
具体流程是:
◼ 先判断此文件是否已经分享,判断集合有没有这个文件,如果有,说明别人已经分享此文件,中断操作(redis操作)。
◼ 如果集合没有此元素,可能因为redis中没有记录,再从mysql中查询,如果mysql也没有,说明真没有(mysql操作)
◼ 如果mysql有记录,而redis没有记录,说明redis没有保存此文件,redis保存此文件信息后,再中断操作(redis操作)
◼ 如果此文件没有被分享,mysql保存一份持久化操作(mysql操作)
◼ redis集合中增加一个元素(redis操作)
◼ redis对应的hash也需要变化 (redis操作)

    //文件标示,md5+文件名sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());if (cache_conn) {ret2 = cache_conn->ZsetExit(FILE_PUBLIC_ZSET, fileid);} else {ret2 = 0;}LogInfo("fileid: {}, ZsetExit: {}", fileid, ret2);//===1、先判断此文件是否已经分享,判断集合有没有这个文件,如果有,说明别人已经分享此文件,中断操作(redis操作)if (ret2 == 1) //存在{LogWarn("别人已经分享此文件");share_state = ShareHad;goto END;} else if (ret2 == 0) //不存在{//===2、如果集合没有此元素,可能因为redis中没有记录,再从mysql中查询,如果mysql也没有,说明真没有(mysql操作)//===3、如果mysql有记录,而redis没有记录,说明redis没有保存此文件,redis保存此文件信息后,再中断操作(redis操作)//查看此文件别人是否已经分享了sprintf(sql_cmd,"select * from share_file_list where md5 = '%s' and file_name ""= '%s'",md5.c_str(), filename.c_str());//返回值:1有记录ret2 = CheckwhetherHaveRecord(db_conn,sql_cmd); //执行sql语句, 最后一个参数为NULL//,如果有则说明没有及时保持到redis,这里需要保存到redisif (ret2 == 1) //说明有结果,别人已经分享此文件{// redis保存此文件信息cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, 0, fileid);cache_conn->Hset(FILE_NAME_HASH, fileid, filename);LogWarn("别人已经分享此文件");share_state = ShareHad;goto END;}} else //出错{ret = -1;goto END;}//===4、如果此文件没有被分享,mysql保存一份持久化操作(mysql操作)// sql语句, 更新共享标志字段sprintf(sql_cmd,"update user_file_list set shared_status = 1 where user = '%s' and ""md5 = '%s' and file_name = '%s'",user.c_str(), md5.c_str(), filename.c_str());if (!db_conn->ExecuteUpdate(sql_cmd, false)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}time_t now;;char create_time[TIME_STRING_LEN];//获取当前时间now = time(NULL);strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S",localtime(&now));//分享文件的信息,额外保存在share_file_list保存列表/*-- user	文件所属用户-- md5 文件md5-- create_time 文件共享时间-- file_name 文件名字-- pv 文件下载量,默认值为1,下载一次加1*/sprintf(sql_cmd,"insert into share_file_list (user, md5, create_time, file_name, ""pv) values ('%s', '%s', '%s', '%s', %d)",user.c_str(), md5.c_str(), create_time, filename.c_str(), 0);if (!db_conn->ExecuteCreate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 共享文件数量+1ret = CacheIncrCount(cache_conn, FILE_PUBLIC_COUNT);if (ret < 0) {LogError("CacheIncrCount failed");ret = -1;goto END;}//===5、redis集合中增加一个元素(redis操作)cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, 0,fileid); // 如果失败是需要撤销mysql数据库的操作的//===6、redis对应的hash也需要变化 (redis操作)//     fileid ------>  filenameLogInfo("Hset FILE_NAME_HASH {}-{}", fileid, filename);ret = cache_conn->Hset(FILE_NAME_HASH, fileid, filename);if (ret < 0) {LogWarn("Hset FILE_NAME_HASH failed");}share_state = ShareOk;
END:return (int)share_state;

2、/api/dealfile?cmd=del 删除文件
在这里插入图片描述
1)先判断此文件是否已经分享
◼ 判断集合有没有这个文件,如果有,说明别人已经分享此文件(redis 操作)
◼ 如果集合没有此元素,可能因为 redis 中没有记录,再从 mysql 中查询,如果 mysql 也没有,说明真没有(mysql 操作)

    //文件标识,文件md5+文件名sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());//===1、先判断此文件是否已经分享,判断集合有没有这个文件,如果有,说明别人已经分享此文件ret2 = cache_conn->ZsetExit(FILE_PUBLIC_ZSET, fileid);LogInfo("ret2: {}", ret2);if (ret2 == 1) //存在{is_shared = 1;        //共享标志redis_has_record = 1; // redis有记录} else if (ret2 == 0)     //不存在{ //===2、如果集合没有此元素,可能因为redis中没有记录,再从mysql中查询,如果mysql也没有,说明真没有(mysql操作)is_shared = 0;// sql语句//查看该文件是否已经分享了sprintf(sql_cmd,"select shared_status from user_file_list where user = '%s' ""and md5 = '%s' and file_name = '%s'",user.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);int shared_status = 0;ret2 = GetResultOneStatus(db_conn, sql_cmd, shared_status); //执行sql语句if (ret2 == 0) {LogInfo("GetResultOneCount share  = {}", shared_status);is_shared = shar    ed_status; // 要从mysql里面获取赋值}} else //出错{ret = -1;goto END;}

2)若此文件被分享,删除分享列表(share_file_list)的数据
◼ 如果 mysql 有记录,而 redis 没有记录,那么分享文件处理只需要处理 mysql (mysql 操作)
◼ 如果 redis 有记录,mysql 和 redis 都需要处理,删除相关记录

    //说明此文件被分享,删除分享列表(share_file_list)的数据if (is_shared == 1) {//===3、如果mysql有记录,删除相关分享记录 (mysql操作)// 删除在共享列表的数据, 如果自己分享了这个文件,那同时从分享列表删除掉sprintf(sql_cmd,"delete from share_file_list where user = '%s' and md5 = '%s' ""and file_name = '%s'",user.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteDrop(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//共享文件的数量-1//查询共享文件数量if (CacheDecrCount(cache_conn, FILE_PUBLIC_COUNT) < 0) {LogError("CacheDecrCount 操作失败");ret = -1;goto END;}//===4、如果redis有记录,redis需要处理,删除相关记录if (1 == redis_has_record) {//有序集合删除指定成员cache_conn->ZsetZrem(FILE_PUBLIC_ZSET, fileid);//从hash移除相应记录cache_conn->Hdel(FILE_NAME_HASH, fileid);}}

3)删除用户文件列表的数据,并使用户文件数量-1

    //用户文件数量-1if (CacheDecrCount(cache_conn, FILE_USER_COUNT + user) < 0) {LogError("CacheDecrCount 操作失败");ret = -1;goto END;}//删除用户文件列表数据sprintf(sql_cmd,"delete from user_file_list where user = '%s' and md5 = '%s' and ""file_name = '%s'",user.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteDrop(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}

4)文件信息表(file_info)的文件引用计数count,减去1。如果count=0,说明没有用户引用此文件,需要在storage删除此文件

    //查看该文件文件引用计数sprintf(sql_cmd, "select count from file_info where md5 = '%s'",md5.c_str());LogInfo("执行: {}", sql_cmd);count = 0;ret2 = GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句LogInfo("ret2: {}, count: {}", ret2, count);if (ret2 != 0) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}if (count > 0) {count -= 1;sprintf(sql_cmd, "update file_info set count=%d where md5 = '%s'",count, md5.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}}if (count == 0) //说明没有用户引用此文件,需要在storage删除此文件{//查询文件的idsprintf(sql_cmd, "select file_id from file_info where md5 = '%s'",md5.c_str());string fileid;CResultSet *result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set->Next()) {fileid = result_set->GetString("file_id");}//删除文件信息表中该文件的信息sprintf(sql_cmd, "delete from file_info where md5 = '%s'", md5.c_str());if (!db_conn->ExecuteDrop(sql_cmd)) {LogWarn("{} 操作失败", sql_cmd);}//从storage服务器删除此文件,参数为为文件idret2 = RemoveFileFromFastDfs(fileid.c_str());if (ret2 != 0) {LogInfo("RemoveFileFromFastDfs err: {}", ret2);ret = -1;goto END;}}ret = 0;

3、/api/dealfile?cmd=pv 更新文件下载计数
用来更新指定文件的下载量,每次成功下载一个文件成功后,调用该接口更新对应文件的 pv 值。
在这里插入图片描述

    // sql语句//查看该文件的pv字段sprintf(sql_cmd,"select pv from user_file_list where user = '%s' and md5 = '%s' ""and file_name = '%s'",user.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);CResultSet *result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {pv = result_set->GetInt("pv");} else {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//更新该文件pv字段,+1sprintf(sql_cmd,"update user_file_list set pv = %d where user = '%s' and md5 = ""'%s' and file_name = '%s'",pv + 1, user.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}

3.8 取消分享/ 转存/ 更新下载计数

1、/dealsharefile?cmd=cancel 取消分享
在这里插入图片描述

   //文件标示,md5+文件名sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());// 1、共享标志设置为0sprintf(sql_cmd,"update user_file_list set shared_status = 0 where user = '%s' and ""md5 = '%s' and file_name = '%s'",user_name.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd, false)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 2、共享文件数量-1ret2 = CacheDecrCount(cache_conn, FILE_PUBLIC_COUNT);if (ret2 < 0) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//3、删除在共享列表的数据sprintf(sql_cmd,"delete from share_file_list where user = '%s' and md5 = '%s' and ""file_name = '%s'",user_name.c_str(), md5.c_str(), filename.c_str());LogInfo("执行: {}, ret = {}", sql_cmd, ret);if (!db_conn->ExecuteDrop(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//4、redis记录操作//4.1 有序集合删除指定成员ret = cache_conn->ZsetZrem(FILE_PUBLIC_ZSET, fileid);if (ret != 0) {LogInfo("执行: ZsetZrem 操作失败");goto END;}//4.2 从hash移除相应记录LogInfo("Hdel FILE_NAME_HASH  {}", fileid);ret = cache_conn->Hdel(FILE_NAME_HASH, fileid);if (ret < 0) {LogInfo("执行: hdel 操作失败: ret = {}", ret);goto END;}

2、/api/dealsharefile?cmd=e save 转存文件
◼ 先查询是个人文件列表是否已经存在该文件。
◼ 增加 file_info表的 count 计数,表示多一个人保存了该文件。
◼ 个人的 user_file_list 增加一条文件记录
◼ 更新个人的 user_file_count
当我们转存该文件后,即使分享者删除自己的文件,不会影响到我们自己转存的文件。
在这里插入图片描述

    //查看此用户,文件名和md5是否存在,如果存在说明此文件存在sprintf(sql_cmd,"select * from user_file_list where user = '%s' and md5 = '%s' and ""file_name = '%s'",user_name.c_str(), md5.c_str(), filename.c_str());ret2 = CheckwhetherHaveRecord(db_conn, sql_cmd); // 有记录返回1,错误返回-1,无记录返回0if (ret2 == 1) { //如果有结果,说明此用户已有此文件LogError("user_name: {}, filename: {}, md5: {} 已存在", user_name, filename, md5);ret = -2; //返回-2错误码goto END;}if (ret2 < 0) {LogError("{} 操作失败", sql_cmd);ret = -1; //返回-1错误码goto END;}//文件信息表,查找该文件的计数器sprintf(sql_cmd, "select count from file_info where md5 = '%s'", md5.c_str());count = 0;ret2 = GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句if (ret2 != 0) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 1、修改file_info中的count字段,+1 (count 文件引用计数)sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",count + 1, md5.c_str());if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 2、user_file_list插入一条数据gettimeofday(&tv, NULL);ptm = localtime(&tv.tv_sec); //把从1970-1-1零点零分到当前时间系统所偏移的秒数时间转换为本地时间// strftime()// 函数根据区域设置格式化本地时间/日期,函数的功能将时间格式化,或者说格式化一个时间字符串strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", ptm);// sql语句/*-- =============================================== 用户文件列表-- user	文件所属用户-- md5 文件md5-- create_time 文件创建时间-- file_name 文件名字-- shared_status 共享状态, 0为没有共享, 1为共享-- pv 文件下载量,默认值为0,下载一次加1*/sprintf(sql_cmd,"insert into user_file_list(user, md5, create_time, file_name, ""shared_status, pv) values ('%s', '%s', '%s', '%s', %d, %d)",user_name.c_str(), md5.c_str(), time_str, filename.c_str(), 0, 0);if (!db_conn->ExecuteCreate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 3、查询用户文件数量,更新该字段数量+1if (CacheIncrCount(cache_conn, FILE_USER_COUNT + user_name) < 0) {LogError("CacheIncrCount 操作失败");ret = -1;goto END;}

3、/api/dealsharefile?cmd=pv 更新共享文件下载计数
在这里插入图片描述
更新 share_file_list 的 pv 值
更新 redis 里的 FILE_PUBLIC_ZSET,用作排行榜

    //文件标示,md5+文件名sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());//===1、mysql的下载量+1(mysql操作)// sql语句//查看该共享文件的pv字段sprintf(sql_cmd,"select pv from share_file_list where md5 = '%s' and file_name = '%s'",md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);CResultSet *result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {pv = result_set->GetInt("pv");} else {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//更新该文件pv字段,+1sprintf(sql_cmd,"update share_file_list set pv = %d where md5 = '%s' and file_name ""= '%s'",pv + 1, md5.c_str(), filename.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd, false)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//===2、判断元素是否在集合中(redis操作)ret2 = cache_conn->ZsetExit(FILE_PUBLIC_ZSET, fileid);if (ret2 == 1) //===3、如果存在,有序集合score+1{              ret = cache_conn->ZsetIncr(FILE_PUBLIC_ZSET,fileid); // zrange FILE_PUBLIC_ZSET  0 -1 withscores 查看if (ret != 0) {LogError("ZsetIncr 操作失败");}} else if (ret2 == 0) //===4、如果不存在,从mysql导入数据{                     //===5、redis集合中增加一个元素(redis操作)cache_conn->ZsetAdd(FILE_PUBLIC_ZSET, pv + 1, fileid);//===6、redis对应的hash也需要变化 (redis操作)//     fileid ------>  filenamecache_conn->Hset(FILE_NAME_HASH, fileid, filename);} else //出错{ret = -1;goto END;}

3.9 图床分享图片

1、/api/sharepic?cmd=share 请求图片分享
前端:
1)访问链接:http://xxx.xxx.xxx.xxx/602fdf30db2aacf517badf456512123,该
访问链接由 web 服务器提供。
2)访问链接的 web 向 后台服务器请求图片下载地址
请求接口 http://xxx.xxx.xxx.xxx/api/sharepic?cmd=browse
请求格式
{
“urlmd5”: “602fdf30db2aacf517badf4565121234” // 来自请求链接
}
返回格式
{
“code”: 0,
“url”: “http://xxx.xxx.xxx.xxx/602fdf30db2aacf517badf4565121234”, // 图片的下载地址
“user”: “qingfu”, // 分享者用户名
“pv”: 1, // 浏览次数
“time”: “2021-12-12 11:23:0” // 分享时间
}
web 页面获取到 url 后下载图片显示,并显示分享者用户名和分享时间。
在这里插入图片描述

    // 1. 生成urlmd5string urlmd5;urlmd5 = RandomString(32); // 这里我们先简单的,直接使用随机数代替 MD5的使用LogInfo("urlmd5: {}", urlmd5);// 2. 插入share_picture_list,即添加图片分享记录time_t now;//获取当前时间now = time(NULL);strftime(create_time, TIME_STRING_LEN - 1, "%Y-%m-%d %H:%M:%S",localtime(&now));sprintf(sql_cmd,"insert into share_picture_list (user, filemd5, file_name, urlmd5, ""`key`, pv, create_time) values ('%s', '%s', '%s', '%s', '%s', %d, ""'%s')",user, filemd5, file_name, urlmd5.c_str(), key, 0, create_time);LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteCreate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}//3、文件信息表,获取该共享图片的数量sprintf(sql_cmd, "select count from file_info where md5 = '%s'", filemd5);count = 0;ret = GetResultOneCount(db_conn, sql_cmd, count); //执行sql语句if (ret != 0) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 4、修改file_info中的count字段,即该共享图片的数量+1 (count 文件引用计数)sprintf(sql_cmd, "update file_info set count = %d where md5 = '%s'",count + 1, filemd5);if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}// 5、 增加分享图片计数  SHARE_PIC_COUNTdarrenif (CacheIncrCount(cache_conn, SHARE_PIC_COUNT + string(user)) < 0) {LogError(" CacheIncrCount 操作失败");}

2、/api/sharepic?cmd=browse 请求浏览图片
请求接口 http://xxx.xxx.xxx.xxx/api/sharepic?cmd=browse,主要用来返回具体的图片下载地址。
在这里插入图片描述

   // 1. 先从分享图片列表查询到文件信息sprintf(sql_cmd,"select user, filemd5, file_name, pv, create_time from ""share_picture_list where urlmd5 = '%s'",urlmd5);LogDebug("执行: {}", sql_cmd);result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {user = result_set->GetString("user");filemd5 = result_set->GetString("filemd5");file_name = result_set->GetString("file_name");pv = result_set->GetInt("pv");create_time = result_set->GetString("create_time");delete result_set;} else {if (result_set)delete result_set;ret = -1;goto END;}// 2. 通过文件的MD5查找对应的url地址sprintf(sql_cmd, "select url from file_info where md5 ='%s'",filemd5.c_str());LogInfo("执行: {}", sql_cmd);result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {picture_url = result_set->GetString("url");delete result_set;} else {if (result_set)delete result_set;ret = -1;goto END;}// 3. 更新浏览次数, 可以考虑保存到redis,减少数据库查询的压力pv += 1; //浏览计数增加sprintf(sql_cmd,"update share_picture_list set pv = %d where urlmd5 = '%s'", pv,urlmd5);LogDebug("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;goto END;}

3、/api/sharepic?cmd=normal 我的图片分享
在这里插入图片描述

4、/api/sharepic?cmd=cance 取消图片分享
在这里插入图片描述
需要注意的是,如果文件引用次数(分享次数)为0,说明没人引用了,需要从文件信息表和fastdfs的storage删除。

    // 获取文件md5LogInfo("urlmd5: {}", urlmd5);// 1. 查看是否有分享记录,先从分享图片列表查询到文件信息sprintf(sql_cmd, "select filemd5 from share_picture_list where urlmd5 = '%s'", urlmd5);LogDebug("执行: {}", sql_cmd);result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {filemd5 = result_set->GetString("filemd5");delete result_set;} else {if (result_set)delete result_set;ret = -1;goto END;}//2、查询文件信息表(file_info)的文件引用计数countsprintf(sql_cmd,"select count, file_id from file_info where md5 = '%s' for update",filemd5.c_str()); //LogInfo("执行: {}", sql_cmd);result_set = db_conn->ExecuteQuery(sql_cmd);if (result_set && result_set->Next()) {fileid = result_set->GetString("file_id");count = result_set->GetInt("count");delete result_set;} else {if (result_set)delete result_set;LogError("{} 操作失败", sql_cmd);ret = -1;// db_conn->Rollback();goto END;}// 3. 更新文件信息表的文件引用数count - 1if (count > 0) {count -= 1;sprintf(sql_cmd, "update file_info set count=%d where md5 = '%s'",count, filemd5.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteUpdate(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;// db_conn->Rollback();goto END;}}//4、删除在共享图片列表的数据sprintf(sql_cmd,"delete from share_picture_list where user = '%s' and urlmd5 = '%s'",user, urlmd5);LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecutePassQuery(sql_cmd)) {LogError("{} 操作失败", sql_cmd);ret = -1;// db_conn->Rollback();goto END;}// 5、若count=0,说明没有用户引用此文件,需要在文件信息表和storage删除此文件if (count == 0){//删除文件信息表中该文件的信息sprintf(sql_cmd, "delete from file_info where md5 = '%s'",filemd5.c_str());LogInfo("执行: {}", sql_cmd);if (!db_conn->ExecuteDrop(sql_cmd)) {LogWarn("{} 操作失败", sql_cmd);ret = -1;goto END;}LogWarn("RemoveFileFromFastDfs");//从storage服务器删除此文件,参数为为文件idret2 = RemoveFileFromFastDfs(fileid.c_str());if (ret2 != 0) {LogError("RemoveFileFromFastDfs err: {}", ret2);ret = -1;goto END;}}// 6、共享图片数量-1if (CacheDecrCount(cache_conn, SHARE_PIC_COUNT + string(user)) < 0) {LogError("CacheDecrCount failed"); // 即使失败 也可以下次从mysql加载计数}

本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/70726.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

想考PMP,符合报名条件么?怎么报考?

报考PMP第一步就是了解报名条件&#xff1a; PMP考试如何报名&#xff1f; 先在PMI官网报英文报名&#xff0c;再在人才交流基金会上报中文报名以及缴费。 1、英文报名 PMP英文报名就是在PMI网站上提交对应的英文材料信息。不限时间&#xff0c;随时可以报名&#xff0c;报…

2023年高教社杯 国赛数学建模思路 - 案例:最短时间生产计划安排

文章目录 0 赛题思路1 模型描述2 实例2.1 问题描述2.2 数学模型2.2.1 模型流程2.2.2 符号约定2.2.3 求解模型 2.3 相关代码2.4 模型求解结果 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 最短时…

leetcode645. 错误的集合(java)

错误的集合 题目描述优化空间代码演示 题目描述 难度 - 简单 LC645 - 错误的集合 集合 s 包含从 1 到 n 的整数。不幸的是&#xff0c;因为数据错误&#xff0c;导致集合里面某一个数字复制了成了集合里面的另外一个数字的值&#xff0c;导致集合 丢失了一个数字 并且 有一个数…

Talk | ICCV‘23南洋理工大学博士后李祥泰:面向统一高效的视频分割方法设计

本期为TechBeat人工智能社区第528期线上Talk&#xff01; 北京时间9月6日(周三)20:00&#xff0c;南洋理工大学博士后研究员—李祥泰的Talk已准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “面向统一高效的视频分割方法设计”&#xff0c;他分享了其在视…

华为云云服务器评测|安装Java8环境 配置环境变量 spring项目部署 【!】存在问题未解决

目录 引出安装JDK8环境查看是否有默认jar上传Linux版本的jar包解压压缩包配置环境变量 上传jar包以及运行问题上传Jar包运行控制台开放端口访问失败—见问题记录关闭Jar的方式1.进程kill -92.ctrl c退出 问题记录&#xff1a;【!】未解决各种方式查看端口情况联系工程师最后排查…

在Ubuntu Linux系统上安装RabbitMQ服务并解决公网远程访问问题

文章目录 前言1.安装erlang 语言2.安装rabbitMQ3. 内网穿透3.1 安装cpolar内网穿透(支持一键自动安装脚本)3.2 创建HTTP隧道 4. 公网远程连接5.固定公网TCP地址5.1 保留一个固定的公网TCP端口地址5.2 配置固定公网TCP端口地址 前言 RabbitMQ是一个在 AMQP(高级消息队列协议)基…

智能电销机器人,主要体现的价值是什么

21世纪科技的迅速发展&#xff0c;人工智能逐渐走入大家的视线&#xff0c;越来越多的机器人出现在我们生活中。见的最多的有电销公司的智能语音机器人、在仓库拣货打包的机器人、商场店铺供娱乐对话的机器人。机器人活跃在各行各业中&#xff0c;降低了人工成本&#xff0c;代…

录音工具哪个好用?亲身测评,推荐这几个

“电脑录音用什么工具呀&#xff0c;前几天录制的视频声音没有录进去&#xff0c;现在需要重新补录声音&#xff0c;但是找不到合适的录音工具&#xff0c;就想问问大家&#xff0c;有没有好用的录音工具推荐呀&#xff1f;” 现如今&#xff0c;录音工具在我们的生活和工作中…

Kubernetes(k8s)上安装Prometheus和Grafana监控

Kubernetes上安装Prometheus和Grafana监控 环境准备Kubernetes准备 安装项目开始安装下载安装的项目安装项目替换镜像替换kube-state-metrics替换prometheus-adapter 修改Service修改alertmanager-service.yaml修改grafana-service.yaml修改prometheus-service.yaml 执行这些ya…

Vue + Element UI 前端篇(三):工具模块封装

Vue Element UI 实现权限管理系统 前端篇&#xff08;三&#xff09;&#xff1a;工具模块封装 封装 axios 模块 封装背景 使用axios发起一个请求是比较简单的事情&#xff0c;但是axios没有进行封装复用&#xff0c;项目越来越大&#xff0c;会引起越来越多的代码冗余&am…

浅谈Spring

Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器&#xff08;框架&#xff09;。 一、什么是IOC&#xff1f; IoC Inversion of Control 翻译成中⽂是“控制反转”的意思&#xff0c;也就是说 Spring 是⼀个“控制反转”的容器。 1.1控制反转推导 这个控制反转怎…

c语言逻辑思维

c语言逻辑思维 1.如何问问题? 有甲、乙两人&#xff0c;其中&#xff0c;甲只说假话&#xff0c;而不说真话;乙则是只说真话&#xff0c;不说假话。但是&#xff0c;他们两个人在回答别人的问题时&#xff0c;只通过点头与摇头来表示&#xff0c;不讲话。有一天&#xff0c;一…

Unity中Shader的时间_Time

文章目录 前言一、_Time.xyzw分别代表什么二、_Time怎么使用 前言 Unity中Shader的时间_Time 一、_Time.xyzw分别代表什么 _Time.y 代表当前时间 二、_Time怎么使用 在需要使用的地方直接 * _Time.y 或 x / z / w 测试代码&#xff1a; Shader "MyShader/P0_9_5&qu…

Kali Linux渗透测试技术介绍【文末送书】

文章目录 写在前面一、什么是Kali Linux二、渗透测试基础概述和方法论三、好书推荐1. 书籍简介2. 读者对象3. 随书资源 写作末尾 写在前面 对于企业网络安全建设工作的质量保障&#xff0c;业界普遍遵循PDCA&#xff08;计划&#xff08;Plan&#xff09;、实施&#xff08;Do…

【网络爬虫笔记】爬虫Robots协议语法详解

Robots协议是指一个被称为Robots Exclusion Protocol的协议。该协议的主要功能是向网络蜘蛛、机器人等搜索引擎爬虫提供一个标准的访问控制机制&#xff0c;告诉它们哪些页面可以被抓取&#xff0c;哪些页面不可以被抓取。本文将进行爬虫Robots协议语法详解&#xff0c;同时提供…

管理学国际化与领导力视角

随着全球化的进程不断加速&#xff0c;管理学的国际化已经成为一个不可忽视的趋势。在这个多元化和全球化的时代&#xff0c;管理者需要具备跨文化的领导力&#xff0c;以适应不同国家和地区的商业环境&#xff0c;并有效地管理全球团队。本文将从管理学国际化和领导力的角度探…

Java # Java容器

Java容器的分类 List ArrayList 源码 public static vooid main(String[] args){ArrayList<String> list new ArrayList<>();list.add("hello");list.add(1, "hello");list.remove("hello"); } list.add("hello"); …

Python代码雨

系列文章 序号文章目录直达链接1浪漫520表白代码https://want595.blog.csdn.net/article/details/1306668812满屏表白代码https://want595.blog.csdn.net/article/details/1297945183跳动的爱心https://want595.blog.csdn.net/article/details/1295031234漂浮爱心https://want…

InnoDB全文索引是如何实现的?

分析&回答 全文索引的底层实现为倒排索引。 为什么叫倒排索引&#xff08;反向索引&#xff09; 当表上存在全文索引时&#xff0c;就会隐式的建立一个名为FTS_DOC_ID的列&#xff0c;并在其上创建一个唯一索引&#xff0c;用于标识分词出现的记录行。你也可以显式的创建…

LeetCode刷题笔记【26】:贪心算法专题-4(柠檬水找零、根据身高重建队列、用最少数量的箭引爆气球)

文章目录 前置知识860.柠檬水找零题目描述解题思路代码 406.根据身高重建队列题目描述解题思路代码 452. 用最少数量的箭引爆气球题目描述踩坑-进行模拟正确思路的贪心 总结 前置知识 参考前文 参考文章&#xff1a; LeetCode刷题笔记【23】&#xff1a;贪心算法专题-1&#x…