diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fc5b7d..5d23f538 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Version 74: * Add file_stdio and File concept * Add file_win32 +* Add file_body -------------------------------------------------------------------------------- diff --git a/doc/0_main.qbk b/doc/0_main.qbk index 658e9cbf..90de1b15 100644 --- a/doc/0_main.qbk +++ b/doc/0_main.qbk @@ -79,12 +79,12 @@ [def __static_buffer_n__ [link beast.ref.beast__static_buffer_n `static_buffer_n`]] [import ../example/common/detect_ssl.hpp] -[import ../example/common/file_body.hpp] [import ../example/doc/http_examples.hpp] [import ../example/echo-op/echo_op.cpp] [import ../example/http-client/http_client.cpp] [import ../example/websocket-client/websocket_client.cpp] +[import ../include/beast/http/file_body.hpp] [import ../test/exemplars.cpp] [import ../test/core/doc_snippets.cpp] diff --git a/doc/5_02_message.qbk b/doc/5_02_message.qbk index 1a7c6992..19536836 100644 --- a/doc/5_02_message.qbk +++ b/doc/5_02_message.qbk @@ -144,6 +144,14 @@ meet the requirements, or use the ones that come with the library: and parsed; however, body octets received while parsing a message with this body will generate a unique error. ]] +[[ + [link beast.ref.beast__http__file_body `file_body`] +][ + This body is represented by a file opened for either reading or + writing. Messages with this body may be serialized and parsed. + HTTP algorithms will use the open file for reading and writing, + for streaming and incremental sends and receives. +]] [[ [link beast.ref.beast__http__string_body `string_body`] ][ diff --git a/doc/5_08_custom_body.qbk b/doc/5_08_custom_body.qbk index baa8f39d..b209ac5d 100644 --- a/doc/5_08_custom_body.qbk +++ b/doc/5_08_custom_body.qbk @@ -103,20 +103,22 @@ Use of the flexible __Body__ concept customization point enables authors to preserve the self-contained nature of the __message__ object while allowing domain specific behaviors. Common operations for HTTP servers include sending responses which deliver file contents, and allowing for file uploads. In this -example we build the `file_body` type which supports both reading and writing -to a file on the file system. +example we build the `basic_file_body` type which supports both reading and +writing to a file on the file system. The interface is a class templated +on the type of file used to access the file system, which must meet the +requirements of __File__. First we declare the type with its nested types: [example_http_file_body_1] We will start with the definition of the `value_type`. Our strategy -will be to store the open file handle directly in the message -container through the `value_type` field. To use this body it will -be necessary to call `msg.body.open()` with the file first. This -ensures that the file exists throughout the operation and prevent -the race condition where the file is removed from the file system -in between calls. +will be to store the file object directly in the message container +through the `value_type` field. To use this body it will be necessary +to call `msg.body.file().open()` first with the required information +such as the path and open mode. This ensures that the file exists +throughout the operation and prevent the race condition where the +file is removed from the file system in between calls. [example_http_file_body_2] @@ -143,9 +145,11 @@ Finally, here is the implementation of the writer member functions: We have created a full featured body type capable of reading and writing files on the filesystem, integrating seamlessly with the -HTTP algorithms and message container. Source code for this body -type, and HTTP servers that use it, are available in the examples -directory. +HTTP algorithms and message container. The body type works with +any file implementation meeting the requirements of __File__ so +it may be transparently used with solutions optimized for particular +platforms. Example HTTP servers which use file bodies are available +in the example directory. [endsect] diff --git a/doc/quickref.xml b/doc/quickref.xml index 02784257..1c899955 100644 --- a/doc/quickref.xml +++ b/doc/quickref.xml @@ -30,6 +30,7 @@ Classes basic_dynamic_body + basic_file_body basic_fields basic_parser buffer_body diff --git a/include/beast/http.hpp b/include/beast/http.hpp index 898edc3a..3d0ae11e 100644 --- a/include/beast/http.hpp +++ b/include/beast/http.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include diff --git a/include/beast/http/file_body.hpp b/include/beast/http/file_body.hpp new file mode 100644 index 00000000..ea59e270 --- /dev/null +++ b/include/beast/http/file_body.hpp @@ -0,0 +1,461 @@ +// +// Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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) +// + +#ifndef BEAST_HTTP_FILE_BODY_HPP +#define BEAST_HTTP_FILE_BODY_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace http { + +//[example_http_file_body_1 + +/** A message body represented by a file on the filesystem. + + Messages with this type have bodies represented by a + file on the file system. When parsing a message using + this body type, the data is stored in the file pointed + to by the path, which must be writable. When serializing, + the implementation will read the file and present those + octets as the body content. This may be used to serve + content from a directory as part of a web service. + + @tparam File The implementation to use for accessing files. + This type must meet the requirements of @b File. +*/ +template +struct basic_file_body +{ + static_assert(is_file::value, + "File requirements not met"); + + /// The type of File this body uses + using file_type = File; + + /** Algorithm for retrieving buffers when serializing. + + Objects of this type are created during serialization + to extract the buffers representing the body. + */ + class reader; + + /** Algorithm for storing buffers when parsing. + + Objects of this type are created during parsing + to store incoming buffers representing the body. + */ + class writer; + + /** The type of the @ref message::body member. + + Messages declared using `basic_file_body` will have this + type for the body member. This rich class interface + allow the file to be opened with the file handle + maintained directly in the object, which is attached + to the message. + */ + class value_type; + + /** Returns the size of the body + + @param v The file body to use + */ + static + std::uint64_t + size(value_type const& v); +}; + +//] + +//[example_http_file_body_2 + +// The body container holds a handle to the file when +// it is open, and also caches the size when set. +// +template +class basic_file_body::value_type +{ + friend class reader; + friend class writer; + friend struct basic_file_body; + + // This represents the open file + File file_; + + // The cached file size + std::uint64_t file_size_ = 0; + +public: + /** Destructor. + + If the file is open, it is closed first. + */ + ~value_type() = default; + + /// Constructor + value_type() = default; + + /// Constructor + value_type(value_type&& other) = default; + + /// Move assignment + value_type& operator=(value_type&& other) = default; + + /// Returns `true` if the file is open + bool + is_open() const + { + return file_.is_open(); + } + + /// Returns the size of the file if open + std::uint64_t + size() const + { + return file_size_; + } + + /// Close the file if open + void + close(); + + /** Open a file at the given path with the specified mode + + @param path The utf-8 encoded path to the file + + @param mode The file mode to use + + @param ec Set to the error, if any occurred + */ + void + open(char const* path, file_mode mode, error_code& ec); + + /** Set the open file + + This function is used to set the open + */ + void + reset(File&& file, error_code& ec); +}; + +template +void +basic_file_body:: +value_type:: +close() +{ + error_code ignored; + file_.close(ignored); +} + +template +void +basic_file_body:: +value_type:: +open(char const* path, file_mode mode, error_code& ec) +{ + // Open the file + file_.open(path, mode, ec); + if(ec) + return; + + // Cache the size + file_size_ = file_.size(ec); + if(ec) + { + close(); + return; + } +} + +template +void +basic_file_body:: +value_type:: +reset(File&& file, error_code& ec) +{ + // First close the file if open + if(file_.is_open()) + { + error_code ignored; + file_.close(ignored); + } + + // Take ownership of the new file + file_ = std::move(file); + + // Cache the size + file_size_ = file_.size(ec); +} + +// This is called from message::payload_size +template +std::uint64_t +basic_file_body:: +size(value_type const& v) +{ + // Forward the call to the body + return v.size(); +} + +//] + +//[example_http_file_body_3 + +template +class basic_file_body::reader +{ + value_type const& body_; // The body we are reading from + std::uint64_t remain_; // The number of unread bytes + char buf_[4096]; // Small buffer for reading + +public: + // The type of buffer sequence returned by `get`. + // + using const_buffers_type = + boost::asio::const_buffers_1; + + // Constructor. + // + // `m` holds the message we are sending, which will + // always have the `file_body` as the body type. + // + template + reader( + message const& m, + error_code& ec); + + // This function is called zero or more times to + // retrieve buffers. A return value of `boost::none` + // means there are no more buffers. Otherwise, + // the contained pair will have the next buffer + // to serialize, and a `bool` indicating whether + // or not there may be additional buffers. + boost::optional> + get(error_code& ec); +}; + +//] + +//[example_http_file_body_4 + +// Here we just stash a reference to the path for later. +// Rather than dealing with messy constructor exceptions, +// we save the things that might fail for the call to `init`. +// +template +template +basic_file_body:: +reader:: +reader( + message const& m, + error_code& ec) + : body_(m.body) +{ + // The file must already be open + BOOST_ASSERT(body_.file_.is_open()); + + // Get the size of the file + remain_ = body_.file_.size(ec); + if(ec) + return; +} + +// This function is called repeatedly by the serializer to +// retrieve the buffers representing the body. Our strategy +// is to read into our buffer and return it until we have +// read through the whole file. +// +template +auto +basic_file_body:: +reader:: +get(error_code& ec) -> + boost::optional> +{ + // Calculate the smaller of our buffer size, + // or the amount of unread data in the file. + auto const amount = remain_ > sizeof(buf_) ? + sizeof(buf_) : static_cast(remain_); + + // Handle the case where the file is zero length + if(amount == 0) + { + // Modify the error code to indicate success + // This is required by the error_code specification. + // + // NOTE We use the existing category instead of calling + // into the library to get the generic category because + // that saves us a possibly expensive atomic operation. + // + ec.assign(0, ec.category()); + return boost::none; + } + + // Now read the next buffer + auto const nread = body_.file_.read(buf_, amount, ec); + if(ec) + return boost::none; + + // Make sure there is forward progress + BOOST_ASSERT(nread != 0); + BOOST_ASSERT(nread <= remain_); + + // Update the amount remaining based on what we got + remain_ -= nread; + + // Return the buffer to the caller. + // + // The second element of the pair indicates whether or + // not there is more data. As long as there is some + // unread bytes, there will be more data. Otherwise, + // we set this bool to `false` so we will not be called + // again. + // + ec.assign(0, ec.category()); + return {{ + const_buffers_type{buf_, nread}, // buffer to return. + remain_ > 0 // `true` if there are more buffers. + }}; +} + +//] + +//[example_http_file_body_5 + +template +class basic_file_body::writer +{ + value_type& body_; // The body we are writing to + +public: + // Constructor. + // + // This is called after the header is parsed and + // indicates that a non-zero sized body may be present. + // `m` holds the message we are receiving, which will + // always have the `file_body` as the body type. + // + template + explicit + writer( + message& m, + boost::optional const& content_length, + error_code& ec); + + // This function is called one or more times to store + // buffer sequences corresponding to the incoming body. + // + template + std::size_t + put(ConstBufferSequence const& buffers, + error_code& ec); + + // This function is called when writing is complete. + // It is an opportunity to perform any final actions + // which might fail, in order to return an error code. + // Operations that might fail should not be attemped in + // destructors, since an exception thrown from there + // would terminate the program. + // + void + finish(error_code& ec); +}; + +//] + +//[example_http_file_body_6 + +// We don't do much in the writer constructor since the +// file is already open. +// +template +template +basic_file_body:: +writer:: +writer( + message& m, + boost::optional const& content_length, + error_code& ec) + : body_(m.body) +{ + // The file must already be open for writing + BOOST_ASSERT(body_.file_.is_open()); + + // We don't do anything with this but a sophisticated + // application might check available space on the device + // to see if there is enough room to store the body. + boost::ignore_unused(content_length); + + // This is required by the error_code specification + ec.assign(0, ec.category()); +} + +// This will get called one or more times with body buffers +// +template +template +std::size_t +basic_file_body:: +writer:: +put(ConstBufferSequence const& buffers, error_code& ec) +{ + // This function must return the total number of + // bytes transferred from the input buffers. + std::size_t nwritten = 0; + + // Loop over all the buffers in the sequence, + // and write each one to the file. + for(boost::asio::const_buffer buffer : buffers) + { + // Write this buffer to the file + nwritten += body_.file_.write( + boost::asio::buffer_cast(buffer), + boost::asio::buffer_size(buffer), + ec); + if(ec) + return nwritten; + } + + // Indicate success + // This is required by the error_code specification + ec.assign(0, ec.category()); + + return nwritten; +} + +// Called after writing is done when there's no error. +template +void +basic_file_body:: +writer:: +finish(error_code& ec) +{ + // This has to be cleared before returning, to + // indicate no error. The specification requires it. + ec.assign(0, ec.category()); +} + +//] + +/// A message body represented by a file on the filesystem. +using file_body = basic_file_body; + +} // http +} // beast + +#endif diff --git a/test/http/CMakeLists.txt b/test/http/CMakeLists.txt index 670ef3ae..743b3a48 100644 --- a/test/http/CMakeLists.txt +++ b/test/http/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable (http-tests error.cpp field.cpp fields.cpp + file_body.cpp message.cpp parser.cpp read.cpp diff --git a/test/http/Jamfile b/test/http/Jamfile index 9a87fdcd..0c059509 100644 --- a/test/http/Jamfile +++ b/test/http/Jamfile @@ -15,6 +15,7 @@ unit-test http-tests : error.cpp field.cpp fields.cpp + file_body.cpp message.cpp parser.cpp read.cpp diff --git a/test/http/doc_examples.cpp b/test/http/doc_examples.cpp index e800a899..3d13b4d5 100644 --- a/test/http/doc_examples.cpp +++ b/test/http/doc_examples.cpp @@ -6,7 +6,6 @@ // #include "example/doc/http_examples.hpp" -#include "example/common/file_body.hpp" #include "example/common/const_body.hpp" #include "example/common/mutable_body.hpp" @@ -286,62 +285,6 @@ public: BEAST_EXPECT(h.body == "Hello, world!"); } - //-------------------------------------------------------------------------- - - void - doFileBody() - { - test::pipe c{ios_}; - - boost::filesystem::path const path = "temp.txt"; - std::string const body = "Hello, world!\n"; - { - request req; - req.version = 11; - req.method(verb::put); - req.target("/"); - req.body = body; - req.prepare_payload(); - write(c.client, req); - } - { - flat_buffer b; - request_parser p0; - read_header(c.server, b, p0); - BEAST_EXPECTS(p0.get().method() == verb::put, - p0.get().method_string()); - { - error_code ec; - request_parser p{std::move(p0)}; - p.get().body.open(path, "wb", ec); - if(ec) - BOOST_THROW_EXCEPTION(system_error{ec}); - read(c.server, b, p); - } - } - { - error_code ec; - response res; - res.version = 11; - res.result(status::ok); - res.insert(field::server, "test"); - res.body.open(path, "rb", ec); - if(ec) - BOOST_THROW_EXCEPTION(system_error{ec}); - res.set(field::content_length, res.body.size()); - write(c.server, res); - } - { - flat_buffer b; - response res; - read(c.client, b, res); - BEAST_EXPECTS(res.body == body, body); - } - error_code ec; - boost::filesystem::remove(path, ec); - BEAST_EXPECTS(! ec, ec.message()); - } - void doConstAndMutableBody() { @@ -424,7 +367,6 @@ public: doCustomParser(); doHEAD(); doDeferredBody(); - doFileBody(); doConstAndMutableBody(); doIncrementalRead(); } diff --git a/test/http/file_body.cpp b/test/http/file_body.cpp new file mode 100644 index 00000000..f04af503 --- /dev/null +++ b/test/http/file_body.cpp @@ -0,0 +1,106 @@ +// +// Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// 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) +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include +#include +#include + +namespace beast { +namespace http { + +class file_body_test : public beast::unit_test::suite +{ +public: + struct lambda + { + flat_buffer buffer; + + template + void + operator()(error_code&, ConstBufferSequence const& buffers) + { + buffer.commit(boost::asio::buffer_copy( + buffer.prepare(boost::asio::buffer_size(buffers)), + buffers)); + } + }; + + template + void + doTestFileBody() + { + error_code ec; + string_view const s = + "HTTP/1.1 200 OK\r\n" + "Server: test\r\n" + "Content-Length: 3\r\n" + "\r\n" + "xyz"; + auto const temp = boost::filesystem::unique_path(); + { + response_parser> p; + p.eager(true); + + p.get().body.open( + temp.string().c_str(), file_mode::write, ec); + BEAST_EXPECTS(! ec, ec.message()); + + p.put(boost::asio::buffer(s.data(), s.size()), ec); + BEAST_EXPECTS(! ec, ec.message()); + } + { + File f; + f.open(temp.string().c_str(), file_mode::read, ec); + auto size = f.size(ec); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECT(size == 3); + std::string s1; + s1.resize(3); + f.read(&s1[0], s1.size(), ec); + BEAST_EXPECTS(! ec, ec.message()); + BEAST_EXPECTS(s1 == "xyz", s); + } + { + lambda visit; + { + response> res{status::ok, 11}; + res.set(field::server, "test"); + res.body.open(temp.string().c_str(), + file_mode::scan, ec); + BEAST_EXPECTS(! ec, ec.message()); + res.prepare_payload(); + + serializer, fields> sr{res}; + sr.next(ec, visit); + BEAST_EXPECTS(! ec, ec.message()); + auto const cb = *visit.buffer.data().begin(); + string_view const s1{ + boost::asio::buffer_cast(cb), + boost::asio::buffer_size(cb)}; + BEAST_EXPECTS(s1 == s, s1); + } + } + boost::filesystem::remove(temp, ec); + BEAST_EXPECTS(! ec, ec.message()); + } + void + run() override + { + doTestFileBody(); + } +}; + +BEAST_DEFINE_TESTSUITE(file_body,http,beast); + +} // http +} // beast