图床项目详解

文章目录

  • 一、图床项目介绍
  • 二、图床项目架构
  • 三、图床功能实现
    • [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课程感兴趣的读者,可以点击链接,详细查看详细的服务器课程

相关推荐
异常君32 分钟前
高并发数据写入场景下 MySQL 的性能瓶颈与替代方案
java·mysql·性能优化
RestCloud37 分钟前
如何通过ETLCloud实现跨系统数据同步?
数据库·数据仓库·mysql·etl·数据处理·数据同步·集成平台
懒羊羊大王呀41 分钟前
Ubuntu20.04中 Redis 的安装和配置
linux·redis
程序员岳焱1 小时前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
秃头摸鱼侠2 小时前
MySQL安装与配置
数据库·mysql·adb
John Song2 小时前
Redis 集群批量删除key报错 CROSSSLOT Keys in request don‘t hash to the same slot
数据库·redis·哈希算法
tonexuan3 小时前
MySQL 8.0 绿色版安装和配置过程
数据库·mysql
JohnYan3 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql
我最厉害。,。3 小时前
Windows权限提升篇&数据库篇&MYSQL&MSSQL&ORACLE&自动化项目
数据库·mysql·sqlserver
@大嘴巴子5 小时前
MySQL知识回顾总结----数据库基础
数据库·mysql