diff --git a/examples/file_upload_client.cc b/examples/file_upload_client.cc index 8a570cb..bca5212 100644 --- a/examples/file_upload_client.cc +++ b/examples/file_upload_client.cc @@ -55,7 +55,7 @@ int main(int argc, char* argv[]) { auto r = session.Request(webcc::HttpRequestBuilder{}.Post(). Url(url). File("file", upload_dir / "remember.txt"). - Form("text", "text default") + Form("json", "{}", "application/json") ()); //std::cout << r->content() << std::endl; diff --git a/examples/file_upload_server.cc b/examples/file_upload_server.cc index 5364684..525d7bf 100644 --- a/examples/file_upload_server.cc +++ b/examples/file_upload_server.cc @@ -11,12 +11,12 @@ class FileUploadService : public webcc::RestService { public: void Handle(const webcc::RestRequest& request, 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; - for (auto& file : request.http->form_parts()) { - std::cout << "name: " << file.name() << std::endl; - std::cout << "data: " << std::endl << file.data() << std::endl; + for (auto& part : request.http->form_parts()) { + std::cout << "name: " << part.name() << std::endl; + std::cout << "data: " << std::endl << part.data() << std::endl; } response->content = "OK"; diff --git a/webcc/common.cc b/webcc/common.cc index e401676..7b8df2d 100644 --- a/webcc/common.cc +++ b/webcc/common.cc @@ -15,6 +15,8 @@ namespace webcc { namespace misc_strings { +// Literal strings can't be used because they have an extra '\0'. + const char HEADER_SEPARATOR[] = { ':', ' ' }; 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, - const std::string& mime_type) - : name_(name), mime_type_(mime_type) { + const std::string& media_type) + : name_(name), media_type_(media_type) { if (!ReadFile(path, &data_)) { throw Exception(kFileIOError, "Cannot read the file."); } @@ -220,50 +222,61 @@ FormPart::FormPart(const std::string& name, const Path& path, // TODO: encoding file_name_ = path.filename().string(std::codecvt_utf8()); - // Determine content type from file extension. - if (mime_type_.empty()) { + // Determine media type from file extension. + if (media_type_.empty()) { 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, - const std::string& mime_type) - : name_(name), data_(std::move(data)), mime_type_(mime_type) { + const std::string& media_type) + : name_(name), data_(std::move(data)), media_type_(media_type) { } -void FormPart::Prepare(std::vector& 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()) { - std::string value = "form-data"; - 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_); - } + SetHeaders(); } using boost::asio::buffer; for (const HttpHeader& h : headers_.data()) { - payload.push_back(buffer(h.first)); - payload.push_back(buffer(misc_strings::HEADER_SEPARATOR)); - payload.push_back(buffer(h.second)); - payload.push_back(buffer(misc_strings::CRLF)); + payload->push_back(buffer(h.first)); + payload->push_back(buffer(misc_strings::HEADER_SEPARATOR)); + 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)); if (!data_.empty()) { - payload.push_back(buffer(data_)); + payload->push_back(buffer(data_)); + } + + 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 - payload.push_back(buffer(misc_strings::CRLF)); + if (!media_type_.empty()) { + headers_.Set(http::headers::kContentType, media_type_); + } } } // namespace webcc diff --git a/webcc/common.h b/webcc/common.h index d232b71..411d374 100644 --- a/webcc/common.h +++ b/webcc/common.h @@ -151,16 +151,23 @@ private: // ----------------------------------------------------------------------------- -// Form data part. +// A part of the multipart form data. class FormPart { public: 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, - 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, - const std::string& mime_type = ""); + const std::string& media_type = ""); #if WEBCC_DEFAULT_MOVE_COPY_ASSIGN @@ -172,7 +179,7 @@ public: FormPart(FormPart&& rhs) : name_(std::move(rhs.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_)) { } @@ -180,7 +187,7 @@ public: if (&rhs != this) { name_ = std::move(rhs.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_); } return *this; @@ -188,53 +195,74 @@ public: #endif // WEBCC_DEFAULT_MOVE_COPY_ASSIGN + // API: SERVER const std::string& name() const { return name_; } + // API: SERVER/PARSER void set_name(const std::string& name) { name_ = name; } + // API: SERVER const std::string& file_name() const { return file_name_; } + // API: SERVER/PARSER void set_file_name(const std::string& file_name) { file_name_ = file_name; } - const std::string& mime_type() const { - return mime_type_; + // API: SERVER + const std::string& media_type() const { + return media_type_; } + // API: SERVER const std::string& data() const { return data_; } + // API: SERVER/PARSER void AppendData(const std::string& data) { data_.append(data); } - void AppendData(const char* data, std::size_t size) { - data_.append(data, size); + // API: SERVER/PARSER + void AppendData(const char* data, std::size_t count) { + data_.append(data, count); } - void Prepare(Payload& payload); + // API: CLIENT + void Prepare(Payload* payload); private: + // Generate headers from properties. + void SetHeaders(); + +private: + // The name within the original HTML form. + // E.g., given HTML form: + // + // the name will be "file1". std::string name_; - // E.g., example.jpg - // TODO: Unicode + // The original local file name. + // E.g., "baby.jpg". std::string file_name_; - // E.g., image/jpeg - std::string mime_type_; + // The content-type if the media type is known (e.g., inferred from the file + // 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_; - // Binary file data. std::string data_; }; diff --git a/webcc/http_message.cc b/webcc/http_message.cc index 1c8deab..2071055 100644 --- a/webcc/http_message.cc +++ b/webcc/http_message.cc @@ -13,6 +13,8 @@ namespace webcc { namespace misc_strings { +// Literal strings can't be used because they have an extra '\0'. + const char HEADER_SEPARATOR[] = { ':', ' ' }; const char CRLF[] = { '\r', '\n' }; diff --git a/webcc/http_request.cc b/webcc/http_request.cc index 491c0e1..29f050e 100644 --- a/webcc/http_request.cc +++ b/webcc/http_request.cc @@ -9,6 +9,8 @@ namespace webcc { namespace misc_strings { +// Literal strings can't be used because they have an extra '\0'. + const char CRLF[] = { '\r', '\n' }; const char DOUBLE_DASHES[] = { '-', '-' }; @@ -27,49 +29,50 @@ void HttpRequest::Prepare() { if (form_parts_.empty()) { HttpMessage::Prepare(); - } else { - // Multipart form data. - - // Another choice to generate the boundary is what Apache does. - // See: https://stackoverflow.com/a/5686863 - if (boundary_.empty()) { - boundary_ = RandomUuid(); - } + return; + } - SetContentType("multipart/form-data; boundary=" + boundary_); + // Multipart form data. - Payload data_payload; + // Another choice to generate the boundary is like what Apache does. + // See: https://stackoverflow.com/a/5686863 + if (boundary_.empty()) { + boundary_ = RandomUuid(); + } - using boost::asio::buffer; + SetContentType("multipart/form-data; boundary=" + boundary_); - for (auto& part : form_parts_) { - // Boundary - data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); - data_payload.push_back(buffer(boundary_)); - data_payload.push_back(buffer(misc_strings::CRLF)); + Payload data_payload; - part.Prepare(data_payload); - } + using boost::asio::buffer; - // Boundary end + for (auto& part : form_parts_) { + // Boundary data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); data_payload.push_back(buffer(boundary_)); - data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); data_payload.push_back(buffer(misc_strings::CRLF)); - // Update Content-Length header. - std::size_t content_length = 0; - for (auto& buffer : data_payload) { - content_length += buffer.size(); - } - SetContentLength(content_length); + part.Prepare(&data_payload); + } - // Prepare start line and headers. - HttpMessage::Prepare(); + // Boundary end + data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + data_payload.push_back(buffer(boundary_)); + data_payload.push_back(buffer(misc_strings::DOUBLE_DASHES)); + data_payload.push_back(buffer(misc_strings::CRLF)); - // Append payload of content data. - payload_.insert(payload_.end(), data_payload.begin(), data_payload.end()); + // Update Content-Length header. + std::size_t content_length = 0; + for (auto& buffer : data_payload) { + content_length += buffer.size(); } + SetContentLength(content_length); + + // Prepare start line and headers. + HttpMessage::Prepare(); + + // Append payload of content data. + payload_.insert(payload_.end(), data_payload.begin(), data_payload.end()); } void HttpRequest::CreateStartLine() { diff --git a/webcc/http_request_builder.h b/webcc/http_request_builder.h index fcf9bd6..4702c70 100644 --- a/webcc/http_request_builder.h +++ b/webcc/http_request_builder.h @@ -1,7 +1,6 @@ #ifndef WEBCC_HTTP_REQUEST_BUILDER_H_ #define WEBCC_HTTP_REQUEST_BUILDER_H_ -#include #include #include