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