上传/下载服务
#pragma once
#include <string>
#include <optional>
#include <utility>
namespace api {
class FileService {
public:
// 上传文件(支持 raw data 或 multipart/form-data)
static bool uploadFile(const std::string& rootDir,
const std::string& filename,
const std::string& fileData);
// 下载文件(返回完整的文件路径)
static std::optional<std::string> getFilePath(const std::string& rootDir,
const std::string& filename);
// 解析 RANGE 请求头并返回第一个有效范围
static std::optional<std::pair<size_t, size_t>> parseRangeHeader(
const std::string& rangeHeader,
size_t fileSize);
// 读取指定范围的文件内容(用于流式传输)
static std::optional<std::pair<std::string, size_t>> readFileRange(
const std::string& rootDir,
const std::string& filename,
size_t start,
size_t end);
};
} // namespace api
#include "FileService.h"
#include "utils/RangeUtils.hpp" // 引入工具类
#include <fstream>
#include <filesystem>
#include <drogon/drogon.h>
namespace fs = std::filesystem;
namespace api {
bool FileService::uploadFile(const std::string& rootDir,
const std::string& filename,
const std::string& fileData) {
try {
std::string saveDir = drogon::app().getUploadPath() + "/" + rootDir;
fs::create_directories(saveDir);
std::string filePath = saveDir + "/" + filename;
std::ofstream out(filePath, std::ios::binary);
if (!out) return false;
out.write(fileData.data(), static_cast<std::streamsize>(fileData.size()));
return !!out;
} catch (...) {
return false;
}
}
std::optional<std::string> FileService::getFilePath(const std::string& rootDir,
const std::string& filename) {
std::string uploadPath = drogon::app().getUploadPath();
std::string filePath = uploadPath + "/" + rootDir + "/" + filename;
if (!fs::exists(filePath)) {
return std::nullopt;
}
return filePath;
}
std::optional<std::pair<size_t, size_t>> FileService::parseRangeHeader(
const std::string& rangeHeader,
size_t fileSize) {
std::vector<std::pair<size_t, size_t>> ranges;
if (!RangeUtils::parseByteRanges(rangeHeader, fileSize, ranges) || ranges.empty()) {
return std::nullopt;
}
return ranges[0]; // 只取第一个 RANGE 段
}
std::optional<std::pair<std::string, size_t>> FileService::readFileRange(
const std::string& rootDir,
const std::string& filename,
size_t start,
size_t end) {
auto filePathOpt = getFilePath(rootDir, filename);
if (!filePathOpt.has_value()) {
return std::nullopt;
}
auto filePath = filePathOpt.value();
auto fileSize = fs::file_size(filePath);
if (start >= fileSize) {
return std::nullopt;
}
if (end >= fileSize) {
end = fileSize - 1;
}
std::string data = RangeUtils::readFileToMemory(filePath, start, end - start + 1);
return std::make_pair(std::move(data), fileSize);
}
} // namespace api
上面的是服务层,我们会在控制器中调用它。先来看看自己写的RangeUtils
/**
* @FilePath : /BitNest/backend/utils/RangeUtils.hpp
* @Description :
* @Author : caomengxuan666 2507560089@qq.com
* @Version : 0.0.1
* @LastEditors : caomengxuan666 2507560089@qq.com
* @LastEditTime : 2025-05-08 23:09:31
* @Copyright : PESONAL DEVELOPER CMX., Copyright (c) 2025.
**/
#pragma once
#include <fstream>
#include <sstream>
#include <vector>
class RangeUtils {
public:
// 解析 Range 头部,如 "bytes=0-9,20-30"
static bool parseByteRanges(const std::string &rangeHeader,
size_t fileSize,
std::vector<std::pair<size_t, size_t>> &ranges);
// 从文件中读取指定偏移和长度的内容
static std::string readFileToMemory(const std::string &filePath,
size_t offset,
size_t length);
};
inline bool RangeUtils::parseByteRanges(const std::string &rangeHeader,
size_t fileSize,
std::vector<std::pair<size_t, size_t>> &ranges) {
ranges.clear();
const std::string prefix = "bytes=";
if (rangeHeader.compare(0, prefix.size(), prefix) != 0) {
return false;// 不是 bytes 范围请求
}
std::string rangesStr = rangeHeader.substr(prefix.size());
std::istringstream ss(rangesStr);
std::string range;
while (std::getline(ss, range, ',')) {
size_t dashPos = range.find('-');
if (dashPos == std::string::npos) continue;
std::string startStr = range.substr(0, dashPos);
std::string endStr = range.substr(dashPos + 1);
// 去除前后空格
startStr.erase(0, startStr.find_first_not_of(" \t"));
startStr.erase(startStr.find_last_not_of(" \t") + 1);
endStr.erase(0, endStr.find_first_not_of(" \t"));
endStr.erase(endStr.find_last_not_of(" \t") + 1);
if (startStr.empty() || endStr.empty()) continue;
try {
size_t start = std::stoull(startStr);
size_t end = std::stoull(endStr);
if (end >= fileSize) end = fileSize - 1;
if (start > end) continue;
ranges.emplace_back(start, end);
} catch (...) {
return false;
}
}
return !ranges.empty();
}
inline std::string RangeUtils::readFileToMemory(const std::string &filePath,
size_t offset,
size_t length) {
std::ifstream file(filePath, std::ios::binary);
if (!file) return "";
file.seekg(offset, std::ios::beg);
std::string buffer(length, '\0');
file.read(&buffer[0], length);
buffer.resize(file.gcount());// 实际读了多少字节
return buffer;
}
我们在控制器中调用我们的服务层
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
namespace api {
class FilesController : public drogon::HttpController<FilesController> {
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(FilesController::downloadFile, "/api/download/{1}/{2}", Get, drogon::Options);
ADD_METHOD_TO(FilesController::upLoadFile, "/api/upload/{1}/{2}", Get, Post, drogon::Options);
METHOD_LIST_END
void downloadFile(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string request_file_root,
std::string filename);
void upLoadFile(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string request_file_root,
std::string filename);
};
}// namespace api
#include "FilesController.h"
#include "service/FileService.h"
#include <json/json.h>
namespace api {
void FilesController::upLoadFile(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string request_file_root,
std::string filename) {
auto contentType = req->getContentType();
if (contentType == CT_APPLICATION_OCTET_STREAM ||
contentType == CT_TEXT_PLAIN ||
contentType == CT_APPLICATION_JSON) {
const auto &fileData = req->getBody();
bool success = FileService::uploadFile(request_file_root, filename, std::string(fileData));
Json::Value json;
if (success) {
json["code"] = 200;
json["message"] = "File uploaded successfully";
} else {
json["code"] = 500;
json["message"] = "Failed to write file";
}
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
if (contentType == CT_MULTIPART_FORM_DATA) {
MultiPartParser parser;
int result = parser.parse(req);
if (result != 0) {
Json::Value json;
json["code"] = 400;
json["message"] = "Failed to parse multipart/form-data request";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
const auto &files = parser.getFiles();
if (files.empty()) {
Json::Value json;
json["code"] = 400;
json["message"] = "No files uploaded";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
const auto &uploadedFile = files[0];
std::string customPath = request_file_root + "/" + filename;
int ret = uploadedFile.save(customPath);
if (ret != 0) {
Json::Value json;
json["code"] = 500;
json["message"] = "Failed to save file";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
Json::Value json;
json["code"] = 200;
json["message"] = "File uploaded successfully";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
Json::Value json;
json["code"] = 400;
json["message"] = "Unsupported Content-Type";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
}
void FilesController::downloadFile(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
std::string request_file_root,
std::string filename) {
// 获取文件路径
auto filePathOpt = FileService::getFilePath(request_file_root, filename);
if (!filePathOpt.has_value()) {
Json::Value json;
json["code"] = 404;
json["message"] = "File not found";
auto resp = HttpResponse::newHttpJsonResponse(json);
callback(resp);
return;
}
// 获取文件大小
auto filePath = filePathOpt.value();
auto fileSize = std::filesystem::file_size(filePath);
// 解析 Range 请求头
auto rangeHeader = req->getHeader("Range");
auto rangeOpt = FileService::parseRangeHeader(rangeHeader, fileSize);
if (!rangeOpt.has_value()) {
auto resp = HttpResponse::newHttpResponse(
drogon::k416RequestedRangeNotSatisfiable,
drogon::CT_TEXT_PLAIN);
callback(resp);
return;
}
auto [start, end] = rangeOpt.value();
if (rangeHeader.empty()) {
// 返回完整文件
auto resp = HttpResponse::newFileResponse(filePath);
resp->setContentTypeCode(drogon::CT_APPLICATION_OCTET_STREAM);
resp->addHeader("Accept-Ranges", "bytes");
callback(resp);
return;
}
// 读取分片数据
auto chunkOpt = FileService::readFileRange(request_file_root, filename, start, end);
if (!chunkOpt.has_value()) {
auto resp = HttpResponse::newHttpResponse(
drogon::k404NotFound,
drogon::CT_TEXT_PLAIN);
callback(resp);
return;
}
auto [data, totalSize] = chunkOpt.value();
// 构造响应
auto resp = HttpResponse::newHttpResponse(
drogon::k206PartialContent,
drogon::CT_APPLICATION_OCTET_STREAM);
resp->addHeader("Content-Range",
"bytes " + std::to_string(start) + "-" +
std::to_string(end) + "/" + std::to_string(totalSize));
resp->addHeader("Accept-Ranges", "bytes");
resp->setBody(data);
callback(resp);
}
}
断点续传、客户端下载测试可以使用libcurl命令行,比如你可以用以下的测试流式下载
# 第一个 Range
curl -H "Range: bytes=0-9" http://127.0.0.1:5555/download/user_123/test.txt --output part1.bin
# 第二个 Range
curl -H "Range: bytes=10-19" http://127.0.0.1:5555/download/user_123/test.txt --output part2.bin
# 第三个 Range
curl -H "Range: bytes=20-29" http://127.0.0.1:5555/download/user_123/test.txt --output part3.bin
# 第四个 Range
curl -H "Range: bytes=30-36" http://127.0.0.1:5555/download/user_123/test.txt --output part4.bin
你也可以用vscode rest client测试
### 下载 file(分段 Range)
GET {{host}}/api/download/user_123/test.txt
Range: bytes=0-20
### 下载 file(完整)
GET {{host}}/api/download/user_123/test.txt
跨域支持
加入如下全局中间件即可
#include <drogon/HttpAppFramework.h>
#include <drogon/drogon.h>
int main() {
// 添加监听地址和端口
drogon::app().setServerHeaderField("CaoMengxuan CloudDisk V1.0.0");
drogon::app().addListener("0.0.0.0", 5555);
// 加载配置文件
drogon::app().loadConfigFile("config.json");
// 注册全局中间件:添加 CORS 响应头
drogon::app().registerPostHandlingAdvice(
[](const drogon::HttpRequestPtr &req, const drogon::HttpResponsePtr &resp) {
// 设置允许的来源
resp->addHeader("Access-Control-Allow-Origin", "*");
// 设置允许的方法和头部
resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
// Cookie 认证
// resp->addHeader("Access-Control-Allow-Credentials", "true");
});
// 启动服务
drogon::app().run();
return 0;
}
633

被折叠的 条评论
为什么被折叠?



