Rework book example to support upload book photo.

master
Chunting Gu 6 years ago
parent 4c413e245d
commit f1ad97b227

@ -4,6 +4,8 @@
#include <list> #include <list>
#include <string> #include <string>
#include "boost/filesystem/path.hpp"
// In-memory test data. // In-memory test data.
// There should be some database in a real product. // There should be some database in a real product.
@ -11,6 +13,7 @@ struct Book {
std::string id; std::string id;
std::string title; std::string title;
double price; double price;
boost::filesystem::path photo;
bool IsNull() const { return id.empty(); } bool IsNull() const { return id.empty(); }
}; };
@ -21,7 +24,9 @@ extern const Book kNullBook;
class BookStore { class BookStore {
public: public:
const std::list<Book>& books() const { return books_; } const std::list<Book>& books() const {
return books_;
}
const Book& GetBook(const std::string& id) const; const Book& GetBook(const std::string& id) const;

@ -1,6 +1,9 @@
#include <iostream> #include <iostream>
#include <list> #include <list>
#include "boost/algorithm/string/predicate.hpp"
#include "boost/filesystem/operations.hpp"
#include "json/json.h" #include "json/json.h"
#include "webcc/client_session.h" #include "webcc/client_session.h"
@ -17,6 +20,8 @@
#endif #endif
#endif #endif
namespace bfs = boost::filesystem;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
class BookClient { class BookClient {
@ -27,7 +32,8 @@ public:
bool ListBooks(std::list<Book>* books); bool ListBooks(std::list<Book>* 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); bool GetBook(const std::string& id, Book* book);
@ -37,6 +43,8 @@ public:
bool DeleteBook(const std::string& id); bool DeleteBook(const std::string& id);
private: private:
bool CheckPhoto(const bfs::path& photo);
// Check HTTP response status. // Check HTTP response status.
bool CheckStatus(webcc::ResponsePtr response, int expected_status); bool CheckStatus(webcc::ResponsePtr response, int expected_status);
@ -83,15 +91,14 @@ bool BookClient::ListBooks(std::list<Book>* books) {
} }
bool BookClient::CreateBook(const std::string& title, double price, bool BookClient::CreateBook(const std::string& title, double price,
std::string* id) { const bfs::path& photo, std::string* id) {
Json::Value req_json; Json::Value req_json;
req_json["title"] = title; req_json["title"] = title;
req_json["price"] = price; req_json["price"] = price;
try { try {
auto r = session_.Send(WEBCC_POST(url_).Path("books"). auto r = session_.Send(WEBCC_POST(url_).Path("books").
Body(JsonToString(req_json)) Body(JsonToString(req_json))());
());
if (!CheckStatus(r, webcc::Status::kCreated)) { if (!CheckStatus(r, webcc::Status::kCreated)) {
return false; return false;
@ -100,7 +107,20 @@ bool BookClient::CreateBook(const std::string& title, double price,
Json::Value rsp_json = StringToJson(r->data()); Json::Value rsp_json = StringToJson(r->data());
*id = rsp_json["id"].asString(); *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) { } catch (const webcc::Error& error) {
std::cerr << error << std::endl; std::cerr << error << std::endl;
@ -132,8 +152,7 @@ bool BookClient::UpdateBook(const std::string& id, const std::string& title,
try { try {
auto r = session_.Send(WEBCC_PUT(url_).Path("books").Path(id). auto r = session_.Send(WEBCC_PUT(url_).Path("books").Path(id).
Body(JsonToString(json)) Body(JsonToString(json))());
());
if (!CheckStatus(r, webcc::Status::kOK)) { if (!CheckStatus(r, webcc::Status::kOK)) {
return false; 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) { bool BookClient::CheckStatus(webcc::ResponsePtr response, int expected_status) {
if (response->status() != expected_status) { if (response->status() != expected_status) {
LOG_ERRO("HTTP status error (actual: %d, expected: %d).", LOG_ERRO("HTTP status error (actual: %d, expected: %d).",
@ -194,40 +230,38 @@ void PrintBookList(const std::list<Book>& books) {
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
if (argc < 2) { if (argc < 2) {
std::cout << "usage: rest_book_client <url> [timeout]" << std::endl; std::cout << "usage: rest_book_client <url>" << std::endl;
std::cout << std::endl;
std::cout << "examples:" << 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" << std::endl;
std::cout << " $ rest_book_client http://localhost:8080 2" << std::endl;
return 1; return 1;
} }
std::string url = argv[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); WEBCC_LOG_INIT("", webcc::LOG_CONSOLE_FILE_OVERWRITE);
BookClient client(url, timeout); BookClient client(url);
PrintSeparator(); PrintSeparator();
// List all books.
std::list<Book> books; std::list<Book> books;
if (client.ListBooks(&books)) { if (client.ListBooks(&books)) {
PrintBookList(books); PrintBookList(books);
} else {
return 1;
} }
PrintSeparator(); PrintSeparator();
// Create a new book.
std::string id; std::string id;
if (client.CreateBook("1984", 12.3, &id)) { if (client.CreateBook("1984", 12.3, "", &id)) {
std::cout << "Book ID: " << id << std::endl; std::cout << "Book ID: " << id << std::endl;
} else { } else {
id = "1"; return 1;
std::cout << "Book ID: " << id << " (faked)"<< std::endl;
} }
PrintSeparator(); PrintSeparator();
@ -235,6 +269,8 @@ int main(int argc, char* argv[]) {
books.clear(); books.clear();
if (client.ListBooks(&books)) { if (client.ListBooks(&books)) {
PrintBookList(books); PrintBookList(books);
} else {
return 1;
} }
PrintSeparator(); PrintSeparator();
@ -242,21 +278,29 @@ int main(int argc, char* argv[]) {
Book book; Book book;
if (client.GetBook(id, &book)) { if (client.GetBook(id, &book)) {
PrintBook(book); PrintBook(book);
} else {
return 1;
} }
PrintSeparator(); PrintSeparator();
client.UpdateBook(id, "1Q84", 32.1); if (!client.UpdateBook(id, "1Q84", 32.1)) {
return 1;
}
PrintSeparator(); PrintSeparator();
if (client.GetBook(id, &book)) { if (client.GetBook(id, &book)) {
PrintBook(book); PrintBook(book);
} else {
return 1;
} }
PrintSeparator(); PrintSeparator();
client.DeleteBook(id); if (!client.DeleteBook(id)) {
return 1;
}
PrintSeparator(); PrintSeparator();

@ -1,8 +1,7 @@
#include <iostream> #include <iostream>
#include <list>
#include <string> #include <string>
#include <thread>
#include <vector> #include "boost/filesystem/operations.hpp"
#include "json/json.h" #include "json/json.h"
@ -21,24 +20,18 @@
#endif #endif
#endif #endif
namespace bfs = boost::filesystem;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
static BookStore g_book_store; 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 { class BookListView : public webcc::View {
public: public:
explicit BookListView(int sleep_seconds) : sleep_seconds_(sleep_seconds) {
}
webcc::ResponsePtr Handle(webcc::RequestPtr request) override { webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
if (request->method() == "GET") { if (request->method() == "GET") {
return Get(request); return Get(request);
@ -53,163 +46,207 @@ public:
private: private:
// Get a list of books based on query parameters. // 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);
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. // Create a new book.
webcc::ResponsePtr Post(webcc::RequestPtr request); webcc::ResponsePtr Post(webcc::RequestPtr request) {
Book book;
if (JsonStringToBook(request->data(), &book)) {
std::string id = g_book_store.AddBook(book);
private: Json::Value json;
// Sleep some seconds before send back the response. json["id"] = id;
// For testing timeout control in client side.
int sleep_seconds_; 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 // URL: /books/{id}
// contains the matched book ID.
class BookDetailView : public webcc::View { class BookDetailView : public webcc::View {
public: public:
explicit BookDetailView(int sleep_seconds) : sleep_seconds_(sleep_seconds) {
}
webcc::ResponsePtr Handle(webcc::RequestPtr request) override { webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
if (request->method() == "GET") { if (request->method() == "GET") {
return Get(request); return Get(request);
} }
if (request->method() == "PUT") { if (request->method() == "PUT") {
return Put(request); return Put(request);
} }
if (request->method() == "DELETE") { if (request->method() == "DELETE") {
return Delete(request); return Delete(request);
} }
return {}; return {};
} }
private: private:
// Get the detailed information of a book. // 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()();
}
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. // Update a book.
webcc::ResponsePtr Put(webcc::RequestPtr request); webcc::ResponsePtr Put(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
// Delete a book. const std::string& id = request->args()[0];
webcc::ResponsePtr Delete(webcc::RequestPtr request);
private: Book book;
// Sleep some seconds before send back the response. if (!JsonStringToBook(request->data(), &book)) {
// For testing timeout control in client side. return webcc::ResponseBuilder{}.BadRequest()();
int sleep_seconds_; }
};
// ----------------------------------------------------------------------------- book.id = id;
g_book_store.UpdateBook(book);
// Return all books as a JSON array. return webcc::ResponseBuilder{}.OK()();
webcc::ResponsePtr BookListView::Get(webcc::RequestPtr request) { }
Sleep(sleep_seconds_);
Json::Value json(Json::arrayValue); // Delete a book.
webcc::ResponsePtr Delete(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
for (const Book& book : g_book_store.books()) { const std::string& id = request->args()[0];
json.append(BookToJson(book));
if (!g_book_store.DeleteBook(id)) {
return webcc::ResponseBuilder{}.NotFound()();
} }
return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json(). return webcc::ResponseBuilder{}.OK()();
Utf8()(); }
} };
webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) { // -----------------------------------------------------------------------------
Sleep(sleep_seconds_); // BookPhotoView
Book book; // URL: /books/{id}/photo
if (JsonStringToBook(request->data(), &book)) { class BookPhotoView : public webcc::View {
std::string id = g_book_store.AddBook(book); public:
explicit BookPhotoView(bfs::path upload_dir)
: upload_dir_(std::move(upload_dir)) {
}
Json::Value json; webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
json["id"] = id; if (request->method() == "GET") {
return Get(request);
}
return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)). if (request->method() == "PUT") {
Json().Utf8()(); return Put(request);
} else {
// Invalid JSON
return webcc::ResponseBuilder{}.BadRequest()();
} }
}
// ----------------------------------------------------------------------------- if (request->method() == "DELETE") {
return Delete(request);
}
return {};
}
webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) { // Stream the request data, an image, of PUT into a temp file.
Sleep(sleep_seconds_); 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) { 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()(); return webcc::ResponseBuilder{}.NotFound()();
} }
const std::string& book_id = request->args()[0]; const std::string& id = request->args()[0];
const Book& book = g_book_store.GetBook(id);
const Book& book = g_book_store.GetBook(book_id);
if (book.IsNull()) { if (book.IsNull()) {
return webcc::ResponseBuilder{}.NotFound()(); return webcc::ResponseBuilder{}.NotFound()();
} }
return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)). bfs::path photo_path = GetPhotoPath(id);
Json().Utf8()(); if (!bfs::exists(photo_path)) {
} return webcc::ResponseBuilder{}.NotFound()();
}
webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) { // File() might throw Error::kFileError.
Sleep(sleep_seconds_); // 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) { if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()(); return webcc::ResponseBuilder{}.NotFound()();
} }
const std::string& book_id = request->args()[0]; const std::string& id = request->args()[0];
Book book; const Book& book = g_book_store.GetBook(id);
if (!JsonStringToBook(request->data(), &book)) { if (book.IsNull()) {
return webcc::ResponseBuilder{}.BadRequest()(); return webcc::ResponseBuilder{}.NotFound()();
} }
book.id = book_id; request->file_body()->Move(GetPhotoPath(id));
g_book_store.UpdateBook(book);
return webcc::ResponseBuilder{}.OK()(); return webcc::ResponseBuilder{}.OK()();
}
webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
} }
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)) { private:
return webcc::ResponseBuilder{}.NotFound()(); 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[]) { int main(int argc, char* argv[]) {
if (argc < 2) { if (argc < 3) {
std::cout << "usage: rest_book_server <port> [seconds]" << std::endl; std::cout << "usage: rest_book_server <port> <upload_dir>" << 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;
std::cout << "examples:" << std::endl; std::cout << "examples:" << std::endl;
std::cout << " $ rest_book_server 8080" << std::endl; std::cout << " $ rest_book_server 8080 D:/upload" << std::endl;
std::cout << " $ rest_book_server 8080 3" << std::endl;
return 1; return 1;
} }
@ -217,20 +254,25 @@ int main(int argc, char* argv[]) {
std::uint16_t port = static_cast<std::uint16_t>(std::atoi(argv[1])); std::uint16_t port = static_cast<std::uint16_t>(std::atoi(argv[1]));
int sleep_seconds = 0; bfs::path upload_dir = argv[2];
if (argc >= 3) { if (!bfs::is_directory(upload_dir) || !bfs::exists(upload_dir)) {
sleep_seconds = std::atoi(argv[2]); std::cerr << "Invalid upload dir!" << std::endl;
return 1;
} }
try { try {
webcc::Server server(port); webcc::Server server(port); // No doc root
server.Route("/books", server.Route("/books",
std::make_shared<BookListView>(sleep_seconds), std::make_shared<BookListView>(),
{ "GET", "POST" }); { "GET", "POST" });
server.Route(webcc::R("/books/(\\d+)"), server.Route(webcc::R("/books/(\\d+)"),
std::make_shared<BookDetailView>(sleep_seconds), std::make_shared<BookDetailView>(),
{ "GET", "PUT", "DELETE" });
server.Route(webcc::R("/books/(\\d+)/photo"),
std::make_shared<BookPhotoView>(upload_dir),
{ "GET", "PUT", "DELETE" }); { "GET", "PUT", "DELETE" });
server.Run(2); server.Run(2);

@ -43,6 +43,18 @@ ResponsePtr ResponseBuilder::operator()() {
return response; 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() { ResponseBuilder& ResponseBuilder::Date() {
headers_.push_back(headers::kDate); headers_.push_back(headers::kDate);
headers_.push_back(utility::GetTimestamp()); headers_.push_back(utility::GetTimestamp());

@ -92,6 +92,11 @@ public:
return *this; 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) { ResponseBuilder& Header(const std::string& key, const std::string& value) {
headers_.push_back(key); headers_.push_back(key);
headers_.push_back(value); headers_.push_back(value);

Loading…
Cancel
Save