Post/upload files support.
This commit is contained in:
+30
-1
@@ -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);
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -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
|
||||||
|
|||||||
+9
-6
@@ -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_));
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Request-level charset.
|
// TODO: Request-level charset.
|
||||||
if (json_) {
|
if (json_) {
|
||||||
request->SetContentType(http::media_types::kApplicationJson, "");
|
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;
|
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
|
} // 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user