diff --git a/CHANGELOG.md b/CHANGELOG.md index 5072297b..bd010d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Version 80: * Add basic_dynamic_body.hpp * Shrink buffer_prefix_view * Remove unused file_path +* Add basic_file_body.hpp -------------------------------------------------------------------------------- diff --git a/doc/0_main.qbk b/doc/0_main.qbk index 90de1b15..91b6c803 100644 --- a/doc/0_main.qbk +++ b/doc/0_main.qbk @@ -84,7 +84,7 @@ [import ../example/http-client/http_client.cpp] [import ../example/websocket-client/websocket_client.cpp] -[import ../include/beast/http/file_body.hpp] +[import ../include/beast/http/basic_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 6101fdc4..df82773f 100644 --- a/doc/5_02_message.qbk +++ b/doc/5_02_message.qbk @@ -146,6 +146,8 @@ meet the requirements, or use the ones that come with the library: ]] [[ [link beast.ref.beast__http__file_body `file_body`] + + [link beast.ref.beast__http__basic_file_body `basic_file_body`] ][ This body is represented by a file opened for either reading or writing. Messages with this body may be serialized and parsed. diff --git a/doc/quickref.xml b/doc/quickref.xml index 3a0f5961..569c27cd 100644 --- a/doc/quickref.xml +++ b/doc/quickref.xml @@ -30,14 +30,15 @@ Classes basic_dynamic_body - basic_file_body basic_fields + basic_file_body basic_parser basic_string_body buffer_body dynamic_body empty_body fields + file_body header message parser diff --git a/include/beast/http/basic_file_body.hpp b/include/beast/http/basic_file_body.hpp new file mode 100644 index 00000000..8fcc2916 --- /dev/null +++ b/include/beast/http/basic_file_body.hpp @@ -0,0 +1,528 @@ +// +// 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_BASIC_FILE_BODY_HPP +#define BEAST_HTTP_BASIC_FILE_BODY_HPP + +#include +#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 +{ + // Make sure the type meets the requirements + 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. + class reader; + + // Algorithm for storing buffers when parsing. + class writer; + + // The type of the @ref message::body member. + class value_type; + + /** Returns the size of the body + + @param body The file body to use + */ + static + std::uint64_t + size(value_type const& body); +}; + +//] + +//[example_http_file_body_2 + +/** 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. +*/ +template +class basic_file_body::value_type +{ + // This body container holds a handle to the file + // when it is open, and also caches the size when set. + + 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& body) +{ + // Forward the call to the body + return body.size(); +} + +//] + +//[example_http_file_body_3 + +/** Algorithm for retrieving buffers when serializing. + + Objects of this type are created during serialization + to extract the buffers representing the body. +*/ +template +class basic_file_body::reader +{ + value_type& 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. + // + // Note that the message is passed by non-const reference. + // This is intentional, because reading from the file + // changes its "current position" which counts makes the + // operation logically not-const (although it is bitwise + // const). + // + // The BodyReader concept allows the reader to choose + // whether to take the message by const reference or + // non-const reference. Depending on the choice, a + // serializer constructed using that body type will + // require the same const or non-const reference to + // construct. + // + // Readers which accept const messages usually allow + // the same body to be serialized by multiple threads + // concurrently, while readers accepting non-const + // messages may only be serialized by one thread at + // a time. + // + template + reader(message< + isRequest, basic_file_body, Fields>& m); + + // Initializer + // + // This is called before the body is serialized and + // gives the reader a chance to do something that might + // need to return an error code. + // + void + init(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& m) + : 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_; +} + +// Initializer +template +void +basic_file_body:: +reader:: +init(error_code& ec) +{ + // The error_code specification requires that we + // either set the error to some value, or set it + // to indicate no error. + // + // We don't do anything fancy so set "no error" + ec.assign(0, ec.category()); +} + +// 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 + +/** Algorithm for storing buffers when parsing. + + Objects of this type are created during parsing + to store incoming buffers representing the body. +*/ +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); + + // Initializer + // + // This is called before the body is parsed and + // gives the writer a chance to do something that might + // need to return an error code. It informs us of + // the payload size (`content_length`) which we can + // optionally use for optimization. + // + void + init(boost::optional const&, 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) + : body_(m.body) +{ +} + +template +void +basic_file_body:: +writer:: +init( + boost::optional const& content_length, + error_code& ec) +{ + // 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); + + // The error_code specification requires that we + // either set the error to some value, or set it + // to indicate no error. + // + // We don't do anything fancy so set "no error" + 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()); +} + +//] + +#if ! BEAST_DOXYGEN +// operator<< is not supported for file_body +template +std::ostream& +operator<<(std::ostream& os, message< + isRequest, basic_file_body, Fields> const& msg) = delete; +#endif + +} // http +} // beast + +#endif diff --git a/include/beast/http/file_body.hpp b/include/beast/http/file_body.hpp index 2986c904..7e98643d 100644 --- a/include/beast/http/file_body.hpp +++ b/include/beast/http/file_body.hpp @@ -8,11 +8,8 @@ #ifndef BEAST_HTTP_FILE_BODY_HPP #define BEAST_HTTP_FILE_BODY_HPP -#include -#include #include -#include -#include +#include #include #include #include @@ -23,505 +20,9 @@ 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& 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. - // - // Note that the message is passed by non-const reference. - // This is intentional, because reading from the file - // changes its "current position" which counts makes the - // operation logically not-const (although it is bitwise - // const). - // - // The BodyReader concept allows the reader to choose - // whether to take the message by const reference or - // non-const reference. Depending on the choice, a - // serializer constructed using that body type will - // require the same const or non-const reference to - // construct. - // - // Readers which accept const messages usually allow - // the same body to be serialized by multiple threads - // concurrently, while readers accepting non-const - // messages may only be serialized by one thread at - // a time. - // - template - reader(message< - isRequest, basic_file_body, Fields>& m); - - // Initializer - // - // This is called before the body is serialized and - // gives the reader a chance to do something that might - // need to return an error code. - // - void - init(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& m) - : 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_; -} - -// Initializer -template -void -basic_file_body:: -reader:: -init(error_code& ec) -{ - // The error_code specification requires that we - // either set the error to some value, or set it - // to indicate no error. - // - // We don't do anything fancy so set "no error" - ec.assign(0, ec.category()); -} - -// 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); - - // Initializer - // - // This is called before the body is parsed and - // gives the writer a chance to do something that might - // need to return an error code. It informs us of - // the payload size (`content_length`) which we can - // optionally use for optimization. - // - void - init(boost::optional const&, 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) - : body_(m.body) -{ -} - -template -void -basic_file_body:: -writer:: -init( - boost::optional const& content_length, - error_code& ec) -{ - // 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); - - // The error_code specification requires that we - // either set the error to some value, or set it - // to indicate no error. - // - // We don't do anything fancy so set "no error" - 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; -#if ! BEAST_DOXYGEN -// operator<< is not supported for file_body -template -std::ostream& -operator<<(std::ostream& os, message< - isRequest, basic_file_body, Fields> const& msg) = delete; -#endif - } // http } // beast diff --git a/test/http/CMakeLists.txt b/test/http/CMakeLists.txt index b74e0eb6..74628edb 100644 --- a/test/http/CMakeLists.txt +++ b/test/http/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable (http-tests test_parser.hpp ../../extras/beast/unit_test/main.cpp basic_dynamic_body.cpp + basic_file_body.cpp basic_parser.cpp buffer_body.cpp doc_examples.cpp diff --git a/test/http/Jamfile b/test/http/Jamfile index 2145d832..4c99df1c 100644 --- a/test/http/Jamfile +++ b/test/http/Jamfile @@ -8,6 +8,7 @@ unit-test http-tests : ../../extras/beast/unit_test/main.cpp basic_dynamic_body.cpp + basic_file_body.cpp basic_parser.cpp buffer_body.cpp doc_examples.cpp diff --git a/test/http/basic_file_body.cpp b/test/http/basic_file_body.cpp new file mode 100644 index 00000000..c6047ffa --- /dev/null +++ b/test/http/basic_file_body.cpp @@ -0,0 +1,9 @@ +// +// 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