mirror of
https://github.com/ZLMediaKit/ZLMediaKit.git
synced 2026-07-28 13:04:07 +08:00
新增支持http文件上传功能
This commit is contained in:
@@ -213,6 +213,27 @@ void api_regist(const string &api_path, const function<void(API_ARGS_STRING_ASYN
|
||||
s_map_api.emplace(api_path, toApi(func));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 文件上传处理器注册机制
|
||||
// File upload handler registration: intercepts PUT/POST body via kBroadcastBeforeHttpRequest
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Handler signature: same as API_ARGS_MAP + HttpBody::Ptr &body, so CHECK_SECRET() etc. work inside.
|
||||
using HttpBodyHandler = std::function<void(UPLOAD_ARGS_MAP)>;
|
||||
static std::map<std::string, HttpBodyHandler, StrCaseCompare> s_map_file_handler;
|
||||
|
||||
/**
|
||||
* Register a handler for a URL path. When a PUT/POST request arrives at that path,
|
||||
* the handler is invoked to create an HttpBody (e.g. HttpFileStorage) that receives
|
||||
* the request body stream directly. CHECK_SECRET() can be used inside the handler.
|
||||
*
|
||||
* @param url_path the HTTP URL path to match
|
||||
* @param handler callback that sets body; may throw AuthException to reject
|
||||
*/
|
||||
void upload_regist(const std::string &url_path, HttpBodyHandler handler) {
|
||||
s_map_file_handler.emplace(url_path, std::move(handler));
|
||||
}
|
||||
|
||||
// 获取HTTP请求中url参数、content参数 [AUTO-TRANSLATED:d161a1e1]
|
||||
// Get URL parameters and content parameters from the HTTP request
|
||||
ApiArgsType getAllArgs(const Parser &parser) {
|
||||
@@ -321,6 +342,19 @@ static inline void addHttpListener(){
|
||||
}
|
||||
},false);
|
||||
});
|
||||
|
||||
// 监听 kBroadcastBeforeHttpRequest:对已注册的 URL 路径设置 HttpBody 以接管 PUT/POST 请求体
|
||||
// Listen for kBroadcastBeforeHttpRequest: set HttpBody for registered URL paths to take over PUT/POST body
|
||||
NoticeCenter::Instance().addListener(&web_api_tag, Broadcast::kBroadcastBeforeHttpRequest, [](BroadcastBeforeHttpRequestArgs) {
|
||||
auto it = s_map_file_handler.find(parser.url());
|
||||
if (it == s_map_file_handler.end()) {
|
||||
return;
|
||||
}
|
||||
HttpSession::KeyValue headerOut;
|
||||
Json::Value val;
|
||||
auto args = getAllArgs(parser);
|
||||
it->second(sender, headerOut, ArgsMap(parser, args), val, body);
|
||||
});
|
||||
}
|
||||
|
||||
// 拉流代理器列表 [AUTO-TRANSLATED:6dcfb11f]
|
||||
@@ -771,6 +805,7 @@ void check_secret(toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &he
|
||||
if (api_secret != allArgs["secret"]) {
|
||||
throw AuthException("Incorrect secret");
|
||||
}
|
||||
val.removeMember("cookie");
|
||||
return;
|
||||
} catch (...) {
|
||||
// 未提供secret或secret不匹配,这个异常隐藏
|
||||
|
||||
@@ -192,6 +192,9 @@ using ArgsString = HttpAllArgs<std::string>;
|
||||
#define API_ARGS_STRING_ASYNC API_ARGS_STRING, const mediakit::HttpSession::HttpResponseInvoker &invoker
|
||||
#define API_ARGS_VALUE sender, headerOut, allArgs, val
|
||||
|
||||
// upload_regist 参数宏,与 API_ARGS_MAP 相同 + HttpBody::Ptr &body,支持 CHECK_SECRET()
|
||||
#define UPLOAD_ARGS_MAP toolkit::SockInfo &sender, mediakit::HttpSession::KeyValue &headerOut, const ArgsMap &allArgs, Json::Value &val, mediakit::HttpBody::Ptr &body
|
||||
|
||||
// 注册http请求参数是map<string, variant, StrCaseCompare>类型的http api [AUTO-TRANSLATED:8a273897]
|
||||
// Register http request parameters as map<string, variant, StrCaseCompare> type http api
|
||||
void api_regist(const std::string &api_path, const std::function<void(API_ARGS_MAP)> &func);
|
||||
|
||||
@@ -130,12 +130,17 @@ const string &Parser::content() const {
|
||||
return _content;
|
||||
}
|
||||
|
||||
const HttpBody::Ptr &Parser::body() const {
|
||||
return _body;
|
||||
}
|
||||
|
||||
void Parser::clear() {
|
||||
_method.clear();
|
||||
_url.clear();
|
||||
_params.clear();
|
||||
_protocol.clear();
|
||||
_content.clear();
|
||||
_body = nullptr;
|
||||
_headers.clear();
|
||||
_url_args.clear();
|
||||
}
|
||||
@@ -152,6 +157,10 @@ void Parser::setContent(string content) {
|
||||
_content = std::move(content);
|
||||
}
|
||||
|
||||
void Parser::setBody(HttpBody::Ptr body) {
|
||||
_body = std::move(body);
|
||||
}
|
||||
|
||||
StrCaseMap &Parser::getHeader() const {
|
||||
return _headers;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include "Util/util.h"
|
||||
#include "Http/HttpBody.h"
|
||||
|
||||
namespace mediakit {
|
||||
|
||||
@@ -95,6 +96,7 @@ public:
|
||||
// 获取http body或sdp [AUTO-TRANSLATED:d6fd1803]
|
||||
// Get http body or sdp
|
||||
const std::string &content() const;
|
||||
const HttpBody::Ptr &body() const;
|
||||
|
||||
// 清空,为了重用 [AUTO-TRANSLATED:cb7a16dd]
|
||||
// Clear, for reuse
|
||||
@@ -111,6 +113,7 @@ public:
|
||||
// 重新设置content [AUTO-TRANSLATED:ac8fc8c0]
|
||||
// Reset content
|
||||
void setContent(std::string content);
|
||||
void setBody(HttpBody::Ptr body);
|
||||
|
||||
// 获取header列表 [AUTO-TRANSLATED:90d90b03]
|
||||
// Get header list
|
||||
@@ -131,6 +134,7 @@ private:
|
||||
std::string _url;
|
||||
std::string _protocol;
|
||||
std::string _content;
|
||||
HttpBody::Ptr _body;
|
||||
std::string _params;
|
||||
mutable StrCaseMap _headers;
|
||||
mutable StrCaseMap _url_args;
|
||||
|
||||
@@ -61,6 +61,7 @@ const string kBroadcastMediaChanged = "kBroadcastMediaChanged";
|
||||
const string kBroadcastRecordMP4 = "kBroadcastRecordMP4";
|
||||
const string kBroadcastRecordTs = "kBroadcastRecordTs";
|
||||
const string kBroadcastHttpRequest = "kBroadcastHttpRequest";
|
||||
const string kBroadcastBeforeHttpRequest = "kBroadcastBeforeHttpRequest";
|
||||
const string kBroadcastHttpAccess = "kBroadcastHttpAccess";
|
||||
const string kBroadcastOnGetRtspRealm = "kBroadcastOnGetRtspRealm";
|
||||
const string kBroadcastOnRtspAuth = "kBroadcastOnRtspAuth";
|
||||
|
||||
@@ -57,6 +57,11 @@ extern const std::string kBroadcastRecordTs;
|
||||
extern const std::string kBroadcastHttpRequest;
|
||||
#define BroadcastHttpRequestArgs const Parser &parser, const HttpSession::HttpResponseInvoker &invoker, bool &consumed, toolkit::SockInfo &sender
|
||||
|
||||
// 收到http PUT/POST请求body前的广播,监听者可以设置body以接管请求体
|
||||
// Broadcast before receiving http PUT/POST request body, listener can set body to take over the request body
|
||||
extern const std::string kBroadcastBeforeHttpRequest;
|
||||
#define BroadcastBeforeHttpRequestArgs const Parser &parser, HttpBody::Ptr &body, HttpSession &sender
|
||||
|
||||
// 在http文件服务器中,收到http访问文件或目录的广播,通过该事件控制访问http目录的权限 [AUTO-TRANSLATED:2de426b4]
|
||||
// In the http file server, broadcast for receiving http access to files or directories. Control access permissions to the http directory through this event.
|
||||
extern const std::string kBroadcastHttpAccess;
|
||||
|
||||
@@ -400,4 +400,50 @@ Buffer::Ptr HttpBufferBody::readData(size_t size) {
|
||||
return Buffer::Ptr(std::move(_buffer));
|
||||
}
|
||||
|
||||
} // namespace mediakit
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// HttpFileStorage — write-only body backed by a file on disk
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
HttpFileStorage::HttpFileStorage(std::string file_path) {
|
||||
_fp = fopen(file_path.data(), "ab");
|
||||
if (!_fp) {
|
||||
throw std::runtime_error("HttpFileStorage: failed to open file: " + file_path + ", err: " + toolkit::get_uv_errmsg());
|
||||
}
|
||||
_path = std::move(file_path);
|
||||
}
|
||||
|
||||
HttpFileStorage::~HttpFileStorage() {
|
||||
if (_fp) {
|
||||
fflush(_fp);
|
||||
fclose(_fp);
|
||||
_fp = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void HttpFileStorage::writeData(const char *data, size_t size) {
|
||||
if (!_fp) {
|
||||
throw std::runtime_error("HttpFileStorage: file not open");
|
||||
}
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
auto written = fwrite(data, 1, size, _fp);
|
||||
if (written != size) {
|
||||
throw std::runtime_error("HttpFileStorage: fwrite failed, expected " + std::to_string(size) + ", got " + std::to_string(written));
|
||||
}
|
||||
_written += written;
|
||||
}
|
||||
|
||||
int64_t HttpFileStorage::remainSize() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Buffer::Ptr HttpFileStorage::readData(size_t size) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::string &HttpFileStorage::filePath() const {
|
||||
return _path;
|
||||
}
|
||||
|
||||
} // namespace kule
|
||||
|
||||
@@ -33,7 +33,11 @@ namespace mediakit {
|
||||
class HttpBody : public std::enable_shared_from_this<HttpBody>{
|
||||
public:
|
||||
using Ptr = std::shared_ptr<HttpBody>;
|
||||
virtual ~HttpBody() = default;
|
||||
virtual ~HttpBody() {
|
||||
if (_on_completed) {
|
||||
_on_completed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 剩余数据大小,如果返回-1, 那么就不设置content-length
|
||||
@@ -88,6 +92,19 @@ public:
|
||||
virtual int sendFile(int fd) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入数据,默认抛出异常表示不支持写入
|
||||
* Write data, default throws exception indicating write not supported
|
||||
*/
|
||||
virtual void writeData(const char *data, size_t size) {
|
||||
throw std::runtime_error("HttpBody::writeData not supported");
|
||||
}
|
||||
|
||||
void setOnCompleted(std::function<void()> on_completed) { _on_completed = std::move(on_completed); }
|
||||
|
||||
private:
|
||||
std::function<void()> _on_completed;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -127,13 +144,21 @@ private:
|
||||
toolkit::Buffer::Ptr _buffer;
|
||||
};
|
||||
|
||||
class HttpFileBodyBase : public HttpBody {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<HttpFileBodyBase>;
|
||||
|
||||
virtual void setRange(uint64_t offset, uint64_t max_size) = 0;
|
||||
virtual ~HttpFileBodyBase() = default;
|
||||
};
|
||||
|
||||
/**
|
||||
* 文件类型的content
|
||||
* File type content
|
||||
|
||||
* [AUTO-TRANSLATED:baf9c0f3]
|
||||
*/
|
||||
class HttpFileBody : public HttpBody {
|
||||
class HttpFileBody : public HttpFileBodyBase {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<HttpFileBody>;
|
||||
|
||||
@@ -159,7 +184,7 @@ public:
|
||||
|
||||
* [AUTO-TRANSLATED:30532a4e]
|
||||
*/
|
||||
void setRange(uint64_t offset, uint64_t max_size);
|
||||
void setRange(uint64_t offset, uint64_t max_size) override;
|
||||
|
||||
int64_t remainSize() override;
|
||||
toolkit::Buffer::Ptr readData(size_t size) override;
|
||||
@@ -173,6 +198,32 @@ private:
|
||||
toolkit::ResourcePool<toolkit::BufferRaw> _pool;
|
||||
};
|
||||
|
||||
/**
|
||||
* 文件写入类型的content,支持通过writeData向文件追加写入
|
||||
* File storage content, supports appending data to a file via writeData
|
||||
*/
|
||||
class HttpFileStorage : public HttpBody {
|
||||
public:
|
||||
using Ptr = std::shared_ptr<HttpFileStorage>;
|
||||
|
||||
/**
|
||||
* @param file_path 文件路径,文件以二进制追加模式打开
|
||||
* @param file_path File path, opened in binary append mode
|
||||
*/
|
||||
HttpFileStorage(std::string file_path);
|
||||
~HttpFileStorage() override;
|
||||
|
||||
void writeData(const char *data, size_t size) override;
|
||||
int64_t remainSize() override;
|
||||
toolkit::Buffer::Ptr readData(size_t size) override;
|
||||
const std::string& filePath() const;
|
||||
|
||||
private:
|
||||
FILE *_fp = nullptr;
|
||||
int64_t _written = 0;
|
||||
std::string _path;
|
||||
};
|
||||
|
||||
class HttpArgs;
|
||||
|
||||
/**
|
||||
@@ -212,7 +263,7 @@ private:
|
||||
int64_t _totalSize;
|
||||
std::string _bodyPrefix;
|
||||
std::string _bodySuffix;
|
||||
HttpFileBody::Ptr _fileBody;
|
||||
HttpFileBodyBase::Ptr _fileBody;
|
||||
};
|
||||
|
||||
}//namespace mediakit
|
||||
|
||||
@@ -138,6 +138,7 @@ void HttpRequestSplitter::input(const char *data,size_t len) {
|
||||
// _content_len < 0; Data is processed according to variable length content
|
||||
onRecvContent(ptr,_remain_data_size);//消费掉所有剩余数据
|
||||
_remain_data.clear();
|
||||
_remain_data_size = 0;
|
||||
}
|
||||
|
||||
void HttpRequestSplitter::setContentLen(ssize_t content_len) {
|
||||
|
||||
@@ -109,19 +109,28 @@ ssize_t HttpSession::onRecvHeader(const char *header, size_t len) {
|
||||
return _on_recv_body ? -1 : 0;
|
||||
}
|
||||
|
||||
if (content_len > _max_req_size) {
|
||||
HttpBody::Ptr body;
|
||||
if (_parser.method() == "PUT" || _parser.method() == "POST") {
|
||||
NOTICE_EMIT(BroadcastBeforeHttpRequestArgs, Broadcast::kBroadcastBeforeHttpRequest, _parser, body, *this);
|
||||
}
|
||||
|
||||
if (content_len > _max_req_size || body) {
|
||||
// // 不定长body或超大body //// [AUTO-TRANSLATED:8d66ee77]
|
||||
// // Indefinite length body or oversized body ////
|
||||
if (content_len != SIZE_MAX) {
|
||||
if (content_len != SIZE_MAX && !body) {
|
||||
WarnL << "Http body size is too huge: " << content_len << " > " << _max_req_size
|
||||
<< ", please set " << Http::kMaxReqSize << " in config.ini file.";
|
||||
}
|
||||
|
||||
size_t received = 0;
|
||||
auto parser = std::move(_parser);
|
||||
_on_recv_body = [this, parser, received, content_len](const char *data, size_t len) mutable {
|
||||
_on_recv_body = [this, received, content_len, body, it](const char *data, size_t len) mutable {
|
||||
received += len;
|
||||
onRecvUnlimitedContent(parser, data, len, content_len, received);
|
||||
if (body) {
|
||||
body->writeData(data, len);
|
||||
} else {
|
||||
onRecvUnlimitedContent(_parser, data, len, content_len, received);
|
||||
}
|
||||
|
||||
if (received < content_len) {
|
||||
// 还没收满 [AUTO-TRANSLATED:cecc867e]
|
||||
// Not yet received
|
||||
@@ -131,6 +140,13 @@ ssize_t HttpSession::onRecvHeader(const char *header, size_t len) {
|
||||
// 收满了 [AUTO-TRANSLATED:0c9cebd7]
|
||||
// Received full
|
||||
setContentLen(0);
|
||||
|
||||
if (body) {
|
||||
_parser.setBody(std::move(body));
|
||||
(this->*(it->second))();
|
||||
}
|
||||
_parser.clear();
|
||||
|
||||
return false;
|
||||
};
|
||||
// 声明后续都是body;Http body在本对象缓冲,不通过HttpRequestSplitter保存 [AUTO-TRANSLATED:0012b6c1]
|
||||
|
||||
Reference in New Issue
Block a user