From f1ad97b2276e14951507d6be896276cd0a8aace8 Mon Sep 17 00:00:00 2001 From: Chunting Gu Date: Thu, 5 Sep 2019 10:53:39 +0800 Subject: [PATCH] Rework book example to support upload book photo. --- examples/common/book.h | 7 +- examples/rest_book_client.cc | 86 ++++++++--- examples/rest_book_server.cc | 268 ++++++++++++++++++++--------------- webcc/response_builder.cc | 12 ++ webcc/response_builder.h | 5 + 5 files changed, 243 insertions(+), 135 deletions(-) diff --git a/examples/common/book.h b/examples/common/book.h index f17b886..2dea1fa 100644 --- a/examples/common/book.h +++ b/examples/common/book.h @@ -4,6 +4,8 @@ #include #include +#include "boost/filesystem/path.hpp" + // In-memory test data. // There should be some database in a real product. @@ -11,6 +13,7 @@ struct Book { std::string id; std::string title; double price; + boost::filesystem::path photo; bool IsNull() const { return id.empty(); } }; @@ -21,7 +24,9 @@ extern const Book kNullBook; class BookStore { public: - const std::list& books() const { return books_; } + const std::list& books() const { + return books_; + } const Book& GetBook(const std::string& id) const; diff --git a/examples/rest_book_client.cc b/examples/rest_book_client.cc index 8102218..f69cc9c 100644 --- a/examples/rest_book_client.cc +++ b/examples/rest_book_client.cc @@ -1,6 +1,9 @@ #include #include +#include "boost/algorithm/string/predicate.hpp" +#include "boost/filesystem/operations.hpp" + #include "json/json.h" #include "webcc/client_session.h" @@ -17,6 +20,8 @@ #endif #endif +namespace bfs = boost::filesystem; + // ----------------------------------------------------------------------------- class BookClient { @@ -27,7 +32,8 @@ public: bool ListBooks(std::list* books); - bool CreateBook(const std::string& title, double price, std::string* id); + bool CreateBook(const std::string& title, double price, + const bfs::path& photo, std::string* id); bool GetBook(const std::string& id, Book* book); @@ -37,6 +43,8 @@ public: bool DeleteBook(const std::string& id); private: + bool CheckPhoto(const bfs::path& photo); + // Check HTTP response status. bool CheckStatus(webcc::ResponsePtr response, int expected_status); @@ -83,15 +91,14 @@ bool BookClient::ListBooks(std::list* books) { } bool BookClient::CreateBook(const std::string& title, double price, - std::string* id) { + const bfs::path& photo, std::string* id) { Json::Value req_json; req_json["title"] = title; req_json["price"] = price; try { auto r = session_.Send(WEBCC_POST(url_).Path("books"). - Body(JsonToString(req_json)) - ()); + Body(JsonToString(req_json))()); if (!CheckStatus(r, webcc::Status::kCreated)) { return false; @@ -100,7 +107,20 @@ bool BookClient::CreateBook(const std::string& title, double price, Json::Value rsp_json = StringToJson(r->data()); *id = rsp_json["id"].asString(); - return !id->empty(); + if (id->empty()) { + return false; + } + + if (CheckPhoto(photo)) { + r = session_.Send(WEBCC_PUT(url_).Path("books").Path(*id).Path("photo"). + File(photo)()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + } + + return true; } catch (const webcc::Error& error) { std::cerr << error << std::endl; @@ -132,8 +152,7 @@ bool BookClient::UpdateBook(const std::string& id, const std::string& title, try { auto r = session_.Send(WEBCC_PUT(url_).Path("books").Path(id). - Body(JsonToString(json)) - ()); + Body(JsonToString(json))()); if (!CheckStatus(r, webcc::Status::kOK)) { return false; @@ -163,6 +182,23 @@ bool BookClient::DeleteBook(const std::string& id) { } } +bool BookClient::CheckPhoto(const bfs::path& photo) { + if (photo.empty()) { + return false; + } + + if (!bfs::is_regular_file(photo) || !bfs::exists(photo)) { + return false; + } + + auto ext = photo.extension().string(); + if (!boost::iequals(ext, ".jpg") && !boost::iequals(ext, ".jpeg")) { + return false; + } + + return true; +} + bool BookClient::CheckStatus(webcc::ResponsePtr response, int expected_status) { if (response->status() != expected_status) { LOG_ERRO("HTTP status error (actual: %d, expected: %d).", @@ -194,40 +230,38 @@ void PrintBookList(const std::list& books) { int main(int argc, char* argv[]) { if (argc < 2) { - std::cout << "usage: rest_book_client [timeout]" << std::endl; - std::cout << std::endl; + std::cout << "usage: rest_book_client " << std::endl; std::cout << "examples:" << std::endl; std::cout << " $ rest_book_client http://localhost:8080" << std::endl; - std::cout << " $ rest_book_client http://localhost:8080 2" << std::endl; return 1; } std::string url = argv[1]; - int timeout = 0; - if (argc > 2) { - timeout = std::atoi(argv[2]); - } - WEBCC_LOG_INIT("", webcc::LOG_CONSOLE_FILE_OVERWRITE); - BookClient client(url, timeout); + BookClient client(url); PrintSeparator(); + // List all books. + std::list books; if (client.ListBooks(&books)) { PrintBookList(books); + } else { + return 1; } PrintSeparator(); + // Create a new book. + std::string id; - if (client.CreateBook("1984", 12.3, &id)) { + if (client.CreateBook("1984", 12.3, "", &id)) { std::cout << "Book ID: " << id << std::endl; } else { - id = "1"; - std::cout << "Book ID: " << id << " (faked)"<< std::endl; + return 1; } PrintSeparator(); @@ -235,6 +269,8 @@ int main(int argc, char* argv[]) { books.clear(); if (client.ListBooks(&books)) { PrintBookList(books); + } else { + return 1; } PrintSeparator(); @@ -242,21 +278,29 @@ int main(int argc, char* argv[]) { Book book; if (client.GetBook(id, &book)) { PrintBook(book); + } else { + return 1; } PrintSeparator(); - client.UpdateBook(id, "1Q84", 32.1); + if (!client.UpdateBook(id, "1Q84", 32.1)) { + return 1; + } PrintSeparator(); if (client.GetBook(id, &book)) { PrintBook(book); + } else { + return 1; } PrintSeparator(); - client.DeleteBook(id); + if (!client.DeleteBook(id)) { + return 1; + } PrintSeparator(); diff --git a/examples/rest_book_server.cc b/examples/rest_book_server.cc index cd80dc7..e8281c7 100644 --- a/examples/rest_book_server.cc +++ b/examples/rest_book_server.cc @@ -1,8 +1,7 @@ #include -#include #include -#include -#include + +#include "boost/filesystem/operations.hpp" #include "json/json.h" @@ -21,24 +20,18 @@ #endif #endif +namespace bfs = boost::filesystem; + // ----------------------------------------------------------------------------- static BookStore g_book_store; -static void Sleep(int seconds) { - if (seconds > 0) { - LOG_INFO("Sleep %d seconds...", seconds); - std::this_thread::sleep_for(std::chrono::seconds(seconds)); - } -} - // ----------------------------------------------------------------------------- +// BookListView +// URL: /books class BookListView : public webcc::View { public: - explicit BookListView(int sleep_seconds) : sleep_seconds_(sleep_seconds) { - } - webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { return Get(request); @@ -53,163 +46,207 @@ public: private: // Get a list of books based on query parameters. - webcc::ResponsePtr Get(webcc::RequestPtr request); + webcc::ResponsePtr Get(webcc::RequestPtr request) { + Json::Value json(Json::arrayValue); - // Create a new book. - webcc::ResponsePtr Post(webcc::RequestPtr request); + for (const Book& book : g_book_store.books()) { + json.append(BookToJson(book)); + } -private: - // Sleep some seconds before send back the response. - // For testing timeout control in client side. - int sleep_seconds_; + // Return all books as a JSON array. + + return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json().Utf8()(); + } + + // Create a new book. + webcc::ResponsePtr Post(webcc::RequestPtr request) { + Book book; + if (JsonStringToBook(request->data(), &book)) { + std::string id = g_book_store.AddBook(book); + + Json::Value json; + json["id"] = id; + + return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)).Json().Utf8()(); + } else { + // Invalid JSON + return webcc::ResponseBuilder{}.BadRequest()(); + } + } }; // ----------------------------------------------------------------------------- +// BookDetailView -// The URL is like '/books/{BookID}', and the 'args' parameter -// contains the matched book ID. +// URL: /books/{id} class BookDetailView : public webcc::View { public: - explicit BookDetailView(int sleep_seconds) : sleep_seconds_(sleep_seconds) { - } - webcc::ResponsePtr Handle(webcc::RequestPtr request) override { if (request->method() == "GET") { return Get(request); } - if (request->method() == "PUT") { return Put(request); } - if (request->method() == "DELETE") { return Delete(request); } - return {}; } private: // Get the detailed information of a book. - webcc::ResponsePtr Get(webcc::RequestPtr request); + webcc::ResponsePtr Get(webcc::RequestPtr request) { + if (request->args().size() != 1) { + // NotFound means the resource specified by the URL cannot be found. + // BadRequest could be another choice. + return webcc::ResponseBuilder{}.NotFound()(); + } - // Update a book. - webcc::ResponsePtr Put(webcc::RequestPtr request); + const std::string& id = request->args()[0]; - // Delete a book. - webcc::ResponsePtr Delete(webcc::RequestPtr request); + const Book& book = g_book_store.GetBook(id); + if (book.IsNull()) { + return webcc::ResponseBuilder{}.NotFound()(); + } -private: - // Sleep some seconds before send back the response. - // For testing timeout control in client side. - int sleep_seconds_; -}; + return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).Json().Utf8()(); + } -// ----------------------------------------------------------------------------- + // Update a book. + webcc::ResponsePtr Put(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } -// Return all books as a JSON array. -webcc::ResponsePtr BookListView::Get(webcc::RequestPtr request) { - Sleep(sleep_seconds_); + const std::string& id = request->args()[0]; - Json::Value json(Json::arrayValue); + Book book; + if (!JsonStringToBook(request->data(), &book)) { + return webcc::ResponseBuilder{}.BadRequest()(); + } - for (const Book& book : g_book_store.books()) { - json.append(BookToJson(book)); - } + book.id = id; + g_book_store.UpdateBook(book); - return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json(). - Utf8()(); -} + return webcc::ResponseBuilder{}.OK()(); + } -webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) { - Sleep(sleep_seconds_); + // Delete a book. + webcc::ResponsePtr Delete(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } - Book book; - if (JsonStringToBook(request->data(), &book)) { - std::string id = g_book_store.AddBook(book); + const std::string& id = request->args()[0]; - Json::Value json; - json["id"] = id; + if (!g_book_store.DeleteBook(id)) { + return webcc::ResponseBuilder{}.NotFound()(); + } - return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)). - Json().Utf8()(); - } else { - // Invalid JSON - return webcc::ResponseBuilder{}.BadRequest()(); + return webcc::ResponseBuilder{}.OK()(); } -} +}; // ----------------------------------------------------------------------------- +// BookPhotoView -webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { - Sleep(sleep_seconds_); - - if (request->args().size() != 1) { - // NotFound means the resource specified by the URL cannot be found. - // BadRequest could be another choice. - return webcc::ResponseBuilder{}.NotFound()(); +// URL: /books/{id}/photo +class BookPhotoView : public webcc::View { +public: + explicit BookPhotoView(bfs::path upload_dir) + : upload_dir_(std::move(upload_dir)) { } - const std::string& book_id = request->args()[0]; + webcc::ResponsePtr Handle(webcc::RequestPtr request) override { + if (request->method() == "GET") { + return Get(request); + } - const Book& book = g_book_store.GetBook(book_id); - if (book.IsNull()) { - return webcc::ResponseBuilder{}.NotFound()(); - } + if (request->method() == "PUT") { + return Put(request); + } - return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)). - Json().Utf8()(); -} + if (request->method() == "DELETE") { + return Delete(request); + } -webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) { - Sleep(sleep_seconds_); + return {}; + } - if (request->args().size() != 1) { - return webcc::ResponseBuilder{}.NotFound()(); + // Stream the request data, an image, of PUT into a temp file. + bool Stream(const std::string& method) override { + return method == "PUT"; } - const std::string& book_id = request->args()[0]; +private: + // Get the photo of the book. + // TODO: Check content type to see if it's JPEG. + webcc::ResponsePtr Get(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + const std::string& id = request->args()[0]; + const Book& book = g_book_store.GetBook(id); + if (book.IsNull()) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + bfs::path photo_path = GetPhotoPath(id); + if (!bfs::exists(photo_path)) { + return webcc::ResponseBuilder{}.NotFound()(); + } - Book book; - if (!JsonStringToBook(request->data(), &book)) { - return webcc::ResponseBuilder{}.BadRequest()(); + // File() might throw Error::kFileError. + // TODO: Avoid exception handling. + try { + return webcc::ResponseBuilder{}.OK().File(photo_path)(); + } catch (const webcc::Error&) { + return webcc::ResponseBuilder{}.NotFound()(); + } } - book.id = book_id; - g_book_store.UpdateBook(book); + // Set the photo of the book. + // TODO: Check content type to see if it's JPEG. + webcc::ResponsePtr Put(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } - return webcc::ResponseBuilder{}.OK()(); -} + const std::string& id = request->args()[0]; + + const Book& book = g_book_store.GetBook(id); + if (book.IsNull()) { + return webcc::ResponseBuilder{}.NotFound()(); + } -webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) { - Sleep(sleep_seconds_); + request->file_body()->Move(GetPhotoPath(id)); - if (request->args().size() != 1) { - return webcc::ResponseBuilder{}.NotFound()(); + return webcc::ResponseBuilder{}.OK()(); } - const std::string& book_id = request->args()[0]; + // Delete the photo of the book. + webcc::ResponsePtr Delete(webcc::RequestPtr request) { + return {}; + } - if (!g_book_store.DeleteBook(book_id)) { - return webcc::ResponseBuilder{}.NotFound()(); +private: + bfs::path GetPhotoPath(const std::string& book_id) const { + return upload_dir_ / "book_photo" / (book_id + ".jpg"); } - return webcc::ResponseBuilder{}.OK()(); -} +private: + bfs::path upload_dir_; +}; // ----------------------------------------------------------------------------- int main(int argc, char* argv[]) { - if (argc < 2) { - std::cout << "usage: rest_book_server [seconds]" << std::endl; - std::cout << std::endl; - std::cout << "If |seconds| is provided, the server will sleep, for testing " - << "timeout, before " << std::endl - << "send back each response." << std::endl; - std::cout << std::endl; + if (argc < 3) { + std::cout << "usage: rest_book_server " << std::endl; std::cout << "examples:" << std::endl; - std::cout << " $ rest_book_server 8080" << std::endl; - std::cout << " $ rest_book_server 8080 3" << std::endl; + std::cout << " $ rest_book_server 8080 D:/upload" << std::endl; return 1; } @@ -217,20 +254,25 @@ int main(int argc, char* argv[]) { std::uint16_t port = static_cast(std::atoi(argv[1])); - int sleep_seconds = 0; - if (argc >= 3) { - sleep_seconds = std::atoi(argv[2]); + bfs::path upload_dir = argv[2]; + if (!bfs::is_directory(upload_dir) || !bfs::exists(upload_dir)) { + std::cerr << "Invalid upload dir!" << std::endl; + return 1; } try { - webcc::Server server(port); + webcc::Server server(port); // No doc root server.Route("/books", - std::make_shared(sleep_seconds), + std::make_shared(), { "GET", "POST" }); server.Route(webcc::R("/books/(\\d+)"), - std::make_shared(sleep_seconds), + std::make_shared(), + { "GET", "PUT", "DELETE" }); + + server.Route(webcc::R("/books/(\\d+)/photo"), + std::make_shared(upload_dir), { "GET", "PUT", "DELETE" }); server.Run(2); diff --git a/webcc/response_builder.cc b/webcc/response_builder.cc index 1272601..e81c034 100644 --- a/webcc/response_builder.cc +++ b/webcc/response_builder.cc @@ -43,6 +43,18 @@ ResponsePtr ResponseBuilder::operator()() { return response; } +ResponseBuilder& ResponseBuilder::File(const webcc::Path& path, + bool infer_media_type, + std::size_t chunk_size) { + body_.reset(new FileBody{ path, chunk_size }); + + if (infer_media_type) { + media_type_ = media_types::FromExtension(path.extension().string()); + } + + return *this; +} + ResponseBuilder& ResponseBuilder::Date() { headers_.push_back(headers::kDate); headers_.push_back(utility::GetTimestamp()); diff --git a/webcc/response_builder.h b/webcc/response_builder.h index fb15173..74d71bb 100644 --- a/webcc/response_builder.h +++ b/webcc/response_builder.h @@ -92,6 +92,11 @@ public: return *this; } + // Use the file content as body. + // NOTE: Error::kFileError might be thrown. + ResponseBuilder& File(const webcc::Path& path, bool infer_media_type = true, + std::size_t chunk_size = 1024); + ResponseBuilder& Header(const std::string& key, const std::string& value) { headers_.push_back(key); headers_.push_back(value);