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 @@
Classesbasic_dynamic_body
+ basic_file_bodybasic_fieldsbasic_parserbuffer_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