From 2a25340ce4400b604c4e83868597f6c453eaf964 Mon Sep 17 00:00:00 2001 From: Chunting Gu Date: Fri, 6 Sep 2019 17:40:46 +0800 Subject: [PATCH] Rework book server and client. --- examples/CMakeLists.txt | 20 +- examples/book_client/CMakeLists.txt | 12 + examples/book_client/book.cc | 11 + examples/book_client/book.h | 22 ++ examples/book_client/book_client.cc | 194 ++++++++++++ examples/book_client/book_client.h | 49 +++ examples/book_client/book_json.cc | 59 ++++ examples/book_client/book_json.h | 22 ++ examples/book_client/main.cc | 141 +++++++++ examples/book_client/photo/1984.jpg | Bin 0 -> 60536 bytes examples/book_client/photo/1Q84.jpg | Bin 0 -> 13489 bytes .../{ => book_client}/rest_book_client.cc | 0 examples/book_server/CMakeLists.txt | 14 + examples/book_server/book.cc | 11 + examples/book_server/book.h | 24 ++ examples/book_server/book_db.cc | 66 ++++ examples/book_server/book_db.h | 44 +++ examples/book_server/book_json.cc | 57 ++++ examples/book_server/book_json.h | 22 ++ examples/book_server/main.cc | 68 +++++ examples/book_server/views.cc | 223 ++++++++++++++ examples/book_server/views.h | 76 +++++ examples/rest_book_server.cc | 286 ------------------ .../{file_server.cc => static_file_server.cc} | 0 24 files changed, 1120 insertions(+), 301 deletions(-) create mode 100644 examples/book_client/CMakeLists.txt create mode 100644 examples/book_client/book.cc create mode 100644 examples/book_client/book.h create mode 100644 examples/book_client/book_client.cc create mode 100644 examples/book_client/book_client.h create mode 100644 examples/book_client/book_json.cc create mode 100644 examples/book_client/book_json.h create mode 100644 examples/book_client/main.cc create mode 100644 examples/book_client/photo/1984.jpg create mode 100644 examples/book_client/photo/1Q84.jpg rename examples/{ => book_client}/rest_book_client.cc (100%) create mode 100644 examples/book_server/CMakeLists.txt create mode 100644 examples/book_server/book.cc create mode 100644 examples/book_server/book.h create mode 100644 examples/book_server/book_db.cc create mode 100644 examples/book_server/book_db.h create mode 100644 examples/book_server/book_json.cc create mode 100644 examples/book_server/book_json.h create mode 100644 examples/book_server/main.cc create mode 100644 examples/book_server/views.cc create mode 100644 examples/book_server/views.h delete mode 100644 examples/rest_book_server.cc rename examples/{file_server.cc => static_file_server.cc} (100%) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 68f3018..63ffcb5 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -29,13 +29,6 @@ if(UNIX) set(EXAMPLE_LIBS ${EXAMPLE_LIBS} ${CMAKE_DL_LIBS}) endif() -set(REST_BOOK_SRCS - common/book.cc - common/book.h - common/book_json.cc - common/book_json.h - ) - add_executable(client_basics client_basics.cc) target_link_libraries(client_basics ${EXAMPLE_LIBS}) @@ -47,14 +40,8 @@ endif() add_executable(hello_world_server hello_world_server.cc) target_link_libraries(hello_world_server ${EXAMPLE_LIBS}) -add_executable(rest_book_server rest_book_server.cc ${REST_BOOK_SRCS}) -target_link_libraries(rest_book_server ${EXAMPLE_LIBS} jsoncpp) - -add_executable(rest_book_client rest_book_client.cc ${REST_BOOK_SRCS}) -target_link_libraries(rest_book_client ${EXAMPLE_LIBS} jsoncpp) - -add_executable(file_server file_server.cc) -target_link_libraries(file_server ${EXAMPLE_LIBS}) +add_executable(static_file_server static_file_server.cc) +target_link_libraries(static_file_server ${EXAMPLE_LIBS}) add_executable(file_downloader file_downloader.cc) target_link_libraries(file_downloader ${EXAMPLE_LIBS}) @@ -67,3 +54,6 @@ target_link_libraries(form_client ${EXAMPLE_LIBS}) add_executable(form_server form_server.cc) target_link_libraries(form_server ${EXAMPLE_LIBS}) + +add_subdirectory(book_server) +add_subdirectory(book_client) diff --git a/examples/book_client/CMakeLists.txt b/examples/book_client/CMakeLists.txt new file mode 100644 index 0000000..efd14b5 --- /dev/null +++ b/examples/book_client/CMakeLists.txt @@ -0,0 +1,12 @@ +set(SRCS + book.cc + book.h + book_json.cc + book_json.h + book_client.cc + book_client.h + main.cc + ) + +add_executable(book_client ${SRCS}) +target_link_libraries(book_client ${EXAMPLE_LIBS} jsoncpp) diff --git a/examples/book_client/book.cc b/examples/book_client/book.cc new file mode 100644 index 0000000..9544d82 --- /dev/null +++ b/examples/book_client/book.cc @@ -0,0 +1,11 @@ +#include "book.h" + +#include + +const Book kNullBook{}; + +std::ostream& operator<<(std::ostream& os, const Book& book) { + os << "{ " << book.id << ", " << book.title << ", " << book.price << ", " + << book.photo << " }"; + return os; +} diff --git a/examples/book_client/book.h b/examples/book_client/book.h new file mode 100644 index 0000000..e83437e --- /dev/null +++ b/examples/book_client/book.h @@ -0,0 +1,22 @@ +#ifndef BOOK_H_ +#define BOOK_H_ + +#include +#include + +struct Book { + std::string id; + std::string title; + double price; + std::string photo; // Name only + + bool IsNull() const { + return id.empty(); + } +}; + +std::ostream& operator<<(std::ostream& os, const Book& book); + +extern const Book kNullBook; + +#endif // BOOK_H_ diff --git a/examples/book_client/book_client.cc b/examples/book_client/book_client.cc new file mode 100644 index 0000000..00f570d --- /dev/null +++ b/examples/book_client/book_client.cc @@ -0,0 +1,194 @@ +#include "book_client.h" + +#include + +#include "boost/algorithm/string/predicate.hpp" +#include "boost/filesystem/operations.hpp" +#include "json/json.h" + +#include "book_json.h" + +BookClient::BookClient(const std::string& url, int timeout) + : url_(url), session_(timeout) { + // Default Content-Type for requests who have a body. + session_.set_media_type("application/json"); + session_.set_charset("utf-8"); +} + +bool BookClient::Query(std::list* books) { + try { + auto r = session_.Send(WEBCC_GET(url_).Path("books")()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + // Response HTTP status error. + return false; + } + + Json::Value json = StringToJson(r->data()); + + if (!json.isArray()) { + return false; // Should be a JSON array of books. + } + + for (Json::ArrayIndex i = 0; i < json.size(); ++i) { + books->push_back(JsonToBook(json[i])); + } + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::Create(const std::string& title, double price, + 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))()); + + if (!CheckStatus(r, webcc::Status::kCreated)) { + return false; + } + + Json::Value rsp_json = StringToJson(r->data()); + *id = rsp_json["id"].asString(); + + if (id->empty()) { + return false; + } + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::Get(const std::string& id, Book* book) { + try { + auto r = session_.Send(WEBCC_GET(url_).Path("books").Path(id)()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + + return JsonStringToBook(r->data(), book); + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::Set(const std::string& id, const std::string& title, + double price) { + Json::Value json; + json["title"] = title; + json["price"] = price; + + try { + auto r = session_.Send(WEBCC_PUT(url_).Path("books").Path(id). + Body(JsonToString(json))()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::Delete(const std::string& id) { + try { + auto r = session_.Send(WEBCC_DELETE(url_).Path("books").Path(id)()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::GetPhoto(const std::string& id, const bfs::path& path) { + try { + auto r = session_.Send(WEBCC_GET(url_). + Path("books").Path(id).Path("photo")(), + true); // Save to temp file + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + + r->file_body()->Move(path); + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +bool BookClient::SetPhoto(const std::string& id, const bfs::path& path) { + try { + if (!CheckPhoto(path)) { + return false; + } + + auto r = session_.Send(WEBCC_PUT(url_). + Path("books").Path(id).Path("photo"). + File(path)()); + + if (!CheckStatus(r, webcc::Status::kOK)) { + return false; + } + + return true; + + } catch (const webcc::Error& error) { + std::cerr << error << std::endl; + return false; + } +} + +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) { + std::cerr << "HTTP status error (actual: " << response->status() + << "expected: " << expected_status << ")." << std::endl; + return false; + } + return true; +} diff --git a/examples/book_client/book_client.h b/examples/book_client/book_client.h new file mode 100644 index 0000000..9dd4b3a --- /dev/null +++ b/examples/book_client/book_client.h @@ -0,0 +1,49 @@ +#ifndef BOOK_CLIENT_H_ +#define BOOK_CLIENT_H_ + +#include +#include + +#include "boost/filesystem/path.hpp" +#include "json/json-forwards.h" + +#include "webcc/client_session.h" + +#include "book.h" + +namespace bfs = boost::filesystem; + +class BookClient { +public: + explicit BookClient(const std::string& url, int timeout = 0); + + ~BookClient() = default; + + bool Query(std::list* books); + + bool Create(const std::string& title, double price, std::string* id); + + bool Get(const std::string& id, Book* book); + + bool Set(const std::string& id, const std::string& title, double price); + + bool Delete(const std::string& id); + + // Get photo, save to the given path. + bool GetPhoto(const std::string& id, const bfs::path& path); + + // Set photo using the file of the given path. + bool SetPhoto(const std::string& id, const bfs::path& path); + +private: + bool CheckPhoto(const bfs::path& photo); + + // Check HTTP response status. + bool CheckStatus(webcc::ResponsePtr response, int expected_status); + +private: + std::string url_; + webcc::ClientSession session_; +}; + +#endif // BOOK_CLIENT_H_ diff --git a/examples/book_client/book_json.cc b/examples/book_client/book_json.cc new file mode 100644 index 0000000..e8213b7 --- /dev/null +++ b/examples/book_client/book_json.cc @@ -0,0 +1,59 @@ +#include "book_json.h" + +#include +#include + +#include "json/json.h" + +#include "book.h" + +std::string JsonToString(const Json::Value& json) { + Json::StreamWriterBuilder builder; + return Json::writeString(builder, json); +} + +Json::Value StringToJson(const std::string& str) { + Json::Value json; + + Json::CharReaderBuilder builder; + std::stringstream stream(str); + std::string errs; + if (!Json::parseFromStream(builder, stream, &json, &errs)) { + std::cerr << errs << std::endl; + } + + return json; +} + +Json::Value BookToJson(const Book& book) { + Json::Value json; + json["id"] = book.id; + json["title"] = book.title; + json["price"] = book.price; + json["photo"] = book.photo; + return json; +} + +Book JsonToBook(const Json::Value& json) { + return { + json["id"].asString(), + json["title"].asString(), + json["price"].asDouble(), + json["photo"].asString(), + }; +} + +std::string BookToJsonString(const Book& book) { + return JsonToString(BookToJson(book)); +} + +bool JsonStringToBook(const std::string& json_str, Book* book) { + Json::Value json = StringToJson(json_str); + + if (!json) { + return false; + } + + *book = JsonToBook(json); + return true; +} diff --git a/examples/book_client/book_json.h b/examples/book_client/book_json.h new file mode 100644 index 0000000..f675bf0 --- /dev/null +++ b/examples/book_client/book_json.h @@ -0,0 +1,22 @@ +#ifndef BOOK_JSON_H_ +#define BOOK_JSON_H_ + +#include + +#include "json/json-forwards.h" + +struct Book; + +std::string JsonToString(const Json::Value& json); + +Json::Value StringToJson(const std::string& str); + +Json::Value BookToJson(const Book& book); + +Book JsonToBook(const Json::Value& json); + +std::string BookToJsonString(const Book& book); + +bool JsonStringToBook(const std::string& json_str, Book* book); + +#endif // BOOK_JSON_H_ diff --git a/examples/book_client/main.cc b/examples/book_client/main.cc new file mode 100644 index 0000000..517446a --- /dev/null +++ b/examples/book_client/main.cc @@ -0,0 +1,141 @@ +#include + +#include "boost/filesystem/operations.hpp" +#include "webcc/logger.h" + +#include "book_client.h" + +// Memory leak detection with VLD. +#if (defined(_WIN32) || defined(_WIN64)) +#if defined(_DEBUG) && defined(WEBCC_ENABLE_VLD) +#pragma message ("< include vld.h >") +#include "vld/vld.h" +#pragma comment(lib, "vld") +#endif +#endif + +// ----------------------------------------------------------------------------- + +void PrintSeparator() { + static const std::string s_line(80, '-'); + std::cout << s_line << std::endl; +} + +void PrintBook(const Book& book) { + std::cout << "Book: " << book << std::endl; +} + +void PrintBookList(const std::list& books) { + std::cout << "Book list: " << books.size() << std::endl; + for (const Book& book : books) { + std::cout << " Book: " << book << std::endl; + } +} + +// ----------------------------------------------------------------------------- + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cout << "usage: book_client " << std::endl; + std::cout << "e.g.," << std::endl; + std::cout << " $ book_client http://localhost:8080 path/to/photo_dir" + << std::endl; + return 1; + } + + std::string url = argv[1]; + + bfs::path photo_dir = argv[2]; + if (!bfs::is_directory(photo_dir) || !bfs::exists(photo_dir)) { + std::cerr << "Invalid photo dir!" << std::endl; + return 1; + } + + std::cout << "Test photo dir: " << photo_dir << std::endl; + + WEBCC_LOG_INIT("", webcc::LOG_CONSOLE_FILE_OVERWRITE); + + BookClient client(url); + + PrintSeparator(); + + std::list books; + if (client.Query(&books)) { + PrintBookList(books); + } else { + return 1; + } + + PrintSeparator(); + + std::string id; + if (client.Create("1984", 12.3, &id)) { + std::cout << "Book ID: " << id << std::endl; + } else { + return 1; + } + + if (!client.SetPhoto(id, photo_dir / "1984.jpg")) { + return 1; + } + + PrintSeparator(); + + books.clear(); + if (client.Query(&books)) { + PrintBookList(books); + } else { + return 1; + } + + PrintSeparator(); + + Book book; + if (client.Get(id, &book)) { + PrintBook(book); + } else { + return 1; + } + + PrintSeparator(); + + std::cout << "Press any key to continue..."; + std::getchar(); + + if (!client.Set(id, "1Q84", 32.1)) { + return 1; + } + + if (!client.SetPhoto(id, photo_dir / "1Q84.jpg")) { + return 1; + } + + PrintSeparator(); + + if (client.Get(id, &book)) { + PrintBook(book); + } else { + return 1; + } + + PrintSeparator(); + + std::cout << "Press any key to continue..."; + std::getchar(); + + if (!client.Delete(id)) { + return 1; + } + + PrintSeparator(); + + books.clear(); + if (client.Query(&books)) { + PrintBookList(books); + } + + std::cout << "Press any key to continue..."; + std::getchar(); + + return 0; +} diff --git a/examples/book_client/photo/1984.jpg b/examples/book_client/photo/1984.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e4c7919368a2684e31fda08c13cc471e1cd6cc9 GIT binary patch literal 60536 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 MC%N>ZiYNpB+2YpAga7~l literal 0 HcmV?d00001 diff --git a/examples/book_client/photo/1Q84.jpg b/examples/book_client/photo/1Q84.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8d2fa884ada1712069eda33507cfd3cbe431fbd GIT binary patch literal 13489 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 literal 0 HcmV?d00001 diff --git a/examples/rest_book_client.cc b/examples/book_client/rest_book_client.cc similarity index 100% rename from examples/rest_book_client.cc rename to examples/book_client/rest_book_client.cc diff --git a/examples/book_server/CMakeLists.txt b/examples/book_server/CMakeLists.txt new file mode 100644 index 0000000..bf4aa4f --- /dev/null +++ b/examples/book_server/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SRCS + book.cc + book.h + book_db.cc + book_db.h + book_json.cc + book_json.h + views.cc + views.h + main.cc + ) + +add_executable(book_server ${SRCS}) +target_link_libraries(book_server ${EXAMPLE_LIBS} jsoncpp) diff --git a/examples/book_server/book.cc b/examples/book_server/book.cc new file mode 100644 index 0000000..9544d82 --- /dev/null +++ b/examples/book_server/book.cc @@ -0,0 +1,11 @@ +#include "book.h" + +#include + +const Book kNullBook{}; + +std::ostream& operator<<(std::ostream& os, const Book& book) { + os << "{ " << book.id << ", " << book.title << ", " << book.price << ", " + << book.photo << " }"; + return os; +} diff --git a/examples/book_server/book.h b/examples/book_server/book.h new file mode 100644 index 0000000..11a3cba --- /dev/null +++ b/examples/book_server/book.h @@ -0,0 +1,24 @@ +#ifndef BOOK_H_ +#define BOOK_H_ + +#include +#include + +#include "boost/filesystem/path.hpp" + +struct Book { + std::string id; + std::string title; + double price; + std::string photo; // Name only + + bool IsNull() const { + return id.empty(); + } +}; + +std::ostream& operator<<(std::ostream& os, const Book& book); + +extern const Book kNullBook; + +#endif // BOOK_H_ diff --git a/examples/book_server/book_db.cc b/examples/book_server/book_db.cc new file mode 100644 index 0000000..4fa3e23 --- /dev/null +++ b/examples/book_server/book_db.cc @@ -0,0 +1,66 @@ +#include "book_db.h" + +#include + +const Book& BookDB::Get(const std::string& id) const { + auto it = Find(id); + return (it == books_.end() ? kNullBook : *it); +} + +std::string BookDB::Add(const Book& book) { + std::string id = NewID(); + books_.push_back({ id, book.title, book.price }); + return id; +} + +bool BookDB::Set(const Book& book) { + auto it = Find(book.id); + if (it != books_.end()) { + it->title = book.title; + it->price = book.price; + return true; + } + return false; +} + +std::string BookDB::GetPhoto(const std::string& id) const { + auto it = Find(id); + return it != books_.end() ? it->photo : ""; +} + +bool BookDB::SetPhoto(const std::string& id, const std::string& photo) { + auto it = Find(id); + if (it != books_.end()) { + it->photo = photo; + return true; + } + return false; +} + +bool BookDB::Delete(const std::string& id) { + auto it = Find(id); + + if (it != books_.end()) { + books_.erase(it); + return true; + } + + return false; +} + +std::list::const_iterator BookDB::Find(const std::string& id) const { + return std::find_if(books_.begin(), books_.end(), + [&id](const Book& book) { return book.id == id; }); +} + +std::list::iterator BookDB::Find(const std::string& id) { + return std::find_if(books_.begin(), books_.end(), + [&id](Book& book) { return book.id == id; }); +} + +std::string BookDB::NewID() const { + static int s_id_counter = 0; + + ++s_id_counter; + return std::to_string(s_id_counter); +} diff --git a/examples/book_server/book_db.h b/examples/book_server/book_db.h new file mode 100644 index 0000000..e2556fb --- /dev/null +++ b/examples/book_server/book_db.h @@ -0,0 +1,44 @@ +#ifndef BOOK_DB_H_ +#define BOOK_DB_H_ + +#include +#include + +#include "book.h" + +// Book database simulator. +// There should be some database in a real product. + +class BookDB { +public: + const std::list& books() const { + return books_; + } + + const Book& Get(const std::string& id) const; + + // Add a book, return the ID. + // NOTE: The ID of the input book will be ignored so should be empty. + std::string Add(const Book& book); + + bool Set(const Book& book); + + std::string GetPhoto(const std::string& id) const; + + bool SetPhoto(const std::string& id, const std::string& photo); + + bool Delete(const std::string& id); + +private: + std::list::const_iterator Find(const std::string& id) const; + + std::list::iterator Find(const std::string& id); + + // Allocate a new book ID. + std::string NewID() const; + +private: + std::list books_; +}; + +#endif // BOOK_DB_H_ diff --git a/examples/book_server/book_json.cc b/examples/book_server/book_json.cc new file mode 100644 index 0000000..206983e --- /dev/null +++ b/examples/book_server/book_json.cc @@ -0,0 +1,57 @@ +#include "book_json.h" + +#include +#include + +#include "json/json.h" + +#include "book.h" + +std::string JsonToString(const Json::Value& json) { + Json::StreamWriterBuilder builder; + return Json::writeString(builder, json); +} + +Json::Value StringToJson(const std::string& str) { + Json::Value json; + + Json::CharReaderBuilder builder; + std::stringstream stream(str); + std::string errs; + if (!Json::parseFromStream(builder, stream, &json, &errs)) { + std::cerr << errs << std::endl; + } + + return json; +} + +Json::Value BookToJson(const Book& book) { + Json::Value json; + json["id"] = book.id; + json["title"] = book.title; + json["price"] = book.price; + return json; +} + +Book JsonToBook(const Json::Value& json) { + return { + json["id"].asString(), + json["title"].asString(), + json["price"].asDouble(), + }; +} + +std::string BookToJsonString(const Book& book) { + return JsonToString(BookToJson(book)); +} + +bool JsonStringToBook(const std::string& json_str, Book* book) { + Json::Value json = StringToJson(json_str); + + if (!json) { + return false; + } + + *book = JsonToBook(json); + return true; +} diff --git a/examples/book_server/book_json.h b/examples/book_server/book_json.h new file mode 100644 index 0000000..f675bf0 --- /dev/null +++ b/examples/book_server/book_json.h @@ -0,0 +1,22 @@ +#ifndef BOOK_JSON_H_ +#define BOOK_JSON_H_ + +#include + +#include "json/json-forwards.h" + +struct Book; + +std::string JsonToString(const Json::Value& json); + +Json::Value StringToJson(const std::string& str); + +Json::Value BookToJson(const Book& book); + +Book JsonToBook(const Json::Value& json); + +std::string BookToJsonString(const Book& book); + +bool JsonStringToBook(const std::string& json_str, Book* book); + +#endif // BOOK_JSON_H_ diff --git a/examples/book_server/main.cc b/examples/book_server/main.cc new file mode 100644 index 0000000..def9bd8 --- /dev/null +++ b/examples/book_server/main.cc @@ -0,0 +1,68 @@ +#include + +#include "boost/filesystem/operations.hpp" + +#include "webcc/logger.h" +#include "webcc/server.h" + +#include "views.h" + +// Memory leak detection with VLD. +#if (defined(_WIN32) || defined(_WIN64)) +#if defined(_DEBUG) && defined(WEBCC_ENABLE_VLD) +#pragma message ("< include vld.h >") +#include "vld/vld.h" +#pragma comment(lib, "vld") +#endif +#endif + +int main(int argc, char* argv[]) { + if (argc < 3) { + std::cout << "usage: book_server " << std::endl; + std::cout << "e.g.," << std::endl; + std::cout << " $ book_server 8080 D:/upload" << std::endl; + return 1; + } + + WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); + + std::uint16_t port = static_cast(std::atoi(argv[1])); + + 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; + } + + // Add a sub-dir for book photos. + bfs::path photo_dir = upload_dir / "books"; + if (!bfs::exists(photo_dir)) { + bfs::create_directory(photo_dir); + } + + std::cout << "Book photos will be saved to: " << photo_dir << std::endl; + + try { + webcc::Server server(port); // No doc root + + server.Route("/books", + std::make_shared(), + { "GET", "POST" }); + + server.Route(webcc::R("/books/(\\d+)"), + std::make_shared(photo_dir), + { "GET", "PUT", "DELETE" }); + + server.Route(webcc::R("/books/(\\d+)/photo"), + std::make_shared(photo_dir), + { "GET", "PUT", "DELETE" }); + + server.Run(2); + + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/examples/book_server/views.cc b/examples/book_server/views.cc new file mode 100644 index 0000000..4015ee9 --- /dev/null +++ b/examples/book_server/views.cc @@ -0,0 +1,223 @@ +#include "views.h" + +#include "json/json.h" + +#include "boost/filesystem/operations.hpp" +#include "webcc/response_builder.h" + +#include "book.h" +#include "book_db.h" +#include "book_json.h" + +// ----------------------------------------------------------------------------- + +static BookDB g_book_db; + +// ----------------------------------------------------------------------------- + +webcc::ResponsePtr BookListView::Handle(webcc::RequestPtr request) { + if (request->method() == "GET") { + return Get(request); + } + if (request->method() == "POST") { + return Post(request); + } + return {}; +} + +webcc::ResponsePtr BookListView::Get(webcc::RequestPtr request) { + Json::Value json(Json::arrayValue); + + for (const Book& book : g_book_db.books()) { + json.append(BookToJson(book)); + } + + // Return all books as a JSON array. + + return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json().Utf8()(); +} + +webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) { + Book book; + if (JsonStringToBook(request->data(), &book)) { + std::string id = g_book_db.Add(book); + + Json::Value json; + json["id"] = id; + + return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)).Json().Utf8()(); + } else { + // Invalid JSON + return webcc::ResponseBuilder{}.BadRequest()(); + } +} + +// ----------------------------------------------------------------------------- + +BookDetailView::BookDetailView(bfs::path photo_dir) + : photo_dir_(std::move(photo_dir)) { +} + +webcc::ResponsePtr BookDetailView::Handle(webcc::RequestPtr request) { + if (request->method() == "GET") { + return Get(request); + } + if (request->method() == "PUT") { + return Put(request); + } + if (request->method() == "DELETE") { + return Delete(request); + } + return {}; +} + +webcc::ResponsePtr BookDetailView::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()(); + } + + const std::string& id = request->args()[0]; + + const Book& book = g_book_db.Get(id); + if (book.IsNull()) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).Json().Utf8()(); +} + +webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + const std::string& id = request->args()[0]; + + Book book; + if (!JsonStringToBook(request->data(), &book)) { + return webcc::ResponseBuilder{}.BadRequest()(); + } + + book.id = id; + g_book_db.Set(book); + + return webcc::ResponseBuilder{}.OK()(); +} + +webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + const std::string& id = request->args()[0]; + + std::string photo_name = g_book_db.GetPhoto(id); + + // Delete the book from DB. + if (!g_book_db.Delete(id)) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + // Delete the photo from file system. + if (!photo_name.empty()) { + boost::system::error_code ec; + bfs::remove(photo_dir_ / photo_name, ec); + } + + return webcc::ResponseBuilder{}.OK()(); +} + +// ----------------------------------------------------------------------------- + +BookPhotoView::BookPhotoView(bfs::path photo_dir) + : photo_dir_(std::move(photo_dir)) { +} + +webcc::ResponsePtr BookPhotoView::Handle(webcc::RequestPtr request) { + if (request->method() == "GET") { + return Get(request); + } + if (request->method() == "PUT") { + return Put(request); + } + if (request->method() == "DELETE") { + return Delete(request); + } + return {}; +} + +// TODO: Check content type to see if it's JPEG. +webcc::ResponsePtr BookPhotoView::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_db.Get(id); + if (book.IsNull() || book.photo.empty()) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + bfs::path photo_path = photo_dir_ / book.photo; + if (!bfs::exists(photo_path)) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + // 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()(); + } +} + +webcc::ResponsePtr BookPhotoView::Put(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + const std::string& id = request->args()[0]; + + const Book& book = g_book_db.Get(id); + if (book.IsNull()) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + // Use ID as the name of the photo. + // You can also use a UUID or any unique string as the name. + auto photo_name = id + ".jpg"; + + if (!request->file_body()->Move(photo_dir_ / photo_name)) { + return webcc::ResponseBuilder{}.InternalServerError()(); + } + + // Set photo name to DB. + if (!g_book_db.SetPhoto(id, photo_name)) { + return webcc::ResponseBuilder{}.InternalServerError()(); + } + + return webcc::ResponseBuilder{}.OK()(); +} + +webcc::ResponsePtr BookPhotoView::Delete(webcc::RequestPtr request) { + if (request->args().size() != 1) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + const std::string& id = request->args()[0]; + + const Book& book = g_book_db.Get(id); + if (book.IsNull() || book.photo.empty()) { + return webcc::ResponseBuilder{}.NotFound()(); + } + + // Error handling is simplified. + boost::system::error_code ec; + bfs::remove(photo_dir_ / book.photo, ec); + + return webcc::ResponseBuilder{}.OK()(); +} diff --git a/examples/book_server/views.h b/examples/book_server/views.h new file mode 100644 index 0000000..399a68f --- /dev/null +++ b/examples/book_server/views.h @@ -0,0 +1,76 @@ +#ifndef VIEWS_H_ +#define VIEWS_H_ + +#include "webcc/view.h" + +#include "boost/filesystem/path.hpp" + +namespace bfs = boost::filesystem; + +// ----------------------------------------------------------------------------- + +// URL: /books +class BookListView : public webcc::View { +public: + webcc::ResponsePtr Handle(webcc::RequestPtr request) override; + +private: + // Get a list of books based on query parameters. + webcc::ResponsePtr Get(webcc::RequestPtr request); + + // Create a new book. + webcc::ResponsePtr Post(webcc::RequestPtr request); +}; + +// ----------------------------------------------------------------------------- + +// URL: /books/{id} +class BookDetailView : public webcc::View { +public: + explicit BookDetailView(bfs::path photo_dir); + + webcc::ResponsePtr Handle(webcc::RequestPtr request) override; + +private: + // Get the detailed information of a book. + webcc::ResponsePtr Get(webcc::RequestPtr request); + + // Update a book. + webcc::ResponsePtr Put(webcc::RequestPtr request); + + // Delete a book. + webcc::ResponsePtr Delete(webcc::RequestPtr request); + +private: + bfs::path photo_dir_; +}; + +// ----------------------------------------------------------------------------- + +// URL: /books/{id}/photo +class BookPhotoView : public webcc::View { +public: + explicit BookPhotoView(bfs::path photo_dir); + + webcc::ResponsePtr Handle(webcc::RequestPtr request) override; + + // Stream the request data, an image, of PUT into a temp file. + bool Stream(const std::string& method) override { + return method == "PUT"; + } + +private: + // Get the photo of the book. + webcc::ResponsePtr Get(webcc::RequestPtr request); + + // Set the photo of the book. + webcc::ResponsePtr Put(webcc::RequestPtr request); + + // Delete the photo of the book. + webcc::ResponsePtr Delete(webcc::RequestPtr request); + +private: + bfs::path photo_dir_; +}; + +#endif // VIEWS_H_ diff --git a/examples/rest_book_server.cc b/examples/rest_book_server.cc deleted file mode 100644 index e8281c7..0000000 --- a/examples/rest_book_server.cc +++ /dev/null @@ -1,286 +0,0 @@ -#include -#include - -#include "boost/filesystem/operations.hpp" - -#include "json/json.h" - -#include "webcc/logger.h" -#include "webcc/response_builder.h" -#include "webcc/server.h" - -#include "examples/common/book.h" -#include "examples/common/book_json.h" - -#if (defined(_WIN32) || defined(_WIN64)) -#if defined(_DEBUG) && defined(WEBCC_ENABLE_VLD) -#pragma message ("< include vld.h >") -#include "vld/vld.h" -#pragma comment(lib, "vld") -#endif -#endif - -namespace bfs = boost::filesystem; - -// ----------------------------------------------------------------------------- - -static BookStore g_book_store; - -// ----------------------------------------------------------------------------- -// BookListView - -// URL: /books -class BookListView : public webcc::View { -public: - webcc::ResponsePtr Handle(webcc::RequestPtr request) override { - if (request->method() == "GET") { - return Get(request); - } - - if (request->method() == "POST") { - return Post(request); - } - - return {}; - } - -private: - // Get a list of books based on query parameters. - webcc::ResponsePtr Get(webcc::RequestPtr request) { - Json::Value json(Json::arrayValue); - - for (const Book& book : g_book_store.books()) { - json.append(BookToJson(book)); - } - - // 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 - -// URL: /books/{id} -class BookDetailView : public webcc::View { -public: - 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) { - 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()(); - } - - const std::string& id = request->args()[0]; - - const Book& book = g_book_store.GetBook(id); - if (book.IsNull()) { - return webcc::ResponseBuilder{}.NotFound()(); - } - - 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()(); - } - - const std::string& id = request->args()[0]; - - Book book; - if (!JsonStringToBook(request->data(), &book)) { - return webcc::ResponseBuilder{}.BadRequest()(); - } - - book.id = id; - g_book_store.UpdateBook(book); - - return webcc::ResponseBuilder{}.OK()(); - } - - // Delete a book. - webcc::ResponsePtr Delete(webcc::RequestPtr request) { - if (request->args().size() != 1) { - return webcc::ResponseBuilder{}.NotFound()(); - } - - const std::string& id = request->args()[0]; - - if (!g_book_store.DeleteBook(id)) { - return webcc::ResponseBuilder{}.NotFound()(); - } - - return webcc::ResponseBuilder{}.OK()(); - } -}; - -// ----------------------------------------------------------------------------- -// BookPhotoView - -// URL: /books/{id}/photo -class BookPhotoView : public webcc::View { -public: - explicit BookPhotoView(bfs::path upload_dir) - : upload_dir_(std::move(upload_dir)) { - } - - 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 {}; - } - - // Stream the request data, an image, of PUT into a temp file. - bool Stream(const std::string& method) override { - return method == "PUT"; - } - -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()(); - } - - // 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()(); - } - } - - // 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()(); - } - - const std::string& id = request->args()[0]; - - const Book& book = g_book_store.GetBook(id); - if (book.IsNull()) { - return webcc::ResponseBuilder{}.NotFound()(); - } - - request->file_body()->Move(GetPhotoPath(id)); - - return webcc::ResponseBuilder{}.OK()(); - } - - // Delete the photo of the book. - webcc::ResponsePtr Delete(webcc::RequestPtr request) { - return {}; - } - -private: - bfs::path GetPhotoPath(const std::string& book_id) const { - return upload_dir_ / "book_photo" / (book_id + ".jpg"); - } - -private: - bfs::path upload_dir_; -}; - -// ----------------------------------------------------------------------------- - -int main(int argc, char* argv[]) { - if (argc < 3) { - std::cout << "usage: rest_book_server " << std::endl; - std::cout << "examples:" << std::endl; - std::cout << " $ rest_book_server 8080 D:/upload" << std::endl; - return 1; - } - - WEBCC_LOG_INIT("", webcc::LOG_CONSOLE); - - std::uint16_t port = static_cast(std::atoi(argv[1])); - - 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); // No doc root - - server.Route("/books", - std::make_shared(), - { "GET", "POST" }); - - server.Route(webcc::R("/books/(\\d+)"), - std::make_shared(), - { "GET", "PUT", "DELETE" }); - - server.Route(webcc::R("/books/(\\d+)/photo"), - std::make_shared(upload_dir), - { "GET", "PUT", "DELETE" }); - - server.Run(2); - - } catch (const std::exception& e) { - std::cerr << e.what() << std::endl; - return 1; - } - - return 0; -} diff --git a/examples/file_server.cc b/examples/static_file_server.cc similarity index 100% rename from examples/file_server.cc rename to examples/static_file_server.cc