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 <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();

@ -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,163 +46,207 @@ public:
private:
// Get a list of books based on query parameters.
webcc::ResponsePtr Get(webcc::RequestPtr request);
webcc::ResponsePtr Get(webcc::RequestPtr request) {
Json::Value json(Json::arrayValue);
// Create a new book.
webcc::ResponsePtr Post(webcc::RequestPtr request);
for (const Book& book : g_book_store.books()) {
json.append(BookToJson(book));
}
private:
// Sleep some seconds before send back the response.
// For testing timeout control in client side.
int sleep_seconds_;
// Return all books as a JSON array.
return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json().Utf8()();
}
// Create a new book.
webcc::ResponsePtr Post(webcc::RequestPtr request) {
Book book;
if (JsonStringToBook(request->data(), &book)) {
std::string id = g_book_store.AddBook(book);
Json::Value json;
json["id"] = id;
return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)).Json().Utf8()();
} else {
// Invalid JSON
return webcc::ResponseBuilder{}.BadRequest()();
}
}
};
// -----------------------------------------------------------------------------
// BookDetailView
// The URL is like '/books/{BookID}', and the 'args' parameter
// contains the matched book ID.
// URL: /books/{id}
class BookDetailView : public webcc::View {
public:
explicit BookDetailView(int sleep_seconds) : sleep_seconds_(sleep_seconds) {
}
webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
if (request->method() == "GET") {
return Get(request);
}
if (request->method() == "PUT") {
return Put(request);
}
if (request->method() == "DELETE") {
return Delete(request);
}
return {};
}
private:
// Get the detailed information of a book.
webcc::ResponsePtr Get(webcc::RequestPtr request);
webcc::ResponsePtr Get(webcc::RequestPtr request) {
if (request->args().size() != 1) {
// NotFound means the resource specified by the URL cannot be found.
// BadRequest could be another choice.
return webcc::ResponseBuilder{}.NotFound()();
}
// Update a book.
webcc::ResponsePtr Put(webcc::RequestPtr request);
const std::string& id = request->args()[0];
// Delete a book.
webcc::ResponsePtr Delete(webcc::RequestPtr request);
const Book& book = g_book_store.GetBook(id);
if (book.IsNull()) {
return webcc::ResponseBuilder{}.NotFound()();
}
private:
// Sleep some seconds before send back the response.
// For testing timeout control in client side.
int sleep_seconds_;
};
return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).Json().Utf8()();
}
// -----------------------------------------------------------------------------
// Update a book.
webcc::ResponsePtr Put(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
// Return all books as a JSON array.
webcc::ResponsePtr BookListView::Get(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
const std::string& id = request->args()[0];
Json::Value json(Json::arrayValue);
Book book;
if (!JsonStringToBook(request->data(), &book)) {
return webcc::ResponseBuilder{}.BadRequest()();
}
for (const Book& book : g_book_store.books()) {
json.append(BookToJson(book));
}
book.id = id;
g_book_store.UpdateBook(book);
return webcc::ResponseBuilder{}.OK().Body(JsonToString(json)).Json().
Utf8()();
}
return webcc::ResponseBuilder{}.OK()();
}
webcc::ResponsePtr BookListView::Post(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
// Delete a book.
webcc::ResponsePtr Delete(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
Book book;
if (JsonStringToBook(request->data(), &book)) {
std::string id = g_book_store.AddBook(book);
const std::string& id = request->args()[0];
Json::Value json;
json["id"] = id;
if (!g_book_store.DeleteBook(id)) {
return webcc::ResponseBuilder{}.NotFound()();
}
return webcc::ResponseBuilder{}.Created().Body(JsonToString(json)).
Json().Utf8()();
} else {
// Invalid JSON
return webcc::ResponseBuilder{}.BadRequest()();
return webcc::ResponseBuilder{}.OK()();
}
}
};
// -----------------------------------------------------------------------------
// BookPhotoView
webcc::ResponsePtr BookDetailView::Get(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
if (request->args().size() != 1) {
// NotFound means the resource specified by the URL cannot be found.
// BadRequest could be another choice.
return webcc::ResponseBuilder{}.NotFound()();
// URL: /books/{id}/photo
class BookPhotoView : public webcc::View {
public:
explicit BookPhotoView(bfs::path upload_dir)
: upload_dir_(std::move(upload_dir)) {
}
const std::string& book_id = request->args()[0];
webcc::ResponsePtr Handle(webcc::RequestPtr request) override {
if (request->method() == "GET") {
return Get(request);
}
const Book& book = g_book_store.GetBook(book_id);
if (book.IsNull()) {
return webcc::ResponseBuilder{}.NotFound()();
}
if (request->method() == "PUT") {
return Put(request);
}
return webcc::ResponseBuilder{}.OK().Body(BookToJsonString(book)).
Json().Utf8()();
}
if (request->method() == "DELETE") {
return Delete(request);
}
webcc::ResponsePtr BookDetailView::Put(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
return {};
}
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
// Stream the request data, an image, of PUT into a temp file.
bool Stream(const std::string& method) override {
return method == "PUT";
}
const std::string& book_id = request->args()[0];
private:
// Get the photo of the book.
// TODO: Check content type to see if it's JPEG.
webcc::ResponsePtr Get(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
const std::string& id = request->args()[0];
const Book& book = g_book_store.GetBook(id);
if (book.IsNull()) {
return webcc::ResponseBuilder{}.NotFound()();
}
bfs::path photo_path = GetPhotoPath(id);
if (!bfs::exists(photo_path)) {
return webcc::ResponseBuilder{}.NotFound()();
}
Book book;
if (!JsonStringToBook(request->data(), &book)) {
return webcc::ResponseBuilder{}.BadRequest()();
// File() might throw Error::kFileError.
// TODO: Avoid exception handling.
try {
return webcc::ResponseBuilder{}.OK().File(photo_path)();
} catch (const webcc::Error&) {
return webcc::ResponseBuilder{}.NotFound()();
}
}
book.id = book_id;
g_book_store.UpdateBook(book);
// Set the photo of the book.
// TODO: Check content type to see if it's JPEG.
webcc::ResponsePtr Put(webcc::RequestPtr request) {
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
}
return webcc::ResponseBuilder{}.OK()();
}
const std::string& id = request->args()[0];
const Book& book = g_book_store.GetBook(id);
if (book.IsNull()) {
return webcc::ResponseBuilder{}.NotFound()();
}
webcc::ResponsePtr BookDetailView::Delete(webcc::RequestPtr request) {
Sleep(sleep_seconds_);
request->file_body()->Move(GetPhotoPath(id));
if (request->args().size() != 1) {
return webcc::ResponseBuilder{}.NotFound()();
return webcc::ResponseBuilder{}.OK()();
}
const std::string& book_id = request->args()[0];
// Delete the photo of the book.
webcc::ResponsePtr Delete(webcc::RequestPtr request) {
return {};
}
if (!g_book_store.DeleteBook(book_id)) {
return webcc::ResponseBuilder{}.NotFound()();
private:
bfs::path GetPhotoPath(const std::string& book_id) const {
return upload_dir_ / "book_photo" / (book_id + ".jpg");
}
return webcc::ResponseBuilder{}.OK()();
}
private:
bfs::path upload_dir_;
};
// -----------------------------------------------------------------------------
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "usage: rest_book_server <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);

Loading…
Cancel
Save