From a207eccf14c47410a332086a30ef675aaafa6808 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Fri, 6 Dec 2024 12:15:34 +0000 Subject: [PATCH] burl: add --upload-file option --- example/client/burl/file.cpp | 42 ++++++ example/client/burl/file.hpp | 25 ++++ example/client/burl/main.cpp | 154 +++++--------------- example/client/burl/message.cpp | 182 ++++++++++++++++++++++++ example/client/burl/message.hpp | 117 +++++++++++++++ example/client/burl/multipart_form.cpp | 97 ++++++------- example/client/burl/multipart_form.hpp | 9 +- example/client/burl/urlencoded_form.cpp | 6 + example/client/burl/urlencoded_form.hpp | 9 +- 9 files changed, 464 insertions(+), 177 deletions(-) create mode 100644 example/client/burl/file.cpp create mode 100644 example/client/burl/file.hpp create mode 100644 example/client/burl/message.cpp create mode 100644 example/client/burl/message.hpp diff --git a/example/client/burl/file.cpp b/example/client/burl/file.cpp new file mode 100644 index 0000000..9c37d77 --- /dev/null +++ b/example/client/burl/file.cpp @@ -0,0 +1,42 @@ +// +// Copyright (c) 2024 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_io +// + +#include "file.hpp" + +#include +#include + +namespace http_proto = boost::http_proto; +using system_error = boost::system::system_error; + +std::uint64_t +filesize(const std::string& path) +{ + http_proto::file file; + boost::system::error_code ec; + + file.open(path.c_str(), http_proto::file_mode::scan, ec); + if(ec) + throw system_error{ ec }; + + const auto size = file.size(ec); + if(ec) + throw system_error{ ec }; + + return size; +} + +core::string_view +filename(core::string_view path) noexcept +{ + const auto pos = path.find_last_of("/\\"); + if((pos != core::string_view::npos)) + return path.substr(pos + 1); + return path; +} diff --git a/example/client/burl/file.hpp b/example/client/burl/file.hpp new file mode 100644 index 0000000..5b5b7ec --- /dev/null +++ b/example/client/burl/file.hpp @@ -0,0 +1,25 @@ +// +// Copyright (c) 2024 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_io +// + +#ifndef BURL_FILE_HPP +#define BURL_FILE_HPP + +#include + +#include + +namespace core = boost::core; + +std::uint64_t +filesize(const std::string& path); + +core::string_view +filename(core::string_view path) noexcept; + +#endif diff --git a/example/client/burl/main.cpp b/example/client/burl/main.cpp index fedbd8b..7b7bd01 100644 --- a/example/client/burl/main.cpp +++ b/example/client/burl/main.cpp @@ -12,6 +12,8 @@ #include "base64.hpp" #include "connect.hpp" #include "cookie.hpp" +#include "file.hpp" +#include "message.hpp" #include "mime_type.hpp" #include "multipart_form.hpp" #include "urlencoded_form.hpp" @@ -96,124 +98,6 @@ can_reuse_connection( return true; } -class json_body -{ - std::string body_; - -public: - void - append(core::string_view value) noexcept - { - body_.append(value); - } - - void - append(std::istream& is) - { - body_.append(std::istreambuf_iterator{ is }, {}); - } - - core::string_view - content_type() const noexcept - { - return "application/json"; - } - - std::size_t - content_length() const noexcept - { - return body_.size(); - } - - buffers::const_buffer - body() const noexcept - { - return { body_.data(), body_.size() }; - } -}; - -class message -{ - std::variant< - std::monostate, - json_body, - urlencoded_form, - multipart_form> body_; -public: - message() = default; - - message(json_body&& json_body) - : body_{ std::move(json_body) } - { - } - - message(urlencoded_form&& form) - : body_{ std::move(form) } - { - } - - message(multipart_form&& form) - : body_{ std::move(form) } - { - } - - void - set_headers(http_proto::request& req) const - { - std::visit( - [&](auto& f) - { - if constexpr(!std::is_same_v< - decltype(f), const std::monostate&>) - { - req.set_method(http_proto::method::post); - - auto content_length = f.content_length(); - req.set_content_length(content_length); - if(content_length >= 1024 * 1024) - { - req.set( - http_proto::field::expect, - "100-continue"); - } - - req.set( - http_proto::field::content_type, - f.content_type()); - } - }, - body_); - } - - void - start_serializer( - http_proto::serializer& ser, - http_proto::request& req) const - { - std::visit( - [&](auto& f) - { - if constexpr(std::is_same_v< - decltype(f), const multipart_form&>) - { - ser.start< - multipart_form::source>(req, &f); - } - else if constexpr( - std::is_same_v || - std::is_same_v) - { - ser.start(req, f.body()); - } - else - { - ser.start(req); - } - }, - body_); - } -}; - http_proto::request create_request( const po::variables_map& vm, @@ -629,6 +513,9 @@ main(int argc, char* argv[]) ("unix-socket", po::value()->value_name(""), "Connect through this Unix domain socket") + ("upload-file,T", + po::value()->value_name(""), + "Transfer local FILE to destination") ("url", po::value()->value_name(""), "URL to work with") @@ -774,9 +661,14 @@ main(int argc, char* argv[]) auto msg = message{}; - if((!!vm.count("form") + !!vm.count("data") + !!vm.count("json")) == 2) + if((!!vm.count("form") + + !!vm.count("data") + + !!vm.count("json") + + !!vm.count("upload-file")) == 2) + { throw std::runtime_error{ "You can only select one HTTP request method"}; + } if(vm.count("form")) { @@ -872,6 +764,30 @@ main(int argc, char* argv[]) msg = std::move(body); } + if(vm.count("upload-file")) + { + msg = [&]()-> message + { + auto path = vm.at("upload-file").as(); + if(path == "-") + return stdin_body{}; + + // Append the filename to the URL if it + // doesn't already end with one + auto segs = url.encoded_segments(); + if(segs.empty()) + { + segs.push_back(::filename(path)); + } + else if(auto back = --segs.end(); back->empty()) + { + segs.replace(back, ::filename(path)); + } + + return file_body{ std::move(path) }; + }(); + } + auto cookie_jar = std::optional<::cookie_jar>{}; auto explicit_cookies = std::string{}; diff --git a/example/client/burl/message.cpp b/example/client/burl/message.cpp new file mode 100644 index 0000000..75edd1c --- /dev/null +++ b/example/client/burl/message.cpp @@ -0,0 +1,182 @@ +// +// Copyright (c) 2024 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_io +// + +#include "message.hpp" +#include "file.hpp" +#include "mime_type.hpp" + +#include +#include + +#include + +using system_error = boost::system::system_error; + +void +json_body::append(core::string_view value) noexcept +{ + body_.append(value); +} + +void +json_body::append(std::istream& is) +{ + body_.append(std::istreambuf_iterator{ is }, {}); +} + +http_proto::method +json_body::method() const +{ + return http_proto::method::post; +} + +core::string_view +json_body::content_type() const noexcept +{ + return "application/json"; +} + +std::size_t +json_body::content_length() const noexcept +{ + return body_.size(); +} + +buffers::const_buffer +json_body::body() const noexcept +{ + return { body_.data(), body_.size() }; +} + +// ----------------------------------------------------------------------------- + +file_body::file_body(std::string path) + : path_{ std::move(path) } +{ +} + +http_proto::method +file_body::method() const +{ + return http_proto::method::put; +} + +core::string_view +file_body::content_type() const noexcept +{ + return mime_type(path_); +} + +std::uint64_t +file_body::content_length() const +{ + return ::filesize(path_); +} + +http_proto::file_body +file_body::body() const +{ + http_proto::file file; + error_code ec; + file.open(path_.c_str(), http_proto::file_mode::read, ec); + if(ec) + throw system_error{ ec }; + + return http_proto::file_body{ std::move(file), content_length() }; +} + +// ----------------------------------------------------------------------------- + +boost::http_proto::source::results +stdin_body::source::on_read(buffers::mutable_buffer mb) +{ + std::cin.read(static_cast(mb.data()), mb.size()); + + return { .ec = {}, + .bytes = static_cast(std::cin.gcount()), + .finished = std::cin.eof() }; +} + +http_proto::method +stdin_body::method() const +{ + return http_proto::method::put; +} + +core::string_view +stdin_body::content_type() const noexcept +{ + return "application/octet-stream"; +} + +boost::optional +stdin_body::content_length() const +{ + return boost::none; +} + +stdin_body::source +stdin_body::body() const +{ + return {}; +} + +// ----------------------------------------------------------------------------- + +void +message::set_headers(http_proto::request& request) const +{ + std::visit( + [&](auto& f) + { + using field = http_proto::field; + if constexpr(!std::is_same_v) + { + request.set_method(f.method()); + + boost::optional content_length = + f.content_length(); + if(content_length.has_value()) + { + request.set_content_length(content_length.value()); + if(content_length.value() >= 1024 * 1024) + request.set(field::expect, "100-continue"); + } + else + { + request.set_chunked(true); + request.set(field::expect, "100-continue"); + } + + request.set(field::content_type, f.content_type()); + } + }, + body_); +} + +void +message::start_serializer( + http_proto::serializer& serializer, + http_proto::request& request) const +{ + std::visit( + [&](auto& f) + { + if constexpr(!std::is_same_v) + { + serializer.start>( + request, f.body()); + } + else + { + serializer.start(request); + } + }, + body_); +} diff --git a/example/client/burl/message.hpp b/example/client/burl/message.hpp new file mode 100644 index 0000000..3bf5059 --- /dev/null +++ b/example/client/burl/message.hpp @@ -0,0 +1,117 @@ +// +// Copyright (c) 2024 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/http_io +// + +#ifndef BURL_MESSAGE_HPP +#define BURL_MESSAGE_HPP + +#include "multipart_form.hpp" +#include "urlencoded_form.hpp" + +#include +#include +#include + +#include + +class json_body +{ + std::string body_; + +public: + void + append(core::string_view value) noexcept; + + void + append(std::istream& is); + + http_proto::method + method() const; + + core::string_view + content_type() const noexcept; + + std::size_t + content_length() const noexcept; + + buffers::const_buffer + body() const noexcept; +}; + +class file_body +{ + std::string path_; + +public: + file_body(std::string path); + + http_proto::method + method() const; + + core::string_view + content_type() const noexcept; + + std::uint64_t + content_length() const; + + http_proto::file_body + body() const; +}; + +class stdin_body +{ +public: + class source : public http_proto::source + { + public: + results + on_read(buffers::mutable_buffer mb) override; + }; + + http_proto::method + method() const; + + core::string_view + content_type() const noexcept; + + boost::optional + content_length() const; + + source + body() const; +}; + +class message +{ + std::variant< + std::monostate, + json_body, + urlencoded_form, + multipart_form, + file_body, + stdin_body> + body_; + +public: + message() = default; + + message(auto&& body) + : body_{ std::move(body) } + { + } + + void + set_headers(http_proto::request& request) const; + + void + start_serializer( + http_proto::serializer& serializer, + http_proto::request& request) const; +}; + +#endif diff --git a/example/client/burl/multipart_form.cpp b/example/client/burl/multipart_form.cpp index 5587ace..14a9f8e 100644 --- a/example/client/burl/multipart_form.cpp +++ b/example/client/burl/multipart_form.cpp @@ -8,6 +8,7 @@ // #include "multipart_form.hpp" +#include "file.hpp" #include #include @@ -35,32 +36,6 @@ generate_boundary() return rs; } -std::uint64_t -filesize(core::string_view path) -{ - http_proto::file file; - boost::system::error_code ec; - - file.open(std::string{ path }.c_str(), http_proto::file_mode::scan, ec); - if(ec) - throw system_error{ ec }; - - const auto size = file.size(ec); - if(ec) - throw system_error{ ec }; - - return size; -} - -core::string_view -filename(core::string_view path) noexcept -{ - const auto pos = path.find_last_of("/\\"); - if((pos != core::string_view::npos)) - return path.substr(pos + 1); - return path; -} - core::string_view content_disposition_ = "\r\nContent-Disposition: form-data; name=\""; core::string_view filename_ = "; filename=\""; @@ -74,6 +49,43 @@ multipart_form::multipart_form() { } +void +multipart_form::append_text( + std::string name, + std::string value, + boost::optional content_type) +{ + auto size = value.size(); + parts_.push_back( + { std::move(name), + boost::none, + std::move(content_type), + size, + std::move(value) }); +} + +void +multipart_form::append_file( + std::string name, + std::string path, + boost::optional content_type) +{ + auto filename = ::filename(path); + auto size = ::filesize(path); + parts_.push_back( + { std::move(name), + std::string{ filename }, + std::move(content_type), + size, + std::move(path) }); +} + +http_proto::method +multipart_form::method() const +{ + return http_proto::method::post; +} + std::string multipart_form::content_type() const { @@ -112,39 +124,14 @@ multipart_form::content_length() const noexcept rs += 2; // after content } rs += storage_.size(); // --boundary-- - rs += 2; // + rs += 2; // return rs; } -void -multipart_form::append_text( - std::string name, - std::string value, - boost::optional content_type) -{ - auto size = value.size(); - parts_.push_back( - { std::move(name), - boost::none, - std::move(content_type), - size, - std::move(value) }); -} - -void -multipart_form::append_file( - std::string name, - std::string path, - boost::optional content_type) +multipart_form::source +multipart_form::body() const { - auto filename = ::filename(path); - auto size = ::filesize(path); - parts_.push_back( - { std::move(name), - std::string{ filename }, - std::move(content_type), - size, - std::move(path) }); + return source{ this }; } // ----------------------------------------------------------------------------- diff --git a/example/client/burl/multipart_form.hpp b/example/client/burl/multipart_form.hpp index 268a67a..7cec43a 100644 --- a/example/client/burl/multipart_form.hpp +++ b/example/client/burl/multipart_form.hpp @@ -12,9 +12,10 @@ #include #include +#include #include -#include #include +#include #include #include @@ -55,11 +56,17 @@ class multipart_form std::string path, boost::optional content_type); + http_proto::method + method() const; + std::string content_type() const; std::uint64_t content_length() const noexcept; + + source + body() const; }; class multipart_form::source : public http_proto::source diff --git a/example/client/burl/urlencoded_form.cpp b/example/client/burl/urlencoded_form.cpp index 019c78d..0eb464f 100644 --- a/example/client/burl/urlencoded_form.cpp +++ b/example/client/burl/urlencoded_form.cpp @@ -43,6 +43,12 @@ urlencoded_form::append(std::istream& is) } } +http_proto::method +urlencoded_form::method() const +{ + return http_proto::method::post; +} + core::string_view urlencoded_form::content_type() const noexcept { diff --git a/example/client/burl/urlencoded_form.hpp b/example/client/burl/urlencoded_form.hpp index fd5d903..68c79e0 100644 --- a/example/client/burl/urlencoded_form.hpp +++ b/example/client/burl/urlencoded_form.hpp @@ -12,12 +12,14 @@ #include #include +#include #include #include -namespace buffers = boost::buffers; -namespace core = boost::core; +namespace buffers = boost::buffers; +namespace core = boost::core; +namespace http_proto = boost::http_proto; class urlencoded_form { @@ -30,6 +32,9 @@ class urlencoded_form void append(std::istream& is); + http_proto::method + method() const; + core::string_view content_type() const noexcept;