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.
// E.g., ExampleImage("E:\\example.jpeg")
// E.g., ExampleImage("E:\\example.jpg")
void ExampleImage(const std::string& path) {
HttpClientSession session;
@ -105,6 +105,35 @@ void ExampleImage(const std::string& path) {
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() {
WEBCC_LOG_INIT("", LOG_CONSOLE);

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

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

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

@ -179,7 +179,7 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent,
} else {
// Split by EOL to achieve more readability.
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;

@ -108,6 +108,10 @@ public:
// Return true if header Accept-Encoding contains "gzip".
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.
void SetContentType(const std::string& media_type,
const std::string& charset);

@ -1,10 +1,32 @@
#include "webcc/http_request_builder.h"
#include <fstream>
#include "webcc/logger.h"
#include "webcc/utility.h"
#include "webcc/zlib_wrapper.h"
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()() {
assert(parameters_.size() % 2 == 0);
assert(headers_.size() % 2 == 0);
@ -25,26 +47,90 @@ HttpRequestPtr HttpRequestBuilder::operator()() {
}
if (!data_.empty()) {
if (gzip_ && data_.size() > kGzipThreshold) {
std::string compressed;
if (Compress(data_, &compressed)) {
request->SetContent(std::move(compressed), true);
request->SetHeader(http::headers::kContentEncoding, "gzip");
} else {
LOG_WARN("Cannot compress the content data!");
request->SetContent(std::move(data_), true);
}
} else {
request->SetContent(std::move(data_), true);
}
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;
if (Compress(data, &compressed)) {
request->SetContent(std::move(compressed), true);
request->SetHeader(http::headers::kContentEncoding, "gzip");
return;
}
LOG_WARN("Cannot compress the content data!");
}
request->SetContent(std::move(data), true);
}
void HttpRequestBuilder::CreateFormData(std::string* data,
const std::string& boundary) {
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);
// 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

@ -51,12 +51,27 @@ public:
return *this;
}
HttpRequestBuilder& json(bool json) {
HttpRequestBuilder& json(bool json = true) {
json_ = json;
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;
return *this;
}
@ -73,6 +88,11 @@ public:
return *this;
}
private:
void SetContent(HttpRequestPtr request, std::string&& data);
void CreateFormData(std::string* data, const std::string& boundary);
private:
std::string method_;
@ -87,6 +107,19 @@ private:
// Is the data to send a JSON string?
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.
// NOTE: Most servers don't support compressed requests.
// Even the requests module from Python doesn't have a built-in support.

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

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

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

Loading…
Cancel
Save