Post/upload files support.

master
Chunting Gu 6 years ago
parent 18db152419
commit a8e86dec5e

@ -91,7 +91,7 @@ void ExampleCompression() {
} }
// Get an image from HttpBin.org and save to the given file path. // Get an image from HttpBin.org and save to the given file path.
// E.g., ExampleImage("E:\\example.jpeg") // E.g., ExampleImage("E:\\example.jpg")
void ExampleImage(const std::string& path) { void ExampleImage(const std::string& path) {
HttpClientSession session; HttpClientSession session;
@ -105,6 +105,35 @@ void ExampleImage(const std::string& path) {
ofs << r->content(); ofs << r->content();
} }
// Post/upload files.
void ExamplePostFiles() {
HttpClientSession session;
auto r = session.Request(HttpRequestBuilder{}
.Post()
.url("http://httpbin.org/post")
.file_data("file1", "report.xls", "<xls report data>", "application/vnd.ms-excel")
.file_data("file2", "report.xml", "<xml report data>", "text/xml")());
std::cout << r->content() << std::endl;
}
// Post/upload files by file path.
void ExamplePostFiles(const std::string& name,
const std::string& file_name,
const std::string& file_path,
const std::string& content_type) {
HttpClientSession session;
auto r =
session.Request(HttpRequestBuilder{}
.Post()
.url("http://httpbin.org/post")
.file(name, file_name, file_path, content_type)());
std::cout << r->content() << std::endl;
}
int main() { int main() {
WEBCC_LOG_INIT("", LOG_CONSOLE); WEBCC_LOG_INIT("", LOG_CONSOLE);

@ -31,6 +31,8 @@ const char* DescribeError(Error error) {
return "HTTP error"; return "HTTP error";
case kServerError: case kServerError:
return "Server error"; return "Server error";
case kFileIOError:
return "File IO error";
case kXmlError: case kXmlError:
return "XML error"; return "XML error";
default: default:
@ -38,14 +40,14 @@ const char* DescribeError(Error error) {
} }
} }
Exception::Exception(Error error, bool timeout, const std::string& details) Exception::Exception(Error error, const std::string& details, bool timeout)
: error_(error), timeout_(timeout), msg_(DescribeError(error)) { : error_(error), msg_(DescribeError(error)), timeout_(timeout) {
if (timeout) {
msg_ += " (timeout)";
}
if (!details.empty()) { if (!details.empty()) {
msg_ += " (" + details + ")"; msg_ += " (" + details + ")";
} }
if (timeout) {
msg_ += " (timeout)";
}
} }
} // namespace webcc } // namespace webcc

@ -145,6 +145,9 @@ enum Error {
// E.g., HTTP status 500 + SOAP Fault element. // E.g., HTTP status 500 + SOAP Fault element.
kServerError, kServerError,
// File read/write error.
kFileIOError,
// XML parsing error. // XML parsing error.
kXmlError, kXmlError,
}; };
@ -154,25 +157,25 @@ const char* DescribeError(Error error);
class Exception : public std::exception { class Exception : public std::exception {
public: public:
explicit Exception(Error error = kNoError, bool timeout = false, explicit Exception(Error error, const std::string& details = "",
const std::string& details = ""); bool timeout = false);
Error error() const { return error_; }
// Note that `noexcept` is required by GCC. // Note that `noexcept` is required by GCC.
const char* what() const noexcept override { const char* what() const noexcept override {
return msg_.c_str(); return msg_.c_str();
} }
Error error() const { return error_; }
bool timeout() const { return timeout_; } bool timeout() const { return timeout_; }
private: private:
Error error_; Error error_;
std::string msg_;
// If the error was caused by timeout or not. // If the error was caused by timeout or not.
bool timeout_; bool timeout_;
std::string msg_;
}; };
} // namespace webcc } // namespace webcc

@ -172,7 +172,7 @@ HttpResponsePtr HttpClientSession::Send(HttpRequestPtr request) {
// Remove the failed connection from pool. // Remove the failed connection from pool.
pool_.Remove(key); pool_.Remove(key);
} }
throw Exception(client->error(), client->timed_out()); throw Exception(client->error(), "", client->timed_out());
} }
// Update connection pool. // Update connection pool.

