mirror of
https://gitee.com/an-tao/drogon.git
synced 2024-11-30 02:37:57 +08:00
Support sending files by range (#1001)
This commit is contained in:
parent
b68aeb43ae
commit
b2bf247048
@ -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
|
||||
/**
|
||||
|
@ -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())
|
||||
|
@ -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_;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
|
39
lib/tests/integration_test/server/RangeTestController.cc
Normal file
39
lib/tests/integration_test/server/RangeTestController.cc
Normal 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);
|
||||
}
|
33
lib/tests/integration_test/server/RangeTestController.h
Normal file
33
lib/tests/integration_test/server/RangeTestController.h
Normal 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_;
|
||||
};
|
Loading…
Reference in New Issue
Block a user