LCOV - code coverage report
Current view: top level - http/server - multipart.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 90.5 % 21 19
Test Date: 2026-11-10 10:38:07 Functions: 90.5 % 21 19

            Line data    Source code
       1              : #ifndef THINGER_HTTP_SERVER_MULTIPART_HPP
       2              : #define THINGER_HTTP_SERVER_MULTIPART_HPP
       3              : 
       4              : #include <cstdint>
       5              : #include <filesystem>
       6              : #include <functional>
       7              : #include <limits>
       8              : #include <memory>
       9              : #include <optional>
      10              : #include <string>
      11              : #include <string_view>
      12              : #include <system_error>
      13              : #include <utility>
      14              : #include <vector>
      15              : #include "../common/multipart_parser.hpp"
      16              : #include "../../util/types.hpp"
      17              : 
      18              : namespace thinger::http {
      19              : 
      20              :     class request;
      21              : 
      22              :     /**
      23              :      * Reads a multipart body (multipart/form-data, or any other multipart type) part by part
      24              :      * as it arrives, like Go's multipart.Reader: the headers of each part, then its content in
      25              :      * pieces, never buffering more than its read buffer and the header section of a part.
      26              :      *
      27              :      *   http::multipart_reader reader(req);
      28              :      *   while (const http::multipart_part* part = co_await reader.next()) {
      29              :      *       for (auto data = co_await reader.read(); !data.empty(); data = co_await reader.read()) { ... }
      30              :      *   }
      31              :      *   if (reader.failed()) res.error(reader.status(), reader.message());
      32              :      *
      33              :      * The body is read with request::read_some(), within the maximum body size of the route
      34              :      * (body_too_large), or from req.body() if the server already read it (routes that are not
      35              :      * deferred). A body with a Content-Encoding is not decoded: not_multipart (415). A read that
      36              :      * reaches the read deadline of the request (request::set_read_deadline()) fails with
      37              :      * invalid_body (400): req.get_body_error() is body_error::timeout, to answer 408 instead.
      38              :      */
      39              :     class multipart_reader {
      40              :     public:
      41              :         // Bytes read from the body at once
      42              :         static constexpr size_t default_buffer_size = 64 * 1024;
      43              : 
      44              :         /// Reader of the body of `req`, which must outlive it
      45              :         explicit multipart_reader(request& req, multipart_limits limits = {}, size_t buffer_size = default_buffer_size);
      46              : 
      47              :         /// The next part, skipping what is left of the current one; null at the end of the
      48              :         /// body, on error (see failed()), or if the read was cancelled. Valid until the next call.
      49              :         thinger::awaitable<const multipart_part*> next();
      50              : 
      51              :         /// The next piece of the content of the current part, of `max_size` bytes at most (what
      52              :         /// is left of it stays for the next call, as Go's Part.Read): a view into the buffer of
      53              :         /// the reader, no copy, valid until the next call. Empty at the end of the part, on
      54              :         /// error or if the read was cancelled. read(0) returns empty at once without advancing
      55              :         /// or reading: it does not mean the end of the part.
      56              :         thinger::awaitable<std::string_view> read(size_t max_size = std::numeric_limits<size_t>::max());
      57              : 
      58              :         /// Append the rest of the content of the current part to `value` (bounded by the
      59              :         /// limits of the part); false on error or if the read was cancelled
      60              :         thinger::awaitable<bool> read_all(std::string& value);
      61              : 
      62              :         /// Media type of the body, lowercase ("multipart/form-data"), empty if not multipart
      63           84 :         const std::string& media_type() const { return media_type_; }
      64              : 
      65              :         /// The current part, null before the first one and after the last
      66            3 :         const multipart_part* part() const { return in_part_ ? &parser_->part() : nullptr; }
      67              : 
      68              :         /// The whole body was read, up to its closing delimiter (and its epilogue)
      69          117 :         bool done() const { return done_; }
      70          222 :         bool failed() const { return error_ != multipart_error::none; }
      71           66 :         multipart_error error() const { return error_; }
      72              : 
      73              :         /// Status and message answering the error (400, 413, 415 or 500)
      74            0 :         http_response::status status() const { return multipart_error_status(error_); }
      75            0 :         const char* message() const { return multipart_error_message(error_); }
      76              : 
      77              :     private:
      78              :         request& req_;
      79              :         std::optional<multipart_parser> parser_;
      80              :         std::string media_type_;
      81              :         std::unique_ptr<uint8_t[]> buffer_;
      82              :         size_t buffer_size_;
      83              :         std::string_view input_;
      84              :         std::string_view pending_;  // what read() left of the last piece of the parser
      85              :         multipart_error error_ = multipart_error::none;
      86              :         bool in_memory_ = false;    // the body was read by the server: parse req.body()
      87              :         bool finished_ = false;     // the whole body was read
      88              :         bool in_part_ = false;
      89              :         bool done_ = false;
      90              : 
      91              :         void fail(multipart_error error);
      92              : 
      93              :         /// The next event of the parser, reading the body as needed; need_more if a read was
      94              :         /// cancelled
      95              :         thinger::awaitable<multipart_parser::event> advance();
      96              : 
      97              :         /// Read more of the body into the input; false if the read was cancelled
      98              :         thinger::awaitable<bool> fill();
      99              :     };
     100              : 
     101              :     /**
     102              :      * A file of a multipart form, stored in a temporary file that is deleted with this object
     103              :      * (at the end of the request handler, usually) unless it is moved to its destination with
     104              :      * move_to() or kept with release().
     105              :      */
     106              :     class multipart_file {
     107              :     public:
     108              :         multipart_file(multipart_part part, std::filesystem::path path, std::uint64_t size);
     109              :         ~multipart_file();
     110              : 
     111              :         multipart_file(multipart_file&& other) noexcept;
     112              :         multipart_file& operator=(multipart_file&& other) noexcept;
     113              :         multipart_file(const multipart_file&) = delete;
     114              :         multipart_file& operator=(const multipart_file&) = delete;
     115              : 
     116              :         /// Field name
     117           27 :         const std::string& name() const { return part_.name; }
     118              :         /// Filename as the client sent it: never use it as a path (see safe_filename())
     119           12 :         const std::string& filename() const { return part_.filename; }
     120              :         /// The filename reduced to a name to store it with (see multipart_part::safe_filename())
     121           15 :         std::string safe_filename() const { return part_.safe_filename(); }
     122              :         /// Content-Type of the part ("text/plain" if it had none), as the client says
     123           12 :         const std::string& content_type() const { return part_.content_type; }
     124              :         /// Every header of the part
     125              :         const multipart_part& part() const { return part_; }
     126              : 
     127              :         /// Bytes of content
     128           15 :         std::uint64_t size() const { return size_; }
     129              : 
     130              :         /// The file with the content: the temporary file (readable by its owner only), or
     131              :         /// wherever move_to() moved it
     132           30 :         const std::filesystem::path& path() const { return path_; }
     133              : 
     134              :         /// Move the file to `destination` (renamed, or copied if it is in another file
     135              :         /// system), replacing any file there: it is no longer deleted. False on error.
     136              :         bool move_to(const std::filesystem::path& destination, std::error_code& error);
     137              :         bool move_to(const std::filesystem::path& destination);
     138              : 
     139              :         /// Keep the temporary file (no longer deleted), returning its path
     140              :         std::filesystem::path release();
     141              : 
     142              :     private:
     143              :         multipart_part part_;
     144              :         std::filesystem::path path_;
     145              :         std::uint64_t size_ = 0;
     146              :         bool owned_ = true;     // the temporary file is deleted with this object
     147              : 
     148              :         void remove();
     149              :     };
     150              : 
     151              :     // How request::read_multipart() reads a form
     152              :     struct multipart_options {
     153              :         multipart_limits limits;
     154              :         // Directory of the temporary files (the system one, std::filesystem::temp_directory_path(), if empty)
     155              :         std::filesystem::path temp_directory;
     156              :         // Handler of the file parts, instead of temporary files: it reads the content with
     157              :         // reader.read() (what it leaves unread is skipped) and stores it wherever it wants.
     158              :         // The form then has no files. What it throws ends the request (500).
     159              :         std::function<thinger::awaitable<void>(const multipart_part& part, multipart_reader& reader)> on_file;
     160              :     };
     161              : 
     162              :     class multipart_form;
     163              : 
     164              :     /// Read the multipart/form-data body of a request (see request::read_multipart())
     165              :     thinger::awaitable<multipart_form> read_multipart(request& req, multipart_options options = {});
     166              : 
     167              :     /**
     168              :      * A multipart/form-data body read whole (request::read_multipart()): its fields in memory
     169              :      * and its files in temporary files, deleted with the form. Parts without a name, or with a
     170              :      * disposition other than form-data, are skipped (as Go and busboy do).
     171              :      */
     172              :     class multipart_form {
     173              :     public:
     174              :         multipart_form() = default;
     175          408 :         multipart_form(multipart_form&&) noexcept = default;
     176              :         multipart_form& operator=(multipart_form&&) noexcept = default;
     177              : 
     178              :         /// Parts without filename: their names and values, in order
     179           24 :         const std::vector<std::pair<std::string, std::string>>& fields() const { return fields_; }
     180              : 
     181              :         /// Value of the first field with this name, null if none
     182              :         const std::string* field(std::string_view name) const;
     183              : 
     184              :         /// Value of the first field with this name, or `default_value`
     185              :         std::string field(std::string_view name, std::string_view default_value) const;
     186              : 
     187              :         /// Parts with a filename, in order
     188            6 :         std::vector<multipart_file>& files() { return files_; }
     189           24 :         const std::vector<multipart_file>& files() const { return files_; }
     190              : 
     191              :         /// The first file with this field name, null if none
     192              :         multipart_file* file(std::string_view name);
     193              : 
     194              :         /// Whether the whole body was read; if not, the error tells why, and the form is empty
     195          102 :         bool ok() const { return error_ == multipart_error::none; }
     196          102 :         explicit operator bool() const { return ok(); }
     197              :         multipart_error error() const { return error_; }
     198           69 :         http_response::status status() const { return multipart_error_status(error_); }
     199           69 :         const char* message() const { return multipart_error_message(error_); }
     200              : 
     201              :     private:
     202              :         std::vector<std::pair<std::string, std::string>> fields_;
     203              :         std::vector<multipart_file> files_;
     204              :         multipart_error error_ = multipart_error::none;
     205              : 
     206              :         friend thinger::awaitable<multipart_form> read_multipart(request& req, multipart_options options);
     207              :     };
     208              : 
     209              : }
     210              : 
     211              : #endif
        

Generated by: LCOV version 2.0-1