Support sending files by range (#1001)

This commit is contained in:
NitroMelon 2021-08-26 23:33:58 +08:00 committed by GitHub
parent b68aeb43ae
commit b2bf247048
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 231 additions and 21 deletions

View File

@ -359,6 +359,29 @@ class DROGON_EXPORT HttpResponse
const std::string &attachmentFileName = "",
ContentType type = CT_NONE);
/// Create a response that returns part of a file to the client.
/**
* @brief If offset and length can not be satisfied, statusCode will be set
* to k416RequestedRangeNotSatisfiable, and nothing else will be modified.
*
* @param fullPath is the full path to the file.
* @param offset is the offset to begin sending, in bytes.
* @param length is the total length to send, in bytes. In particular,
* length = 0 means send all content from offset till end of file.
* @param setContentRange whether set 'Content-Range' header automatically.
* @param attachmentFileName if the parameter is not empty, the browser
* does not open the file, but saves it as an attachment.
* @param type if the parameter is CT_NONE, the content type is set by
* drogon based on the file extension.
*/
static HttpResponsePtr newFileResponse(
const std::string &fullPath,
size_t offset,
size_t length,
bool setContentRange = true,
const std::string &attachmentFileName = "",
ContentType type = CT_NONE);
/// Create a response that returns a file to the client from buffer in
/// memory/stack
/**

View File

@ -233,9 +233,21 @@ HttpResponsePtr HttpResponse::newFileResponse(
const std::string &fullPath,
const std::string &attachmentFileName,
ContentType type)
{
return newFileResponse(fullPath, 0, 0, false, attachmentFileName, type);
}
HttpResponsePtr HttpResponse::newFileResponse(
const std::string &fullPath,
size_t offset,
size_t length,
bool setContentRange,
const std::string &attachmentFileName,
ContentType type)
{
std::ifstream infile(utils::toNativePath(fullPath), std::ifstream::binary);
LOG_TRACE << "send http file:" << fullPath;
LOG_TRACE << "send http file:" << fullPath << " offset " << offset
<< " length " << length;
if (!infile)
{
auto resp = HttpResponse::newNotFoundResponse();
@ -244,23 +256,54 @@ HttpResponsePtr HttpResponse::newFileResponse(
auto resp = std::make_shared<HttpResponseImpl>();
std::streambuf *pbuf = infile.rdbuf();
std::streamsize filesize = pbuf->pubseekoff(0, std::ifstream::end);
pbuf->pubseekoff(0, std::ifstream::beg); // rewind
if (HttpAppFrameworkImpl::instance().useSendfile() && filesize > 1024 * 200)
if (offset > filesize || length > filesize || // in case of overflow
offset + length > filesize)
{
resp->setStatusCode(k416RequestedRangeNotSatisfiable);
if (setContentRange)
{
char buf[64];
snprintf(buf, sizeof(buf), "bytes */%zu", filesize);
resp->addHeader("Content-Range", std::string(buf));
}
return resp;
}
if (length == 0)
{
length = filesize - offset;
}
pbuf->pubseekoff(offset, std::ifstream::beg); // rewind
if (HttpAppFrameworkImpl::instance().useSendfile() && length > 1024 * 200)
// TODO : Is 200k an appropriate value? Or set it to be configurable
{
// The advantages of sendfile() can only be reflected in sending large
// files.
resp->setSendfile(fullPath);
// Must set length with the right value! Content-Length header relys on
// this value.
resp->setSendfileRange(offset, length);
}
else
{
std::string str;
str.resize(filesize);
pbuf->sgetn(&str[0], filesize);
str.resize(length);
pbuf->sgetn(&str[0], length);
resp->setBody(std::move(str));
resp->setSendfileRange(offset, length);
}
resp->setStatusCode(k200OK);
// Set correct status code
if (length < filesize)
{
resp->setStatusCode(k206PartialContent);
}
else
{
resp->setStatusCode(k200OK);
}
// Infer content type
if (type == CT_NONE)
{
if (!attachmentFileName.empty())
@ -278,11 +321,23 @@ HttpResponsePtr HttpResponse::newFileResponse(
resp->setContentTypeCode(type);
}
// Set headers
if (!attachmentFileName.empty())
{
resp->addHeader("Content-Disposition",
"attachment; filename=" + attachmentFileName);
}
if (setContentRange && length > 0)
{
char buf[128];
snprintf(buf,
sizeof(buf),
"bytes %zu-%zu/%zu",
offset,
offset + length - 1,
filesize);
resp->addHeader("Content-Range", std::string(buf));
}
doResponseCreateAdvices(resp);
return resp;
}
@ -344,19 +399,11 @@ void HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer)
}
else
{
drogon::error_code err;
filesystem::path fsSendfile(utils::toNativePath(sendfileName_));
auto fileSize = filesystem::file_size(fsSendfile, err);
if (err)
{
LOG_SYSERR << fsSendfile << " stat error " << err.value()
<< ": " << err.message();
return;
}
auto bodyLength = sendfileRange_.second;
len = snprintf(buffer.beginWrite(),
buffer.writableBytes(),
contentLengthFormatString<decltype(fileSize)>(),
fileSize);
contentLengthFormatString<decltype(bodyLength)>(),
bodyLength);
}
buffer.hasWritten(len);
if (headers_.find("connection") == headers_.end())

View File

