diff --git a/include/cinatra/coro_http_client.hpp b/include/cinatra/coro_http_client.hpp index 2a84efab..68461518 100644 --- a/include/cinatra/coro_http_client.hpp +++ b/include/cinatra/coro_http_client.hpp @@ -164,6 +164,7 @@ class coro_http_client : public std::enable_shared_from_this { : coro_http_client(executor->get_asio_executor()) {} bool init_config(const config &conf) { + config_ = conf; if (conf.conn_timeout_duration.has_value()) { set_conn_timeout(*conf.conn_timeout_duration); } @@ -207,6 +208,8 @@ class coro_http_client : public std::enable_shared_from_this { coro_io::ExecutorWrapper<> &get_executor() { return executor_wrapper_; } + const config &get_config() { return config_; } + #ifdef CINATRA_ENABLE_SSL bool init_ssl(int verify_mode, const std::string &base_path, const std::string &cert_file, const std::string &sni_hostname) { @@ -875,7 +878,8 @@ class coro_http_client : public std::enable_shared_from_this { } async_simple::coro::Lazy send_file_no_chunked_with_copy( - std::string_view source, std::error_code &ec, std::size_t length) { + std::string_view source, std::error_code &ec, std::size_t length, + std::size_t offset) { if (length <= 0) { co_return; } @@ -883,6 +887,7 @@ class coro_http_client : public std::enable_shared_from_this { detail::resize(file_data, std::min(max_single_part_size_, length)); coro_io::coro_file file{}; file.open(source, std::ios::in); + file.seek(offset, std::ios::cur); if (!file.is_open()) { ec = std::make_error_code(std::errc::bad_file_descriptor); co_return; @@ -920,15 +925,15 @@ class coro_http_client : public std::enable_shared_from_this { }; async_simple::coro::Lazy send_file_without_copy( const std::filesystem::path &source, std::error_code &ec, - std::size_t length) { + std::size_t length, std::size_t offset) { fd_guard guard(source.c_str()); if (guard.fd < 0) [[unlikely]] { ec = std::make_error_code(std::errc::bad_file_descriptor); co_return; } std::size_t actual_len = 0; - std::tie(ec, actual_len) = - co_await coro_io::async_sendfile(socket_->impl_, guard.fd, 0, length); + std::tie(ec, actual_len) = co_await coro_io::async_sendfile( + socket_->impl_, guard.fd, offset, length); if (ec) [[unlikely]] { co_return; } @@ -1006,7 +1011,8 @@ class coro_http_client : public std::enable_shared_from_this { template async_simple::coro::Lazy async_upload( S uri, http_method method, Source source /* file */, - int64_t content_length = -1, + uint64_t offset = 0 /*file offset*/, + int64_t content_length = -1 /*upload size*/, req_content_type content_type = req_content_type::text, std::unordered_map headers = {}) { std::error_code ec{}; @@ -1050,6 +1056,12 @@ class coro_http_client : public std::enable_shared_from_this { co_return resp_data{std::make_error_code(std::errc::invalid_argument), 404}; } + content_length -= offset; + if (content_length < 0) { + CINATRA_LOG_ERROR << "the offset is larger than the end of file"; + co_return resp_data{std::make_error_code(std::errc::invalid_argument), + 404}; + } } assert(content_length >= 0); @@ -1088,6 +1100,7 @@ class coro_http_client : public std::enable_shared_from_this { } if constexpr (is_stream_file) { + source->seekg(offset, std::ios::cur); std::string file_data; detail::resize(file_data, std::min(max_single_part_size_, content_length)); @@ -1116,15 +1129,17 @@ class coro_http_client : public std::enable_shared_from_this { if (!has_init_ssl_) { #endif co_await send_file_without_copy(std::filesystem::path{source}, ec, - content_length); + content_length, offset); #ifdef CINATRA_ENABLE_SSL } else { - co_await send_file_no_chunked_with_copy(source, ec, content_length); + co_await send_file_no_chunked_with_copy(source, ec, content_length, + offset); } #endif #else - co_await send_file_no_chunked_with_copy(source, ec, content_length); + co_await send_file_no_chunked_with_copy(source, ec, content_length, + offset); #endif } else { @@ -2432,6 +2447,7 @@ class coro_http_client : public std::enable_shared_from_this { std::string resp_chunk_str_; std::span out_buf_; bool should_reset_ = false; + config config_; #ifdef CINATRA_ENABLE_GZIP bool enable_ws_deflate_ = false; diff --git a/tests/test_cinatra.cpp b/tests/test_cinatra.cpp index d066acfa..28108afb 100644 --- a/tests/test_cinatra.cpp +++ b/tests/test_cinatra.cpp @@ -271,7 +271,10 @@ bool create_file(std::string_view filename, size_t file_size = 1024) { return false; } - std::string str(file_size, 'A'); + std::string str; + for (int i = 0; i < file_size; ++i) { + str.push_back(rand() % 26 + 'A'); + } out.write(str.data(), str.size()); return true; } @@ -1146,40 +1149,45 @@ TEST_CASE("test coro_http_client multipart upload") { TEST_CASE("test coro_http_client upload") { auto test_upload_by_file_path = [](std::string filename, + std::size_t offset = 0, std::size_t r_size = SIZE_MAX, bool should_failed = false) { coro_http_client client{}; client.add_header("filename", filename); + client.add_header("offset", std::to_string(offset)); if (r_size != SIZE_MAX) client.add_header("filesize", std::to_string(r_size)); std::string uri = "http://127.0.0.1:8090/upload"; cinatra::resp_data result; if (r_size != SIZE_MAX) { - auto lazy = client.async_upload(uri, http_method::PUT, filename, r_size); + auto lazy = + client.async_upload(uri, http_method::PUT, filename, offset, r_size); result = async_simple::coro::syncAwait(lazy); } else { - auto lazy = client.async_upload(uri, http_method::PUT, filename); + auto lazy = client.async_upload(uri, http_method::PUT, filename, offset); result = async_simple::coro::syncAwait(lazy); } CHECK(((result.status == 200) ^ should_failed)); }; - auto test_upload_by_stream = [](std::string filename, + auto test_upload_by_stream = [](std::string filename, std::size_t offset = 0, std::size_t r_size = SIZE_MAX, bool should_failed = false) { coro_http_client client{}; client.add_header("filename", filename); + client.add_header("offset", std::to_string(offset)); if (r_size != SIZE_MAX) client.add_header("filesize", std::to_string(r_size)); std::string uri = "http://127.0.0.1:8090/upload"; std::ifstream ifs(filename, std::ios::binary); cinatra::resp_data result; if (r_size != SIZE_MAX) { - auto lazy = client.async_upload(uri, http_method::PUT, filename, r_size); + auto lazy = + client.async_upload(uri, http_method::PUT, filename, offset, r_size); result = async_simple::coro::syncAwait(lazy); } else { - auto lazy = client.async_upload(uri, http_method::PUT, filename); + auto lazy = client.async_upload(uri, http_method::PUT, filename, offset); result = async_simple::coro::syncAwait(lazy); } CHECK(((result.status == 200) ^ should_failed)); @@ -1189,6 +1197,7 @@ TEST_CASE("test coro_http_client upload") { bool should_failed = false) { coro_http_client client{}; client.add_header("filename", filename); + client.add_header("offset", "0"); if (r_size != SIZE_MAX) client.add_header("filesize", std::to_string(r_size)); std::string uri = "http://127.0.0.1:8090/upload"; @@ -1210,7 +1219,7 @@ TEST_CASE("test coro_http_client upload") { } else { auto lazy = - client.async_upload(uri, http_method::PUT, async_read, r_size); + client.async_upload(uri, http_method::PUT, async_read, 0, r_size); result = async_simple::coro::syncAwait(lazy); CHECK(((result.status == 200) ^ should_failed)); } @@ -1231,15 +1240,31 @@ TEST_CASE("test coro_http_client upload") { file.write(req.get_body().data(), req.get_body().size()); file.flush(); file.close(); + + size_t offset = 0; + std::string offset_s = std::string{req.get_header_value("offset")}; + if (!offset_s.empty()) { + offset = stoull(offset_s); + } + std::string filesize = std::string{req.get_header_value("filesize")}; + if (!filesize.empty()) { sz = stoull(filesize); } else { sz = std::filesystem::file_size(oldpath); + sz -= offset; } + CHECK(!filename.empty()); CHECK(sz == std::filesystem::file_size(newpath)); + std::ifstream ifs(oldpath); + ifs.seekg(offset, std::ios::cur); + std::string str; + str.resize(sz); + ifs.read(str.data(), sz); + CHECK(str == req.get_body()); resp.set_status_and_content(status_type::ok, std::string(filename)); co_return; }); @@ -1274,8 +1299,8 @@ TEST_CASE("test coro_http_client upload") { } bool r = create_file(filename, size); CHECK(r); - test_upload_by_file_path(filename, r_size); - test_upload_by_stream(filename, r_size); + test_upload_by_file_path(filename, 0, r_size); + test_upload_by_stream(filename, 0, r_size); test_upload_by_coro(filename, r_size); } } @@ -1292,11 +1317,62 @@ TEST_CASE("test coro_http_client upload") { } bool r = create_file(filename, size); CHECK(r); - test_upload_by_file_path(filename, r_size, true); - test_upload_by_stream(filename, r_size, true); + test_upload_by_file_path(filename, 0, r_size, true); + test_upload_by_stream(filename, 0, r_size, true); test_upload_by_coro(filename, r_size, true); } } + // upload with offset + { + auto sizes = {std::pair{1024 * 1024, 1'000'000}, + std::pair{2'000'000, 1'999'999}, std::pair{200, 1}, + std::pair{100, 0}, std::pair{0, 0}}; + for (auto [size, offset] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset); + test_upload_by_stream(filename, offset); + } + } + // upload with size & offset + { + auto sizes = {std::tuple{1024 * 1024, 500'000, 500'000}, + std::tuple{2'000'000, 1'999'999, 1}, std::tuple{200, 1, 199}, + std::tuple{100, 100, 0}}; + for (auto [size, offset, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset, r_size); + test_upload_by_stream(filename, offset, r_size); + } + } + // upload with too large size & offset + { + auto sizes = {std::tuple{1024 * 1024, 1'000'000, 50'000}, + std::tuple{2'000'000, 1'999'999, 2}, std::tuple{200, 1, 200}, + std::tuple{100, 100, 1}}; + for (auto [size, offset, r_size] : sizes) { + std::error_code ec{}; + fs::remove(filename, ec); + if (ec) { + std::cout << ec << "\n"; + } + bool r = create_file(filename, size); + CHECK(r); + test_upload_by_file_path(filename, offset, r_size, true); + test_upload_by_stream(filename, offset, r_size, true); + } + } } TEST_CASE("test coro_http_client chunked upload and download") {