Support file upload in server side.
parent
5f9532759e
commit
5c465a1e2d
@ -0,0 +1,55 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "boost/filesystem.hpp"
|
||||
|
||||
#include "webcc/http_client_session.h"
|
||||
#include "webcc/logger.h"
|
||||
|
||||
#if (defined(WIN32) || defined(_WIN64))
|
||||
// You need to set environment variable SSL_CERT_FILE properly to enable
|
||||
// SSL verification.
|
||||
bool kSslVerify = false;
|
||||
#else
|
||||
bool kSslVerify = true;
|
||||
#endif
|
||||
|
||||
void Help(const char* argv0) {
|
||||
std::cout << "Usage: " << argv0 << " <upload_dir>" << std::endl;
|
||||
std::cout << " E.g.," << std::endl;
|
||||
std::cout << " " << argv0 << "E:/github/webcc/data/upload" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
Help(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
|
||||
|
||||
const webcc::Path upload_dir(argv[1]);
|
||||
|
||||
namespace bfs = boost::filesystem;
|
||||
|
||||
if (!bfs::is_directory(upload_dir) || !bfs::exists(upload_dir)) {
|
||||
std::cerr << "Invalid upload dir!" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
webcc::HttpClientSession session;
|
||||
|
||||
//std::string url = "http://httpbin.org/post";
|
||||
std::string url = "http://localhost:8080/upload";
|
||||
|
||||
try {
|
||||
auto r = session.PostFile(url, "file",
|
||||
upload_dir / "remember.txt");
|
||||
|
||||
//std::cout << r->content() << std::endl;
|
||||
|
||||
} catch (const webcc::Exception& e) {
|
||||
std::cout << "Exception: " << e.what() << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "webcc/logger.h"
|
||||
#include "webcc/rest_server.h"
|
||||
#include "webcc/rest_service.h"
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
class FileUploadService : public webcc::RestService {
|
||||
public:
|
||||
void Handle(const webcc::RestRequest& request,
|
||||
webcc::RestResponse* response) final {
|
||||
if (request.http->method() == webcc::http::methods::kPost) {
|
||||
std::cout << "files: " << request.http->files().size() << std::endl;
|
||||
|
||||
for (auto& pair : request.http->files()) {
|
||||
std::cout << "name: " << pair.first << std::endl;
|
||||
std::cout << "data: " << std::endl << pair.second.data() << std::endl;
|
||||
}
|
||||
|
||||
response->content = "OK";
|
||||
response->media_type = webcc::http::media_types::kTextPlain;
|
||||
response->charset = "utf-8";
|
||||
response->status = webcc::http::Status::kCreated;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void Help(const char* argv0) {
|
||||
std::cout << "Usage: " << argv0 << " <port>" << std::endl;
|
||||
std::cout << " E.g.," << std::endl;
|
||||
std::cout << " " << argv0 << " 8080" << std::endl;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
Help(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
WEBCC_LOG_INIT("", webcc::LOG_CONSOLE);
|
||||
|
||||
std::uint16_t port = static_cast<std::uint16_t>(std::atoi(argv[1]));
|
||||
|
||||
std::size_t workers = 2;
|
||||
|
||||
try {
|
||||
webcc::RestServer server(port, workers);
|
||||
|
||||
server.Bind(std::make_shared<FileUploadService>(), "/upload", false);
|
||||
|
||||
server.Run();
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Exception: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
#include "webcc/common.h"
|
||||
|
||||
#include "boost/algorithm/string.hpp"
|
||||
|
||||
#include "webcc/logger.h"
|
||||
|
||||
namespace webcc {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool Split2(const std::string& str, char token, std::string* part1,
|
||||
std::string* part2) {
|
||||
std::size_t pos = str.find(token);
|
||||
if (pos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*part1 = str.substr(0, pos);
|
||||
*part2 = str.substr(pos + 1);
|
||||
|
||||
boost::trim(*part1);
|
||||
boost::trim(*part2);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void HttpHeaders::Set(const std::string& key, const std::string& value) {
|
||||
auto it = Find(key);
|
||||
if (it != headers_.end()) {
|
||||
it->second = value;
|
||||
} else {
|
||||
headers_.push_back({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
void HttpHeaders::Set(std::string&& key, std::string&& value) {
|
||||
auto it = Find(key);
|
||||
if (it != headers_.end()) {
|
||||
it->second = std::move(value);
|
||||
} else {
|
||||
headers_.push_back({ std::move(key), std::move(value) });
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpHeaders::Has(const std::string& key) const {
|
||||
return const_cast<HttpHeaders*>(this)->Find(key) != headers_.end();
|
||||
}
|
||||
|
||||
const std::string& HttpHeaders::Get(const std::string& key,
|
||||
bool* existed) const {
|
||||
auto it = const_cast<HttpHeaders*>(this)->Find(key);
|
||||
|
||||
if (existed != nullptr) {
|
||||
*existed = (it != headers_.end());
|
||||
}
|
||||
|
||||
if (it != headers_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
static const std::string s_no_value;
|
||||
return s_no_value;
|
||||
}
|
||||
|
||||
std::vector<HttpHeader>::iterator HttpHeaders::Find(const std::string& key) {
|
||||
auto it = headers_.begin();
|
||||
for (; it != headers_.end(); ++it) {
|
||||
if (boost::iequals(it->first, key)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static bool ParseValue(const std::string& str, const std::string& expected_key,
|
||||
std::string* value) {
|
||||
std::string key;
|
||||
if (!Split2(str, '=', &key, value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key != expected_key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !value->empty();
|
||||
}
|
||||
|
||||
ContentType::ContentType(const std::string& str) {
|
||||
Init(str);
|
||||
}
|
||||
|
||||
void ContentType::Parse(const std::string& str) {
|
||||
media_type_.clear();
|
||||
additional_.clear();
|
||||
multipart_ = false;
|
||||
|
||||
Init(str);
|
||||
}
|
||||
|
||||
bool ContentType::Valid() const {
|
||||
if (media_type_.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (multipart_) {
|
||||
return !boundary().empty();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ContentType::Init(const std::string& str) {
|
||||
std::string other;
|
||||
Split2(str, ';', &media_type_, &other);
|
||||
|
||||
if (media_type_ == "multipart/form-data") {
|
||||
multipart_ = true;
|
||||
if (!ParseValue(other, "boundary", &additional_)) {
|
||||
LOG_ERRO("Invalid 'multipart/form-data' content-type (no boundary).");
|
||||
} else {
|
||||
LOG_INFO("Content-type multipart boundary: %s.", additional_.c_str());
|
||||
}
|
||||
} else {
|
||||
if (ParseValue(other, "charset", &additional_)) {
|
||||
LOG_INFO("Content-type charset: %s.", additional_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static void Unquote(std::string& str) {
|
||||
boost::trim_if(str, boost::is_any_of("\""));
|
||||
}
|
||||
|
||||
bool ContentDisposition::Init(const std::string& str) {
|
||||
std::vector<std::string> parts;
|
||||
boost::split(parts, str, boost::is_any_of(";"));
|
||||
|
||||
if (parts.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parts[0] != "form-data") {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string key;
|
||||
std::string value;
|
||||
for (std::size_t i = 1; i < parts.size(); ++i) {
|
||||
if (!Split2(parts[i], '=', &key, &value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (key == "name") {
|
||||
name_ = value;
|
||||
Unquote(name_);
|
||||
} else if (key == "filename") {
|
||||
file_name_ = value;
|
||||
Unquote(file_name_);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
@ -0,0 +1,136 @@
|
||||
#ifndef WEBCC_COMMON_H_
|
||||
#define WEBCC_COMMON_H_
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace webcc {
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Split a string to two parts by the given token.
|
||||
bool Split2(const std::string& str, char token, std::string* part1,
|
||||
std::string* part2);
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
typedef std::pair<std::string, std::string> HttpHeader;
|
||||
|
||||
class HttpHeaders {
|
||||
public:
|
||||
std::size_t size() const {
|
||||
return headers_.size();
|
||||
}
|
||||
|
||||
const std::vector<HttpHeader>& data() const {
|
||||
return headers_;
|
||||
}
|
||||
|
||||
void Set(const std::string& key, const std::string& value);
|
||||
|
||||
void Set(std::string&& key, std::string&& value);
|
||||
|
||||
bool Has(const std::string& key) const;
|
||||
|
||||
// Get header by index.
|
||||
const HttpHeader& Get(std::size_t index) const {
|
||||
assert(index < size());
|
||||
return headers_[index];
|
||||
}
|
||||
|
||||
// Get header value by key.
|
||||
// If there's no such header with the given key, besides return empty, the
|
||||
// optional |existed| parameter will be set to false.
|
||||
const std::string& Get(const std::string& key, bool* existed = nullptr) const;
|
||||
|
||||
void Clear() {
|
||||
headers_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<HttpHeader>::iterator Find(const std::string& key);
|
||||
|
||||
std::vector<HttpHeader> headers_;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Content-Type header.
|
||||
// Syntax:
|
||||
// Content-Type: text/html; charset=utf-8
|
||||
// Content-Type: multipart/form-data; boundary=something
|
||||
class ContentType {
|
||||
public:
|
||||
explicit ContentType(const std::string& str = "");
|
||||
|
||||
void Parse(const std::string& str);
|
||||
|
||||
bool Valid() const;
|
||||
|
||||
bool multipart() const {
|
||||
return multipart_;
|
||||
}
|
||||
|
||||
const std::string& media_type() const {
|
||||
return media_type_;
|
||||
}
|
||||
|
||||
const std::string& charset() const {
|
||||
assert(!multipart_);
|
||||
return additional_;
|
||||
}
|
||||
|
||||
const std::string& boundary() const {
|
||||
assert(multipart_);
|
||||
return additional_;
|
||||
}
|
||||
|
||||
private:
|
||||
void Init(const std::string& str);
|
||||
|
||||
private:
|
||||
std::string media_type_;
|
||||
std::string additional_;
|
||||
bool multipart_ = false;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Content-Disposition header.
|
||||
// Syntax:
|
||||
// Content-Disposition: form-data
|
||||
// Content-Disposition: form-data; name="fieldName"
|
||||
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
|
||||
class ContentDisposition {
|
||||
public:
|
||||
explicit ContentDisposition(const std::string& str) {
|
||||
valid_ = Init(str);
|
||||
}
|
||||
|
||||
bool valid() const {
|
||||
return valid_;
|
||||
}
|
||||
|
||||
const std::string& name() const {
|
||||
return name_;
|
||||
}
|
||||
|
||||
const std::string& file_name() const {
|
||||
return file_name_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool Init(const std::string& str);
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::string file_name_;
|
||||
bool valid_ = false;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_COMMON_H_
|
Loading…
Reference in New Issue