图床项目详解

文章目录

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

相关推荐
monkey_meng1 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
hlsd#1 小时前
go 集成go-redis 缓存操作
redis·缓存·golang
瓜牛_gn2 小时前
mysql特性
数据库·mysql
奶糖趣多多3 小时前
Redis知识点
数据库·redis·缓存
CoderIsArt4 小时前
Redis的三种模式:主从模式,哨兵与集群模式
数据库·redis·缓存
Yaml47 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
追风林8 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
ketil279 小时前
Redis - String 字符串
数据库·redis·缓存
Hsu_kk10 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境10 小时前
第02章 MySQL环境搭建
数据库·mysql