Refine the support for uploading files.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
|
||||
**upload**: files for testing upload.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -0,0 +1,17 @@
|
||||
Remember
|
||||
BY CHRISTINA ROSSETTI
|
||||
|
||||
Remember me when I am gone away,
|
||||
Gone far away into the silent land;
|
||||
When you can no more hold me by the hand,
|
||||
Nor I half turn to go yet turning stay.
|
||||
Remember me when no more day by day
|
||||
You tell me of our future that you plann'd:
|
||||
Only remember me; you understand
|
||||
It will be late to counsel then or pray.
|
||||
Yet if you should forget me for a while
|
||||
And afterwards remember, do not grieve:
|
||||
For if the darkness and corruption leave
|
||||
A vestige of the thoughts that once I had,
|
||||
Better by far you should forget and smile
|
||||
Than that you should remember and be sad.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
+13
-16
@@ -4,8 +4,6 @@
|
||||
#include "webcc/http_client_session.h"
|
||||
#include "webcc/logger.h"
|
||||
|
||||
using namespace webcc;
|
||||
|
||||
#if (defined(WIN32) || defined(_WIN64))
|
||||
// You need to set environment variable SSL_CERT_FILE properly to enable
|
||||
// SSL verification.
|
||||
@@ -65,7 +63,7 @@ void ExampleHttps() {
|
||||
// ExampleKeepAlive("https://api.github.com/events");
|
||||
//
|
||||
void ExampleKeepAlive(const std::string& url) {
|
||||
HttpClientSession session;
|
||||
webcc::HttpClientSession session;
|
||||
session.set_ssl_verify(kSslVerify);
|
||||
|
||||
// Keep-Alive
|
||||
@@ -79,7 +77,7 @@ void ExampleKeepAlive(const std::string& url) {
|
||||
}
|
||||
|
||||
void ExampleCompression() {
|
||||
HttpClientSession session;
|
||||
webcc::HttpClientSession session;
|
||||
|
||||
auto r = session.Get("http://httpbin.org/gzip");
|
||||
|
||||
@@ -93,7 +91,7 @@ void ExampleCompression() {
|
||||
// Get an image from HttpBin.org and save to the given file path.
|
||||
// E.g., ExampleImage("E:\\example.jpg")
|
||||
void ExampleImage(const std::string& path) {
|
||||
HttpClientSession session;
|
||||
webcc::HttpClientSession session;
|
||||
|
||||
auto r = session.Get("http://httpbin.org/image/jpeg");
|
||||
|
||||
@@ -107,9 +105,9 @@ void ExampleImage(const std::string& path) {
|
||||
|
||||
// Post/upload files.
|
||||
void ExamplePostFiles() {
|
||||
HttpClientSession session;
|
||||
webcc::HttpClientSession session;
|
||||
|
||||
auto r = session.Request(HttpRequestBuilder{}
|
||||
auto r = session.Request(webcc::HttpRequestBuilder{}
|
||||
.Post()
|
||||
.Url("http://httpbin.org/post")
|
||||
.FileData("file1", "report.xls", "<xls report data>", "application/vnd.ms-excel")
|
||||
@@ -119,29 +117,28 @@ void ExamplePostFiles() {
|
||||
}
|
||||
|
||||
// Post/upload files by file path.
|
||||
void ExamplePostFiles(const std::string& name,
|
||||
void ExamplePostFiles(const std::string& url,
|
||||
const std::string& name,
|
||||
const std::string& file_name,
|
||||
const std::string& file_path,
|
||||
const std::string& content_type) {
|
||||
HttpClientSession session;
|
||||
webcc::HttpClientSession session;
|
||||
|
||||
auto r =
|
||||
session.Request(HttpRequestBuilder{}
|
||||
.Post()
|
||||
.Url("http://httpbin.org/post")
|
||||
.File(name, file_name, file_path, content_type)());
|
||||
auto r = session.Request(webcc::HttpRequestBuilder{}.Post().
|
||||
Url(url).
|
||||
File(name, file_name, file_path, content_type)());
|
||||
|
||||
std::cout << r->content() << std::endl;
|
||||
}
|
||||
|
||||
int main() {
|
||||
WEBCC_LOG_INIT("", LOG_CONSOLE);
|
||||
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
|
||||
|
||||
try {
|
||||
|
||||
ExampleBasic();
|
||||
|
||||
} catch (const Exception& e) {
|
||||
} catch (const webcc::Exception& e) {
|
||||
std::cout << "Exception: " << e.what() << std::endl;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
#include "webcc/globals.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
|
||||
#include "boost/filesystem/path.hpp"
|
||||
|
||||
#include "webcc/version.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;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
namespace http {
|
||||
|
||||
const std::string& UserAgent() {
|
||||
@@ -11,6 +35,66 @@ const std::string& UserAgent() {
|
||||
return s_user_agent;
|
||||
}
|
||||
|
||||
File::File(const std::string& file_path) {
|
||||
if (!ReadFile(file_path, &data)) {
|
||||
throw Exception(kFileIOError, "Cannot read the file.");
|
||||
}
|
||||
|
||||
namespace bfs = boost::filesystem;
|
||||
|
||||
// Determine file name from file path.
|
||||
//if (file_name.empty()) {
|
||||
file_name = bfs::path(file_path).filename().string();
|
||||
//} else {
|
||||
// file_name = file_name;
|
||||
//}
|
||||
|
||||
// Determine content type from file extension.
|
||||
//if (mime_type.empty()) {
|
||||
std::string extension = bfs::path(file_path).extension().string();
|
||||
mime_type = http::media_types::FromExtension(extension, false);
|
||||
//} else {
|
||||
//mime_type = mime_type;
|
||||
//}
|
||||
}
|
||||
|
||||
namespace media_types {
|
||||
|
||||
// TODO: Add more.
|
||||
static void InitMap(std::map<std::string, std::string>& map) {
|
||||
map["gif"] = "image/gif";
|
||||
map["htm"] = "text/html";
|
||||
map["html"] = "text/html";
|
||||
map["jpg"] = "image/jpeg";
|
||||
map["jpeg"] = "image/jpeg";
|
||||
map["png"] = "image/png";
|
||||
map["txt"] = "text/plain";
|
||||
map[""] = "";
|
||||
}
|
||||
|
||||
// TODO: Ignore case on Windows.
|
||||
std::string FromExtension(const std::string& extension,
|
||||
bool default_to_plain_text) {
|
||||
static std::map<std::string, std::string> s_map;
|
||||
|
||||
if (s_map.empty()) {
|
||||
InitMap(s_map);
|
||||
}
|
||||
|
||||
auto it = s_map.find(extension);
|
||||
if (it != s_map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
if (default_to_plain_text) {
|
||||
return "text/plain";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace media_types
|
||||
|
||||
} // namespace http
|
||||
|
||||
const char* DescribeError(Error error) {
|
||||
|
||||
+35
-16
@@ -24,8 +24,9 @@ const std::size_t kMaxDumpSize = 2048;
|
||||
// Default buffer size for socket reading.
|
||||
const std::size_t kBufferSize = 1024;
|
||||
|
||||
// Default ports.
|
||||
// Default port for HTTP.
|
||||
const char* const kPort80 = "80";
|
||||
// Default port for HTTPS.
|
||||
const char* const kPort443 = "443";
|
||||
|
||||
// Why 1400? See the following page:
|
||||
@@ -36,7 +37,6 @@ const std::size_t kGzipThreshold = 1400;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// HTTP headers.
|
||||
namespace http {
|
||||
|
||||
namespace methods {
|
||||
@@ -44,15 +44,15 @@ namespace methods {
|
||||
// HTTP methods (verbs) in string.
|
||||
// Don't use enum to avoid converting back and forth.
|
||||
|
||||
const char* const kGet = "GET";
|
||||
const char* const kHead = "HEAD";
|
||||
const char* const kPost = "POST";
|
||||
const char* const kPut = "PUT";
|
||||
const char* const kDelete = "DELETE";
|
||||
const char* const kConnect = "CONNECT";
|
||||
const char* const kOptions = "OPTIONS";
|
||||
const char* const kTrace = "TRACE";
|
||||
const char* const kPatch = "PATCH";
|
||||
const char* const kGet = "GET";
|
||||
const char* const kHead = "HEAD";
|
||||
const char* const kPost = "POST";
|
||||
const char* const kPut = "PUT";
|
||||
const char* const kDelete = "DELETE";
|
||||
const char* const kConnect = "CONNECT";
|
||||
const char* const kOptions = "OPTIONS";
|
||||
const char* const kTrace = "TRACE";
|
||||
const char* const kPatch = "PATCH";
|
||||
|
||||
} // namespace methods
|
||||
|
||||
@@ -95,16 +95,17 @@ const char* const kServer = "Server";
|
||||
|
||||
namespace media_types {
|
||||
|
||||
// NOTE:
|
||||
// According to www.w3.org when placing SOAP messages in HTTP bodies, the HTTP
|
||||
// Content-type header must be chosen as "application/soap+xml" [RFC 3902].
|
||||
// But in practice, many web servers cannot understand it.
|
||||
// See: https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L26854
|
||||
// See the following link for the full list of media types:
|
||||
// https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
|
||||
const char* const kApplicationJson = "application/json";
|
||||
const char* const kApplicationSoapXml = "application/soap+xml";
|
||||
const char* const kTextXml = "text/xml";
|
||||
|
||||
// Get media type from file extension.
|
||||
std::string FromExtension(const std::string& extension,
|
||||
bool default_to_plain_text = true);
|
||||
|
||||
} // namespace media_types
|
||||
|
||||
namespace charsets {
|
||||
@@ -122,6 +123,24 @@ enum class ContentEncoding {
|
||||
// Return default user agent for HTTP headers.
|
||||
const std::string& UserAgent();
|
||||
|
||||
// File for HTTP transfer (upload/download).
|
||||
class File {
|
||||
public:
|
||||
File() = default;
|
||||
|
||||
File(const std::string& file_path);
|
||||
|
||||
// Binary file data.
|
||||
// TODO: don't use std::string?
|
||||
std::string data;
|
||||
|
||||
// E.g., example.jpg
|
||||
std::string file_name;
|
||||
|
||||
// E.g., image/jpeg
|
||||
std::string mime_type;
|
||||
};
|
||||
|
||||
} // namespace http
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -165,6 +165,7 @@ void HttpClient::DoReadResponse(Error* error) {
|
||||
// Stop the deadline timer once the read has started (or failed).
|
||||
CancelTimer();
|
||||
|
||||
// TODO: Is it necessary to check `length == 0`?
|
||||
if (ec || length == 0) {
|
||||
Close();
|
||||
*error = kSocketReadError;
|
||||
|
||||
@@ -67,6 +67,20 @@ HttpResponsePtr HttpClientSession::Post(
|
||||
return Request(builder());
|
||||
}
|
||||
|
||||
HttpResponsePtr HttpClientSession::PostFile(const std::string& url,
|
||||
const std::string& name,
|
||||
http::File&& file,
|
||||
const std::vector<std::string>& headers) {
|
||||
HttpRequestBuilder builder{http::methods::kPost};
|
||||
builder.Url(url);
|
||||
|
||||
SetHeaders(headers, &builder);
|
||||
|
||||
builder.File(name, std::move(file));
|
||||
|
||||
return Request(builder());
|
||||
}
|
||||
|
||||
HttpResponsePtr HttpClientSession::Put(
|
||||
const std::string& url, std::string&& data, bool json,
|
||||
const std::vector<std::string>& headers) {
|
||||
|
||||
@@ -59,6 +59,12 @@ public:
|
||||
HttpResponsePtr Post(const std::string& url, std::string&& data, bool json,
|
||||
const std::vector<std::string>& headers = {});
|
||||
|
||||
// Post a file.
|
||||
HttpResponsePtr PostFile(const std::string& url,
|
||||
const std::string& name,
|
||||
http::File&& file,
|
||||
const std::vector<std::string>& headers = {});
|
||||
|
||||
// Shortcut for PUT request.
|
||||
HttpResponsePtr Put(const std::string& url, std::string&& data, bool json,
|
||||
const std::vector<std::string>& headers = {});
|
||||
|
||||
@@ -166,7 +166,9 @@ void HttpMessage::Dump(std::ostream& os, std::size_t indent,
|
||||
|
||||
os << indent_str << std::endl;
|
||||
|
||||
// NOTE: The content will be truncated if it's too large to display.
|
||||
// NOTE:
|
||||
// - The content will be truncated if it's too large to display.
|
||||
// - Binary content will not be dumped (TODO).
|
||||
|
||||
if (!content_.empty()) {
|
||||
if (indent == 0) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "webcc/http_request_builder.h"
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "webcc/base64.h"
|
||||
#include "webcc/logger.h"
|
||||
#include "webcc/utility.h"
|
||||
@@ -9,25 +7,6 @@
|
||||
|
||||
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::Build() {
|
||||
assert(parameters_.size() % 2 == 0);
|
||||
assert(headers_.size() % 2 == 0);
|
||||
@@ -72,15 +51,33 @@ HttpRequestPtr HttpRequestBuilder::Build() {
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
const std::string& file_name,
|
||||
const std::string& mime_type) {
|
||||
assert(!name.empty());
|
||||
|
||||
files_.push_back({name, file_name, std::move(file_data), content_type});
|
||||
// TODO
|
||||
files_[name] = http::File(file_path/*, file_name, mime_type*/);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpRequestBuilder& HttpRequestBuilder::File(const std::string& name,
|
||||
http::File&& file) {
|
||||
files_[name] = std::move(file);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpRequestBuilder& HttpRequestBuilder::FileData(const std::string& name,
|
||||
std::string&& file_data,
|
||||
const std::string& file_name,
|
||||
const std::string& mime_type) {
|
||||
http::File file;
|
||||
file.data = std::move(file_data);
|
||||
file.file_name = file_name;
|
||||
file.mime_type = mime_type;
|
||||
|
||||
files_[name] = std::move(file);
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -116,29 +113,29 @@ void HttpRequestBuilder::SetContent(HttpRequestPtr request,
|
||||
|
||||
void HttpRequestBuilder::CreateFormData(std::string* data,
|
||||
const std::string& boundary) {
|
||||
for (UploadFile& file : files_) {
|
||||
for (auto& pair : files_) {
|
||||
data->append("--" + boundary + kCRLF);
|
||||
|
||||
// Content-Disposition header
|
||||
data->append("Content-Disposition: form-data");
|
||||
if (!file.name.empty()) {
|
||||
data->append("; name=\"" + file.name + "\"");
|
||||
if (!pair.first.empty()) {
|
||||
data->append("; name=\"" + pair.first + "\"");
|
||||
}
|
||||
if (!file.file_name.empty()) {
|
||||
data->append("; filename=\"" + file.file_name + "\"");
|
||||
if (!pair.second.file_name.empty()) {
|
||||
data->append("; filename=\"" + pair.second.file_name + "\"");
|
||||
}
|
||||
data->append(kCRLF);
|
||||
|
||||
// Content-Type header
|
||||
if (!file.content_type.empty()) {
|
||||
data->append("Content-Type: " + file.content_type);
|
||||
if (!pair.second.mime_type.empty()) {
|
||||
data->append("Content-Type: " + pair.second.mime_type);
|
||||
data->append(kCRLF);
|
||||
}
|
||||
|
||||
data->append(kCRLF);
|
||||
|
||||
// Payload
|
||||
data->append(file.file_data);
|
||||
data->append(pair.second.data);
|
||||
|
||||
data->append(kCRLF);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef WEBCC_HTTP_REQUEST_BUILDER_H_
|
||||
#define WEBCC_HTTP_REQUEST_BUILDER_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -67,18 +68,17 @@ public:
|
||||
// Upload a file with its path.
|
||||
// TODO: UNICODE file path.
|
||||
HttpRequestBuilder& File(const std::string& name,
|
||||
const std::string& file_name,
|
||||
const std::string& file_path,
|
||||
const std::string& content_type = "");
|
||||
const std::string& file_name = "",
|
||||
const std::string& mime_type = "");
|
||||
|
||||
HttpRequestBuilder& File(const std::string& name, http::File&& file);
|
||||
|
||||
// Upload a file with its data.
|
||||
HttpRequestBuilder& FileData(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;
|
||||
}
|
||||
const std::string& file_name = "",
|
||||
const std::string& mime_type = "");
|
||||
|
||||
HttpRequestBuilder& Gzip(bool gzip = true) {
|
||||
gzip_ = gzip;
|
||||
@@ -122,19 +122,8 @@ private:
|
||||
// Is the data to send a JSON string?
|
||||
bool json_ = false;
|
||||
|
||||
// A file to upload.
|
||||
// Examples:
|
||||
// { "images", "example.jpg", "BinaryData", "image/jpeg" }
|
||||
// { "file", "report.csv", "BinaryData", "" }
|
||||
struct UploadFile {
|
||||
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<UploadFile> files_;
|
||||
std::map<std::string, http::File> files_;
|
||||
|
||||
// Compress the request content.
|
||||
// NOTE: Most servers don't support compressed requests.
|
||||
|
||||
@@ -51,6 +51,12 @@ bool SoapClient::Request(const std::string& operation,
|
||||
|
||||
http_request->SetContent(std::move(http_content), true);
|
||||
|
||||
// NOTE:
|
||||
// According to www.w3.org when placing SOAP messages in HTTP bodies, the HTTP
|
||||
// Content-type header must be chosen as "application/soap+xml" [RFC 3902].
|
||||
// But in practice, many web servers cannot understand it.
|
||||
// See: https://www.w3.org/TR/2007/REC-soap12-part0-20070427/#L26854
|
||||
|
||||
if (soap_version_ == kSoapV11) {
|
||||
http_request->SetContentType(http::media_types::kTextXml,
|
||||
http::charsets::kUtf8);
|
||||
|
||||
Reference in New Issue
Block a user