@ -317,10 +317,20 @@ class DROGON_EXPORT HttpResponseImpl : public HttpResponse
{
return sendfileName_;
}
using SendfileRange = std::pair<size_t, size_t>; // { offset, length }
const SendfileRange &sendfileRange() const
{
return sendfileRange_;
}
void setSendfile(const std::string &filename)
{
sendfileName_ = filename;
}
void setSendfileRange(size_t offset, size_t len)
{
sendfileRange_.first = offset;
sendfileRange_.second = len;
}
void makeHeaderString()
{
fullHeaderString_ = std::make_shared<trantor::MsgBuffer>(128);
@ -398,6 +408,8 @@ class DROGON_EXPORT HttpResponseImpl : public HttpResponse
mutable std::shared_ptr<HttpMessageBody> bodyPtr_;
ssize_t expriedTime_{-1};
std::string sendfileName_;
SendfileRange sendfileRange_{0, 0};
mutable std::shared_ptr<Json::Value> jsonPtr_;
std::shared_ptr<trantor::MsgBuffer> fullHeaderString_;

View File

@ -557,10 +557,11 @@ void HttpServer::sendResponse(const TcpConnectionPtr &conn,
{
auto httpString = respImplPtr->renderToBuffer();
conn->send(httpString);
auto &sendfileName = respImplPtr->sendfileName();
const std::string &sendfileName = respImplPtr->sendfileName();
if (!sendfileName.empty())
{
conn->sendFile(sendfileName.c_str());
const auto &range = respImplPtr->sendfileRange();
conn->sendFile(sendfileName.c_str(), range.first, range.second);
}
COZ_PROGRESS
}
@ -598,12 +599,13 @@ void HttpServer::sendResponses(
{
// Not HEAD method
respImplPtr->renderToBuffer(buffer);
auto &sendfileName = respImplPtr->sendfileName();
const std::string &sendfileName = respImplPtr->sendfileName();
if (!sendfileName.empty())
{
const auto &range = respImplPtr->sendfileRange();
conn->send(buffer);
buffer.retrieveAll();
conn->sendFile(sendfileName.c_str());
conn->sendFile(sendfileName.c_str(), range.first, range.second);
COZ_PROGRESS
}
}

View File

@ -59,6 +59,7 @@ set(INTEGRATION_TEST_SERVER_SOURCES
integration_test/server/TimeFilter.cc
integration_test/server/DigestAuthFilter.cc
integration_test/server/MethodTest.cc
integration_test/server/RangeTestController.cc
integration_test/server/main.cc)
if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)

View File

@ -691,6 +691,59 @@ void doTest(const HttpClientPtr &client, std::shared_ptr<test::Case> TEST_CTX)
CHECK((*json)["P2"] == "test");
});
// Test send file by range
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/");
client->sendRequest(
req, [req, TEST_CTX](ReqResult result, const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
CHECK(resp->getStatusCode() == k200OK);
CHECK(resp->getBody().size() == 1'000'000);
CHECK(resp->getHeader("Content-Length") == "1000000");
});
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/999980/0");
client->sendRequest(req,
[req, TEST_CTX](ReqResult result,
const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
CHECK(resp->getStatusCode() == k206PartialContent);
CHECK(resp->getBody() == "01234567890123456789");
});
// Test > 200k
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/1/500000");
client->sendRequest(
req, [req, TEST_CTX](ReqResult result, const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
CHECK(resp->getBody().size() == 500'000);
CHECK(resp->getHeader("Content-Length") == "500000");
CHECK(resp->getHeader("Content-Range") == "bytes 1-500000/1000000");
});
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/10/20");
client->sendRequest(
req, [req, TEST_CTX](ReqResult result, const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
CHECK(resp->getStatusCode() == k206PartialContent);
CHECK(resp->getBody() == "01234567890123456789");
CHECK(resp->getHeader("Content-Length") == "20");
CHECK(resp->getHeader("Content-Range") == "bytes 10-29/1000000");
});
// Test invalid range
req = HttpRequest::newHttpRequest();
req->setPath("/RangeTestController/0/2000000");
client->sendRequest(
req, [req, TEST_CTX](ReqResult result, const HttpResponsePtr &resp) {
REQUIRE(result == ReqResult::Ok);
CHECK(resp->getHeader("Content-Range") == "bytes */1000000");
CHECK(resp->getStatusCode() == k416RequestedRangeNotSatisfiable);
});
// Using .. to access a upper directory should be permitted as long as
// it never leaves the document root
req = HttpRequest::newHttpRequest();

View File

@ -0,0 +1,39 @@
#include "RangeTestController.h"
#include <fstream>
size_t RangeTestController::fileSize_ = 10000 * 100; // 1e6 Bytes
RangeTestController::RangeTestController()
{
std::ofstream outfile("./range-test.txt", std::ios::out | std::ios::trunc);
for (int i = 0; i < 10000; ++i)
{
outfile.write(
"01234567890123456789"
"01234567890123456789"
"01234567890123456789"
"01234567890123456789"
"01234567890123456789",
100);
}
}
void RangeTestController::getFile(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
auto resp = HttpResponse::newFileResponse("./range-test.txt");
callback(resp);
}
void RangeTestController::getFileByRange(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
size_t offset,
size_t length) const
{
auto resp =
HttpResponse::newFileResponse("./range-test.txt", offset, length);
callback(resp);
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <drogon/HttpController.h>
using namespace drogon;
class RangeTestController : public drogon::HttpController<RangeTestController>
{
public:
METHOD_LIST_BEGIN
// path is /RangeTestController
METHOD_ADD(RangeTestController::getFile, "/", Get);
// path is /RangeTestController/{offset}/{length}
METHOD_ADD(RangeTestController::getFileByRange, "/{offset}/{length}", Get);
METHOD_LIST_END
RangeTestController();
void getFile(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) const;
// We do not provide 'Range' header decoding, simply use path as range
// parameter.
void getFileByRange(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
size_t offset,
size_t length) const;
static size_t getFileSize()
{
return fileSize_;
}
private:
static size_t fileSize_;
};