Refine multipart form.
This commit is contained in:
@@ -55,7 +55,7 @@ int main(int argc, char* argv[]) {
|
|||||||
auto r = session.Request(webcc::HttpRequestBuilder{}.Post().
|
auto r = session.Request(webcc::HttpRequestBuilder{}.Post().
|
||||||
Url(url).
|
Url(url).
|
||||||
File("file", upload_dir / "remember.txt").
|
File("file", upload_dir / "remember.txt").
|
||||||
Form("text", "text default")
|
Form("json", "{}", "application/json")
|
||||||
());
|
());
|
||||||
|
|
||||||
//std::cout << r->content() << std::endl;
|
//std::cout << r->content() << std::endl;
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ class FileUploadService : public webcc::RestService {
|
|||||||
public:
|
public:
|
||||||
void Handle(const webcc::RestRequest& request,
|
void Handle(const webcc::RestRequest& request,
|
||||||
webcc::RestResponse* response) final {
|
webcc::RestResponse* response) final {
|
||||||
if (request.http->method() == webcc::http::methods::kPost) {
|
if (request.http->method() == "POST") {
|
||||||
std::cout << "files: " << request.http->form_parts().size() << std::endl;
|
std::cout << "files: " << request.http->form_parts().size() << std::endl;
|
||||||
|
|
||||||
for (auto& file : request.http->form_parts()) {
|
for (auto& part : request.http->form_parts()) {
|
||||||
std::cout << "name: " << file.name() << std::endl;
|
std::cout << "name: " << part.name() << std::endl;
|
||||||
std::cout << "data: " << std::endl << file.data() << std::endl;
|
std::cout << "data: " << std::endl << part.data() << std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
response->content = "OK";
|
response->content = "OK";
|
||||||
|
|||||||
+40
-27
@@ -15,6 +15,8 @@ namespace webcc {
|
|||||||
|
|
||||||
namespace misc_strings {
|
namespace misc_strings {
|
||||||
|
|
||||||
|
// Literal strings can't be used because they have an extra '\0'.
|
||||||
|
|
||||||
const char HEADER_SEPARATOR[] = { ':', ' ' };
|
const char HEADER_SEPARATOR[] = { ':', ' ' };
|
||||||
const char CRLF[] = { '\r', '\n' };
|
const char CRLF[] = { '\r', '\n' };
|
||||||
|
|
||||||
@@ -210,8 +212,8 @@ bool ContentDisposition::Init(const std::string& str) {
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
FormPart::FormPart(const std::string& name, const Path& path,
|
FormPart::FormPart(const std::string& name, const Path& path,
|
||||||
const std::string& mime_type)
|
const std::string& media_type)
|
||||||
: name_(name), mime_type_(mime_type) {
|
: name_(name), media_type_(media_type) {
|
||||||
if (!ReadFile(path, &data_)) {
|
if (!ReadFile(path, &data_)) {
|
||||||
throw Exception(kFileIOError, "Cannot read the file.");
|
throw Exception(kFileIOError, "Cannot read the file.");
|
||||||
}
|
}
|
||||||
@@ -220,50 +222,61 @@ FormPart::FormPart(const std::string& name, const Path& path,
|
|||||||
// TODO: encoding
|
// TODO: encoding
|
||||||
file_name_ = path.filename().string(std::codecvt_utf8<wchar_t>());
|
file_name_ = path.filename().string(std::codecvt_utf8<wchar_t>());
|
||||||
|
|
||||||
// Determine content type from file extension.
|
// Determine media type from file extension.
|
||||||
if (mime_type_.empty()) {
|
if (media_type_.empty()) {
|
||||||
std::string extension = path.extension().string();
|
std::string extension = path.extension().string();
|
||||||
mime_type_ = http::media_types::FromExtension(extension, false);
|
media_type_ = http::media_types::FromExtension(extension, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FormPart::FormPart(const std::string& name, std::string&& data,
|
FormPart::FormPart(const std::string& name, std::string&& data,
|
||||||
const std::string& mime_type)
|
const std::string& media_type)
|
||||||
: name_(name), data_(std::move(data)), mime_type_(mime_type) {
|
: name_(name), data_(std::move(data)), media_type_(media_type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void FormPart::Prepare(std::vector<boost::asio::const_buffer>& payload) {
|
void FormPart::Prepare(Payload* payload) {
|
||||||
|
// The payload buffers don't own the memory.
|
||||||
|
// It depends on some existing variables/objects to keep the memory.
|
||||||
|
// That's why we need |headers_|.
|
||||||
if (headers_.empty()) {
|
if (headers_.empty()) {
|
||||||
std::string value = "form-data";
|
SetHeaders();
|
||||||
if (!name_.empty()) {
|
|
||||||
value.append("; name=\"" + name_ + "\"");
|
|
||||||
}
|
|
||||||
if (!file_name_.empty()) {
|
|
||||||
value.append("; filename=\"" + file_name_ + "\"");
|
|
||||||
}
|
|
||||||
headers_.Set(http::headers::kContentDisposition, value);
|
|
||||||
|
|
||||||
if (!mime_type_.empty()) {
|
|
||||||
headers_.Set(http::headers::kContentType, mime_type_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using boost::asio::buffer;
|
using boost::asio::buffer;
|
||||||
|
|
||||||
for (const HttpHeader& h : headers_.data()) {
|
for (const HttpHeader& h : headers_.data()) {
|
||||||
payload.push_back(buffer(h.first));
|
payload->push_back(buffer(h.first));
|
||||||
payload.push_back(buffer(misc_strings::HEADER_SEPARATOR));
|
payload->push_back(buffer(misc_strings::HEADER_SEPARATOR));
|
||||||
payload.push_back(buffer(h.second));
|
payload->push_back(buffer(h.second));
|
||||||
payload.push_back(buffer(misc_strings::CRLF));
|
payload->push_back(buffer(misc_strings::CRLF));
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.push_back(buffer(misc_strings::CRLF));
|
payload->push_back(buffer(misc_strings::CRLF));
|
||||||
|
|
||||||
if (!data_.empty()) {
|
if (!data_.empty()) {
|
||||||
payload.push_back(buffer(data_));
|
payload->push_back(buffer(data_));
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.push_back(buffer(misc_strings::CRLF));
|
payload->push_back(buffer(misc_strings::CRLF));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FormPart::SetHeaders() {
|
||||||
|
// Header: Content-Disposition
|
||||||
|
|
||||||
|
std::string content_disposition = "form-data";
|
||||||
|
if (!name_.empty()) {
|
||||||
|
content_disposition.append("; name=\"" + name_ + "\"");
|
||||||
|
}
|
||||||
|
if (!file_name_.empty()) {
|
||||||
|
content_disposition.append("; filename=\"" + file_name_ + "\"");
|
||||||
|
}
|
||||||
|
headers_.Set(http::headers::kContentDisposition, content_disposition);
|
||||||
|
|
||||||
|
// Header: Content-Type
|
||||||
|
|
||||||
|
if (!media_type_.empty()) {
|
||||||
|
headers_.Set(http::headers::kContentType, media_type_);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace webcc
|
} // namespace webcc
|
||||||
|
|||||||
+43
-15
@@ -151,16 +151,23 @@ private:
|
|||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Form data part.
|
// A part of the multipart form data.
|
||||||
class FormPart {
|
class FormPart {
|
||||||
public:
|
public:
|
||||||
FormPart() = default;
|
FormPart() = default;
|
||||||
|
|
||||||
|
// Construct a file part.
|
||||||
|
// The file name will be extracted from path.
|
||||||
|
// The media type, if not provided, will be inferred from file extension.
|
||||||
FormPart(const std::string& name, const Path& path,
|
FormPart(const std::string& name, const Path& path,
|
||||||
const std::string& mime_type = "");
|
const std::string& media_type = "");
|
||||||
|
|
||||||
|
// Construct a non-file part.
|
||||||
|
// The data will be moved, no file name is needed.
|
||||||
|
// The media type is optional. If the data is a JSON string, you can specify
|
||||||
|
// media type as "application/json".
|
||||||
FormPart(const std::string& name, std::string&& data,
|
FormPart(const std::string& name, std::string&& data,
|
||||||
const std::string& mime_type = "");
|
const std::string& media_type = "");
|
||||||
|
|
||||||
#if WEBCC_DEFAULT_MOVE_COPY_ASSIGN
|
#if WEBCC_DEFAULT_MOVE_COPY_ASSIGN
|
||||||
|
|
||||||
@@ -172,7 +179,7 @@ public:
|
|||||||
FormPart(FormPart&& rhs)
|
FormPart(FormPart&& rhs)
|
||||||
: name_(std::move(rhs.name_)),
|
: name_(std::move(rhs.name_)),
|
||||||
file_name_(std::move(rhs.file_name_)),
|
file_name_(std::move(rhs.file_name_)),
|
||||||
mime_type_(std::move(rhs.mime_type_)),
|
media_type_(std::move(rhs.media_type_)),
|
||||||
data_(std::move(rhs.data_)) {
|
data_(std::move(rhs.data_)) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +187,7 @@ public:
|
|||||||
if (&rhs != this) {
|
if (&rhs != this) {
|
||||||
name_ = std::move(rhs.name_);
|
name_ = std::move(rhs.name_);
|
||||||
file_name_ = std::move(rhs.file_name_);
|
file_name_ = std::move(rhs.file_name_);
|
||||||
mime_type_ = std::move(rhs.mime_type_);
|
media_type_ = std::move(rhs.media_type_);
|
||||||
data_ = std::move(rhs.data_);
|
data_ = std::move(rhs.data_);
|
||||||
}
|
}
|
||||||
return *this;
|
return *this;
|
||||||
@@ -188,53 +195,74 @@ public:
|
|||||||
|
|
||||||
#endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN
|
#endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN
|
||||||
|
|
||||||
|
// API: SERVER
|
||||||
const std::string& name() const {
|
const std::string& name() const {
|
||||||
return name_;
|
return name_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: SERVER/PARSER
|
||||||
void set_name(const std::string& name) {
|
void set_name(const std::string& name) {
|
||||||
name_ = name;
|
name_ = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: SERVER
|
||||||
const std::string& file_name() const {
|
const std::string& file_name() const {
|
||||||
return file_name_;
|
return file_name_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: SERVER/PARSER
|
||||||
void set_file_name(const std::string& file_name) {
|
void set_file_name(const std::string& file_name) {
|
||||||
file_name_ = file_name;
|
file_name_ = file_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& mime_type() const {
|
// API: SERVER
|
||||||
return mime_type_;
|
const std::string& media_type() const {
|
||||||
|
return media_type_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: SERVER
|
||||||
const std::string& data() const {
|
const std::string& data() const {
|
||||||
return data_;
|
return data_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API: SERVER/PARSER
|
||||||
void AppendData(const std::string& data) {
|
void AppendData(const std::string& data) {
|
||||||
data_.append(data);
|
data_.append(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppendData(const char* data, std::size_t size) {
|
// API: SERVER/PARSER
|
||||||
data_.append(data, size);
|
void AppendData(const char* data, std::size_t count) {
|
||||||
|
data_.append(data, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Prepare(Payload& payload);
|
// API: CLIENT
|
||||||
|
void Prepare(Payload* payload);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Generate headers from properties.
|
||||||
|
void SetHeaders();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// The <input> name within the original HTML form.
|
||||||
|
// E.g., given HTML form:
|
||||||
|
// <input name="file1" type="file">
|
||||||
|
// the name will be "file1".
|
||||||
std::string name_;
|
std::string name_;
|
||||||
|
|
||||||
// E.g., example.jpg
|
// The original local file name.
|
||||||
// TODO: Unicode
|
// E.g., "baby.jpg".
|
||||||
std::string file_name_;
|
std::string file_name_;
|
||||||
|
|
||||||
// E.g., image/jpeg
|
// The content-type if the media type is known (e.g., inferred from the file
|
||||||
std::string mime_type_;
|
// extension or operating system typing information) or as
|
||||||
|
// application/octet-stream.
|
||||||
|
// E.g., "image/jpeg".
|
||||||
|
std::string media_type_;
|
||||||
|
|
||||||
|
// Headers generated from the above properties.
|
||||||
|
// Only Used to prepare payload.
|
||||||
HttpHeaders headers_;
|
HttpHeaders headers_;
|
||||||
|
|
||||||
// Binary file data.
|
|
||||||
std::string data_;
|
std::string data_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace webcc {
|
|||||||
|
|
||||||
namespace misc_strings {
|
namespace misc_strings {
|
||||||
|
|
||||||
|
// Literal strings can't be used because they have an extra '\0'.
|
||||||
|
|
||||||
const char HEADER_SEPARATOR[] = { ':', ' ' };
|
const char HEADER_SEPARATOR[] = { ':', ' ' };
|
||||||
const char CRLF[] = { '\r', '\n' };
|
const char CRLF[] = { '\r', '\n' };
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace webcc {
|
|||||||
|
|
||||||
namespace misc_strings {
|
namespace misc_strings {
|
||||||
|
|
||||||
|
// Literal strings can't be used because they have an extra '\0'.
|
||||||
|
|
||||||
const char CRLF[] = { '\r', '\n' };
|
const char CRLF[] = { '\r', '\n' };
|
||||||
const char DOUBLE_DASHES[] = { '-', '-' };
|
const char DOUBLE_DASHES[] = { '-', '-' };
|
||||||
|
|
||||||
@@ -27,10 +29,12 @@ void HttpRequest::Prepare() {
|
|||||||
|
|
||||||
if (form_parts_.empty()) {
|
if (form_parts_.empty()) {
|
||||||
HttpMessage::Prepare();
|
HttpMessage::Prepare();
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Multipart form data.
|
// Multipart form data.
|
||||||
|
|
||||||
// Another choice to generate the boundary is what Apache does.
|
// Another choice to generate the boundary is like what Apache does.
|
||||||
// See: https://stackoverflow.com/a/5686863
|
// See: https://stackoverflow.com/a/5686863
|
||||||
if (boundary_.empty()) {
|
if (boundary_.empty()) {
|
||||||
boundary_ = RandomUuid();
|
boundary_ = RandomUuid();
|
||||||
@@ -48,7 +52,7 @@ void HttpRequest::Prepare() {
|
|||||||
data_payload.push_back(buffer(boundary_));
|
data_payload.push_back(buffer(boundary_));
|
||||||
data_payload.push_back(buffer(misc_strings::CRLF));
|
data_payload.push_back(buffer(misc_strings::CRLF));
|
||||||
|
|
||||||
part.Prepare(data_payload);
|
part.Prepare(&data_payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boundary end
|
// Boundary end
|
||||||
@@ -69,7 +73,6 @@ void HttpRequest::Prepare() {
|
|||||||
|
|
||||||
// Append payload of content data.
|
// Append payload of content data.
|
||||||
payload_.insert(payload_.end(), data_payload.begin(), data_payload.end());
|
payload_.insert(payload_.end(), data_payload.begin(), data_payload.end());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HttpRequest::CreateStartLine() {
|
void HttpRequest::CreateStartLine() {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#ifndef WEBCC_HTTP_REQUEST_BUILDER_H_
|
#ifndef WEBCC_HTTP_REQUEST_BUILDER_H_
|
||||||
#define WEBCC_HTTP_REQUEST_BUILDER_H_
|
#define WEBCC_HTTP_REQUEST_BUILDER_H_
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user