文章目录
- 一、图床项目介绍
- 二、图床项目架构
- 三、图床功能实现
-
- [3.1 注册功能](#3.1 注册功能)
- [3.2 登录功能](#3.2 登录功能)
- [3.3 用户文件列表](#3.3 用户文件列表)
- [3.4 上传文件](#3.4 上传文件)
- [3.5 上传文件之秒传](#3.5 上传文件之秒传)
- [3.6 获取共享文件列表或下载榜](#3.6 获取共享文件列表或下载榜)
- [3.7 分享/ 删除文件/ 更新下载数](#3.7 分享/ 删除文件/ 更新下载数)
- [3.8 取消分享/ 转存/ 更新下载计数](#3.8 取消分享/ 转存/ 更新下载计数)
- [3.9 图床分享图片](#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 注册功能
cpp
// 回发信息给前端的格式
#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、解析json
if (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);
的处理过程:先查看用户是否存在,存在就返回,不存在需要就把用户信息添加到数据库,完成注册。
cpp
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 登录功能
cpp
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、解析json
if (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 就成了这两者之间的密钥,它可以让服务器确认请求是来自客户端还是恶意的第三方。
cpp
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)
:加载 我的文件数量 和 我的分享图片数量
cpp
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. 存储到redis
redis_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. 存储到redis
redis_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 文件列表( ( 按下载量降序) )
cpp
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 解析json
if (decodeCountJson(post_data, user_name, token) < 0) {
encodeCountJson(1, 0, str_json);
LogError("decodeCountJson failed");
return -1;
}
//2.2 验证登陆token,成功返回0,失败-1
ret = VerifyToken(user_name, token); // util_cgi.h
if (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,失败-1
ret = VerifyToken(user_name, token); // util_cgi.h
if (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也没有,就报错。
cpp
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获取到,再写入redis
if (file_count == 0)
{
// 2.1 从mysql加载
count = 0;
if (DBGetUserFilesCountByUsername(db_conn, user_name, count) < 0) { // 如果MySQL也没有就报错
LogError("DBGetUserFilesCountByUsername failed");
return -1;
}
// 2.2 将获取的数据写入redis
file_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:文件列表
这是我们后端程序返回的结果,前端根据这些字段解析展现
html
{
"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打包的过程
cpp
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对应的数据。
cpp
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请求
cpp
------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请求中解析出相应的文件信息
cpp
//===============> 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); // 缓存分界线, 比如:WebKitFormBoundary88asdgewtgewx
boundary[p1 - begin] = '\0'; //字符串结束符
LogInfo("boundary: {}", boundary); //打印出来
// 查找文件名file_name
begin = 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、根据文件后缀对临时文件做重命名
cpp
//===============> 2. 根据文件后缀对临时文件做重命名 <============
// 获取文件名后缀
GetFileSuffix(file_name, suffix); // 20230720-2.txt -> txt mp4, jpg, png
strcat(new_file_path, file_path); // /root/tmp/1/0045118901
strcat(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.txt
if (ret < 0) {
LogError("rename {} to {} failed", file_path, new_file_path);
ret = -1;
goto END;
}
3、将该文件存入fastDFS中,并得到文件的file_id
cpp
//===============> 3. 将该文件存入fastDFS中,并得到文件的file_id <============
// file_id 例如 group1/M00/00/00/ctepQmIWLzWAHzHrAAAAKTIQHvk745.txt
LogInfo("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写的的客户端上传文件的程序
cpp
//fdfs_upload_file 客户端的配置文件(/etc/fdfs/client.conf) 要上传的文件
fdfs_upload_file /etc/fdfs/client.conf zxm.txt
cpp
/* -------------------------------------------*/
/**
* @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、删除本地临时存放的上传文件
cpp
//================> 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地址
cpp
//================> 5. 得到文件所存放storage的host_name <=================
// 拼接出完整的http地址
LogInfo("getFullurlByFileid, fileid: {}", fileid);
if (getFullurlByFileid(fileid, fdfs_file_url) < 0) {
LogError("getFullurlByFileid failed ");
ret = -1;
goto END;
}
和把文件上传到fastdfs系统一样,都是多进程加管道通信
cpp
// 子进程
//将标准输出 重定向 写管道
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中
cpp
//===============> 将该文件的FastDFS相关信息存入mysql中 <======
LogInfo("storeFileinfo, url: {}", fdfs_file_url);
// 把文件写入file_info
if (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值的文件)
cpp
sprintf(sql_cmd, "select count from file_info where md5 = '%s'", md5);
2、若查询不到,秒传失败
3、若查询到,再查询此用户是否已经有此文件
◼ 如果存在,说明此用户已经保存此文件,不能能重复上传
◼ 如果不存在,修改file_info对应MD5文件的count字段,进行+1,表示多一个用户拥有。同时向用户文件列表user_file_list插入一条数据。
cpp
"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);
cpp
//秒传处理
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值文件的文件计数器 count
sprintf(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;
}
//查询用户文件数量, 用户数量+1
if (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。
cpp
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查询确定是否为0
if (file_count == 0) {
// 2.1 从mysql加载
if (DBGetShareFilesCount(db_conn, count) < 0) {
LogError("DBGetShareFilesCount failed");
return -1;
}
file_count = (int64_t)count;
// 2.2 同步数据到redis
if (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打包。
cpp
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交互)
cpp
//===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"); // 文件的MD5
string 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操作)
cpp
//文件标示,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,这里需要保存到redis
if (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;
}
// 共享文件数量+1
ret = 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 ------> filename
LogInfo("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 操作)
cpp
//文件标识,文件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 都需要处理,删除相关记录
cpp
//说明此文件被分享,删除分享列表(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
cpp
//用户文件数量-1
if (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删除此文件
cpp
//查看该文件文件引用计数
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删除此文件
{
//查询文件的id
sprintf(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服务器删除此文件,参数为为文件id
ret2 = RemoveFileFromFastDfs(fileid.c_str());
if (ret2 != 0) {
LogInfo("RemoveFileFromFastDfs err: {}", ret2);
ret = -1;
goto END;
}
}
ret = 0;
3、/api/dealfile?cmd=pv 更新文件下载计数
用来更新指定文件的下载量,每次成功下载一个文件成功后,调用该接口更新对应文件的 pv 值。
cpp
// 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字段,+1
sprintf(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 取消分享
cpp
//文件标示,md5+文件名
sprintf(fileid, "%s%s", md5.c_str(), filename.c_str());
// 1、共享标志设置为0
sprintf(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、共享文件数量-1
ret2 = 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
当我们转存该文件后,即使分享者删除自己的文件,不会影响到我们自己转存的文件。
cpp
//查看此用户,文件名和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,无记录返回0
if (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、查询用户文件数量,更新该字段数量+1
if (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,用作排行榜
cpp
//文件标示,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字段,+1
sprintf(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 ------> filename
cache_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 后下载图片显示,并显示分享者用户名和分享时间。
cpp
// 1. 生成urlmd5
string 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_COUNTdarren
if (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,主要用来返回具体的图片下载地址。
cpp
// 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删除。
cpp
// 获取文件md5
LogInfo("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)的文件引用计数count
sprintf(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 - 1
if (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服务器删除此文件,参数为为文件id
ret2 = RemoveFileFromFastDfs(fileid.c_str());
if (ret2 != 0) {
LogError("RemoveFileFromFastDfs err: {}", ret2);
ret = -1;
goto END;
}
}
// 6、共享图片数量-1
if (CacheDecrCount(cache_conn, SHARE_PIC_COUNT + string(user)) < 0) {
LogError("CacheDecrCount failed"); // 即使失败 也可以下次从mysql加载计数
}
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程