使用drogon支持流式下载、断点续传(range),多线程下载,跨域

上传/下载服务

#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;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值