@ -179,7 +179,7 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent,
} else { } else {
// Split by EOL to achieve more readability. // Split by EOL to achieve more readability.
std::vector<std::string> splitted; std::vector<std::string> splitted;
boost::split(splitted, content_, boost::is_any_of(kCRLF)); boost::split(splitted, content_, boost::is_any_of("\n"));
std::size_t size = 0; std::size_t size = 0;

@ -108,6 +108,10 @@ public:
// Return true if header Accept-Encoding contains "gzip". // Return true if header Accept-Encoding contains "gzip".
bool AcceptEncodingGzip() const; bool AcceptEncodingGzip() const;
void SetContentType(const std::string& content_type) {
SetHeader(http::headers::kContentType, content_type);
}
// E.g., "text/html", "application/json; charset=utf-8", etc. // E.g., "text/html", "application/json; charset=utf-8", etc.
void SetContentType(const std::string& media_type, void SetContentType(const std::string& media_type,
const std::string& charset); const std::string& charset);

@ -1,10 +1,32 @@
#include "webcc/http_request_builder.h" #include "webcc/http_request_builder.h"
#include <fstream>
#include "webcc/logger.h" #include "webcc/logger.h"
#include "webcc/utility.h"
#include "webcc/zlib_wrapper.h" #include "webcc/zlib_wrapper.h"
namespace webcc { namespace webcc {
// -----------------------------------------------------------------------------
// Read entire file into string.
static bool ReadFile(const std::string& path, std::string* output) {
std::ifstream ifs{path, std::ios::binary | std::ios::ate};
if (!ifs) {
return false;
}
auto size = ifs.tellg();
output->resize(size, '\0');
ifs.seekg(0);
ifs.read(&(*output)[0], size); // TODO: Error handling
return true;
}
// -----------------------------------------------------------------------------
HttpRequestPtr HttpRequestBuilder::operator()() { HttpRequestPtr HttpRequestBuilder::operator()() {
assert(parameters_.size() % 2 == 0); assert(parameters_.size() % 2 == 0);
assert(headers_.size() % 2 == 0); assert(headers_.size() % 2 == 0);
@ -25,26 +47,90 @@ HttpRequestPtr HttpRequestBuilder::operator()() {
} }
if (!data_.empty()) { if (!data_.empty()) {
if (gzip_ && data_.size() > kGzipThreshold) { SetContent(request, std::move(data_));
// TODO: Request-level charset.
if (json_) {
request->SetContentType(http::media_types::kApplicationJson, "");
}
} else if (!files_.empty()) {
// Another choice to generate the boundary is what Apache does, see:
// https://stackoverflow.com/a/5686863
const std::string boundary = RandomUuid();
request->SetContentType("multipart/form-data; boundary=" + boundary);
std::string data;
CreateFormData(&data, boundary);
// Ingore gzip since most servers don't support it.
request->SetContent(std::move(data), true);
}
return request;
}
HttpRequestBuilder& HttpRequestBuilder::file(const std::string& name,
const std::string& file_name,
const std::string& file_path,
const std::string& content_type) {
std::string file_data;
if (!ReadFile(file_path, &file_data)) {
throw Exception(kFileIOError, "Cannot read the file.");
}
files_.push_back({name, file_name, std::move(file_data), content_type});
return *this;
}
void HttpRequestBuilder::SetContent(HttpRequestPtr request,
std::string&& data) {
if (gzip_ && data.size() > kGzipThreshold) {
std::string compressed; std::string compressed;
if (Compress(data_, &compressed)) { if (Compress(data, &compressed)) {
request->SetContent(std::move(compressed), true); request->SetContent(std::move(compressed), true);
request->SetHeader(http::headers::kContentEncoding, "gzip"); request->SetHeader(http::headers::kContentEncoding, "gzip");
} else { return;
}
LOG_WARN("Cannot compress the content data!"); LOG_WARN("Cannot compress the content data!");
request->SetContent(std::move(data_), true);
} }
} else {
request->SetContent(std::move(data_), true); request->SetContent(std::move(data), true);
} }
// TODO: Request-level charset. void HttpRequestBuilder::CreateFormData(std::string* data,
if (json_) { const std::string& boundary) {
request->SetContentType(http::media_types::kApplicationJson, ""); for (File& file : files_) {
data->append("--" + boundary + kCRLF);
// Content-Disposition header
data->append("Content-Disposition: form-data");
if (!file.name.empty()) {
data->append("; name=\"" + file.name + "\"");
} }
if (!file.file_name.empty()) {
data->append("; filename=\"" + file.file_name + "\"");
} }
data->append(kCRLF);
return request; // Content-Type header
if (!file.content_type.empty()) {
data->append("Content-Type: " + file.content_type);
data->append(kCRLF);
}
data->append(kCRLF);
// Payload
data->append(file.file_data);
data->append(kCRLF);
}
data->append("--" + boundary + "--");
data->append(kCRLF);
} }
} // namespace webcc } // namespace webcc

