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 0000000..2e4c791 Binary files /dev/null and b/examples/book_client/photo/1984.jpg differ diff --git a/examples/book_client/photo/1Q84.jpg b/examples/book_client/photo/1Q84.jpg new file mode 100644 index 0000000..a8d2fa8 Binary files /dev/null and b/examples/book_client/photo/1Q84.jpg differ 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