@ -51,12 +51,27 @@ public:
return *this; return *this;
} }
HttpRequestBuilder& json(bool json) { HttpRequestBuilder& json(bool json = true) {
json_ = json; json_ = json;
return *this; return *this;
} }
HttpRequestBuilder& gzip(bool gzip) { // Upload a file with its path.
HttpRequestBuilder& file(const std::string& name,
const std::string& file_name,
const std::string& file_path, // TODO: UNICODE
const std::string& content_type = "");
// Upload a file with its data.
HttpRequestBuilder& file_data(const std::string& name,
const std::string& file_name,
std::string&& file_data,
const std::string& content_type = "") {
files_.push_back({name, file_name, file_data, content_type});
return *this;
}
HttpRequestBuilder& gzip(bool gzip = true) {
gzip_ = gzip; gzip_ = gzip;
return *this; return *this;
} }
@ -73,6 +88,11 @@ public:
return *this; return *this;
} }
private:
void SetContent(HttpRequestPtr request, std::string&& data);
void CreateFormData(std::string* data, const std::string& boundary);
private: private:
std::string method_; std::string method_;
@ -87,6 +107,19 @@ private:
// Is the data to send a JSON string? // Is the data to send a JSON string?
bool json_ = false; bool json_ = false;
// Examples:
// { "images", "example.jpg", "BinaryData", "image/jpeg" }
// { "file", "report.csv", "BinaryData", "" }
struct File {
std::string name;
std::string file_name;
std::string file_data; // Binary file data
std::string content_type;
};
// Files to upload for a POST (or PUT?) request.
std::vector<File> files_;
// Compress the request content. // Compress the request content.
// NOTE: Most servers don't support compressed requests. // NOTE: Most servers don't support compressed requests.
// Even the requests module from Python doesn't have a built-in support. // Even the requests module from Python doesn't have a built-in support.

@ -6,6 +6,9 @@
#include <ostream> #include <ostream>
#include <sstream> #include <sstream>
#include "boost/uuid/random_generator.hpp"
#include "boost/uuid/uuid_io.hpp"
#include "webcc/logger.h" #include "webcc/logger.h"
using tcp = boost::asio::ip::tcp; using tcp = boost::asio::ip::tcp;
@ -44,4 +47,11 @@ std::string GetHttpDateTimestamp() {
return ss.str(); return ss.str();
} }
std::string RandomUuid() {
boost::uuids::uuid u = boost::uuids::random_generator()();
std::stringstream ss;
ss << u;
return ss.str();
}
} // namespace webcc } // namespace webcc

@ -22,6 +22,8 @@ std::string EndpointToString(const TcpEndpoint& endpoint);
// See: https://tools.ietf.org/html/rfc7231#section-7.1.1.2 // See: https://tools.ietf.org/html/rfc7231#section-7.1.1.2
std::string GetHttpDateTimestamp(); std::string GetHttpDateTimestamp();
std::string RandomUuid();
} // namespace webcc } // namespace webcc
#endif // WEBCC_UTILITY_H_ #endif // WEBCC_UTILITY_H_

@ -39,9 +39,7 @@ bool Compress(const std::string& input, std::string* output) {
int err = deflate(&stream, Z_FINISH); int err = deflate(&stream, Z_FINISH);
assert(err != Z_STREAM_ERROR); if (err != Z_OK && err != Z_STREAM_END) {
if (err != Z_OK) {
deflateEnd(&stream); deflateEnd(&stream);
if (stream.msg != nullptr) { if (stream.msg != nullptr) {
LOG_ERRO("zlib deflate error: %s", stream.msg); LOG_ERRO("zlib deflate error: %s", stream.msg);
@ -51,6 +49,7 @@ bool Compress(const std::string& input, std::string* output) {
std::size_t size = buf.size() - stream.avail_out; std::size_t size = buf.size() - stream.avail_out;
output->insert(output->end(), buf.data(), buf.data() + size); output->insert(output->end(), buf.data(), buf.data() + size);
} while (stream.avail_out == 0); } while (stream.avail_out == 0);
if (deflateEnd(&stream) != Z_OK) { if (deflateEnd(&stream) != Z_OK) {
@ -105,7 +104,8 @@ bool Decompress(const std::string& input, std::string* output) {
if (err == Z_STREAM_END) { if (err == Z_STREAM_END) {
break; break;
} else if (err != Z_OK) { }
if (err != Z_OK) {
inflateEnd(&stream); inflateEnd(&stream);
if (stream.msg != nullptr) { if (stream.msg != nullptr) {
LOG_ERRO("zlib inflate error: %s", stream.msg); LOG_ERRO("zlib inflate error: %s", stream.msg);

Loading…
Cancel
Save