diff options
Diffstat (limited to 'src/site.cc')
| -rw-r--r-- | src/site.cc | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/src/site.cc b/src/site.cc new file mode 100644 index 0000000..e1111de --- /dev/null +++ b/src/site.cc @@ -0,0 +1,488 @@ +#include "common.hh" + +#include "config.hh" +#include "document.hh" +#include "hasher.hh" +#include "jsutil.hh" +#include "logger.hh" +#include "send_file.hh" +#include "site.hh" +#include "static_files.hh" +#include "strutil.hh" +#include "travel.hh" +#include "urlutil.hh" +#include "weak_ptr.hh" + +#include <optional> +#include <unordered_map> + +namespace { + +const std::string kEmptyGif = std::string( + "GIF89a\x01\x00\x01\x00\x00\x00\x00\x21\xf9\x04" + "\x01\x0a\x00\x01\x00\x2c\x00\x00\x00\x00\x01\x00" + "\x01\x00\x00\x02\x02\x4c\x01\x00\x3b", 0x25); + +class SiteImpl : public Site, public Transport::Handler { +public: + SiteImpl(std::shared_ptr<Logger> logger, std::shared_ptr<TaskRunner> runner, + std::shared_ptr<Travel> travel) + : logger_(std::move(logger)), runner_(std::move(runner)), + travel_(std::move(travel)), media_sendfile_(SendFile::create()), + static_sendfile_(SendFile::create()), weak_ptr_owner_(this) {} + + bool setup(Logger* logger, Config* config) override { + trips_title_ = config->get("site.title", "Travels"); + trips_index_ = Document::create(trips_title_); + trips_index_->add_style("style/base.css"); + preload_trips_index(); + static_root_ = config->get_path("site.static_root", ""); + if (static_root_.empty()) { + logger->err( + "site.static_root must be set to directory with static files"); + return false; + } + auto threads = config->get("site.hasher_threads", 4); + if (!threads.has_value()) { + logger->err("site.hasher_threads is not a number: %s", + config->get("site.hasher_threads", "")); + return false; + } + if (threads.value() <= 0) { + logger->err("site.hasher_threads must be > 0"); + return false; + } + threads_ = threads.value(); + if (!media_sendfile_->setup(logger, config, + "site.sendfile.header", + "site.sendfile.path")) + return false; + if (!static_sendfile_->setup(logger, config, + "site.sendfile.header", + "site.sendfile.static_path")) + return false; + return true; + } + + void start() override { + assert(instance_ == 0); + + media_hasher_ = Hasher::create(logger_, runner_, threads_); + + do_start(); + } + + void reload() override { + preload_trips_index(); + + do_start(); + } + + Transport::Handler* handler() override { + return this; + } + + std::unique_ptr<Transport::Response> request( + Transport* transport, + Transport::Request const* request) override { + if (request->path() == "/") + return trips_index_->build(transport); + auto slash = request->path().find('/', 1); + if (slash == std::string::npos) { + auto trip_id = request->path().substr(1); + if (trip_.count(std::string(trip_id))) + return transport->create_redirect(std::string(request->path()) + "/", + false); + } else { + auto trip_id = std::string(request->path().substr(1, slash - 1)); + auto trip_it = trip_.find(trip_id); + if (trip_it != trip_.end()) { + auto media_id = std::string(request->path().substr(slash + 1)); + if (media_id.empty()) + return trip_it->second.index_->build(transport); + if (media_id == "viewer") + return trip_it->second.viewer_->build(transport); + if (media_id == "thumbnail") { + auto idx = str::parse_uint64( + std::string(request->query("media"))); + if (idx.has_value()) { + auto thumb_it = trip_it->second.thumbnail_.find(idx.value()); + if (thumb_it != trip_it->second.thumbnail_.end()) { + std::unique_ptr<Transport::Response> ret; + switch (thumb_it->second.type_) { + case Travel::Thumbnail::ThumbType::FILE: + ret = transport->create_ok_file(thumb_it->second.path_); + break; + case Travel::Thumbnail::ThumbType::EXIF: + ret = transport->create_ok_exif_thumbnail( + thumb_it->second.path_); + break; + } + if (ret) { + ret->add_header("Content-Type", + std::string(thumb_it->second.mime_type_)); + if (!thumb_it->second.etag_.empty()) + ret->add_header("ETag", thumb_it->second.etag_); + ret->add_header("Content-Length", + std::to_string(thumb_it->second.size_)); + return ret; + } + } + return create_empty_image(transport); + } + return transport->create_not_found(); + } + + auto media_it = trip_it->second.media_.find(media_id); + if (media_it != trip_it->second.media_.end()) { + std::string relative = trip_id + "/" + media_id; + return media_sendfile_->create_ok_file(transport, + media_it->second.path_, + relative, + media_it->second.etag_, + media_it->second.size_); + } + } + } + + auto resp = static_->request(transport, request->path()); + if (resp) + return resp; + return transport->create_not_found(); + } + +private: + struct Thumbnail { + Travel::Thumbnail::ThumbType type_; + std::filesystem::path path_; + std::string_view mime_type_; + std::string etag_; + uint64_t size_; + + explicit Thumbnail(Travel::Thumbnail const* thumbnail) + : type_(thumbnail->thumb_type()), path_(thumbnail->path()), mime_type_( + thumbnail->mime_type()), size_(thumbnail->size()) {} + }; + + struct Media { + std::filesystem::path path_; + std::string etag_; + std::optional<uint64_t> size_; + + explicit Media(std::filesystem::path path) + : path_(std::move(path)) {} + }; + + struct Trip { + std::unique_ptr<Document> index_; + std::unique_ptr<Document> viewer_; + std::unordered_map<std::string, Media> media_; + std::unordered_map<uint64_t, Thumbnail> thumbnail_; + }; + + static void weak_loaded(std::shared_ptr<WeakPtr<SiteImpl>> weak_ptr, + uint16_t instance) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->loaded(instance); + } + + static void weak_hashed_media(std::shared_ptr<WeakPtr<SiteImpl>> weak_ptr, + std::string const& trip_id, + std::string const& media_id, + size_t thumbnail_index, + std::string const& hash, uint64_t size) { + auto* ptr = weak_ptr->get(); + if (ptr) + ptr->hashed_media(trip_id, media_id, thumbnail_index, hash, size); + } + + void do_start() { + uint16_t instance = ++instance_; + + static_ = StaticFiles::create(logger_, runner_, static_sendfile_, + static_root_, threads_); + travel_->call_when_loaded( + std::bind(&SiteImpl::weak_loaded, weak_ptr_owner_.get(), + instance)); + } + + void preload_trips_index() { + auto* body = trips_index_->body(); + body->clear_content(); + body->add_tag("p", "Loading, please wait..."); + } + + void loaded(uint16_t instance) { + if (instance != instance_) + return; + + load_trips_index(); + + load_trips(); + } + + void load_trips_index() { + auto* body = trips_index_->body(); + body->clear_content(); + + body->add_tag("h1", trips_title_); + + auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "trips"); + for (size_t i = 0; i < travel_->trips(); ++i) { + auto& trip = travel_->trip(i); + auto* item = list->add_tag("li")->attr("class", "trip"); + item->add_tag( + "a", + std::string(trip.title()) + " - " + std::to_string(trip.year())) + ->attr("href", url::escape(trip.id(), + url::EscapeFlags::KEEP_SLASH) + "/"); + item->add_tag("br"); + std::string extra; + if (trip.images() > 0) { + if (trip.images() > 1) + extra += std::to_string(trip.images()) + " images"; + else + extra += "1 image"; + } + if (trip.videos() > 0) { + if (!extra.empty()) + extra += ", "; + if (trip.videos() > 1) + extra += std::to_string(trip.videos()) + " videos"; + else + extra += "1 video"; + } + item->add_tag("span", extra)->attr("class", "subtitle"); + if (!trip.location().empty()) { + item->add(" "); + item->add_tag("a", "Map") + ->attr("class", "maps") + ->attr("href", maps_url(trip.location())) + ->attr("target", "_blank"); + } + } + } + + void load_trips() { + trip_.clear(); + + for (size_t i = 0; i < travel_->trips(); ++i) { + load_trip(travel_->trip(i)); + } + } + + void load_trip_index(Travel::Trip const& trip, Trip& site_trip) { + site_trip.index_ = Document::create(std::string(trip.title())); + site_trip.index_->add_style("../style/base.css"); + + auto* body = site_trip.index_->body(); + + body->add_tag("h1", std::string(trip.title())); + + auto* list = body->add_tag("p")->add_tag("ul")->attr("class", "days"); + for (size_t i = 0; i < trip.day_count(); ++i) { + auto& day = trip.day(i); + auto* item = list->add_tag("li")->attr("class", "day"); + item->add_tag("h4", day.date().to_format("%d/%m")); + for (size_t j = day.first(); j <= day.last(); ++j) { + auto& media = trip.media(j); + auto* link = item->add_tag("a"); + link->attr("href", "viewer?media=" + std::to_string(j)); + link->attr("title", media.date().to_format("%H:%M")); + std::string type; + switch (media.type()) { + case Travel::Media::Type::IMAGE: + type = "image"; + break; + case Travel::Media::Type::VIDEO: + type = "video"; + break; + } + auto* img = link->add_tag("img")->attr("class", "thumbnail " + type) + ->attr("width", "64")->attr("height", "64"); + if (media.thumbnail()) + img->attr("src", "thumbnail?media=" + std::to_string(j)); + item->add(" "); + } + } + } + + void load_trip_viewer(Travel::Trip const& trip, Trip& site_trip) { + site_trip.viewer_ = Document::create(std::string(trip.title())); + site_trip.viewer_->add_style("../style/base.css"); + site_trip.viewer_->add_style("../style/viewer.css"); + site_trip.viewer_->add_script("../js/media.js"); + + { + auto script = Tag::create("script"); + script->attr("type", "text/javascript"); + std::string content = "\n"; + for (size_t i = 0; i < trip.media_count(); ++i) { + auto& media = trip.media(i); + std::vector<std::string> args; + site_trip.media_.emplace(media.id(), media.path()); + if (media.thumbnail()) + site_trip.thumbnail_.emplace(i, media.thumbnail()); + media_hasher_->hash(media.path(), + std::bind(&SiteImpl::weak_hashed_media, + weak_ptr_owner_.get(), + std::string(trip.id()), + std::string(media.id()), + i, + std::placeholders::_1, + std::placeholders::_2)); + args.push_back(js::quote(url::escape(media.id(), + url::EscapeFlags::KEEP_SLASH))); + args.push_back(std::to_string(media.width())); + args.push_back(std::to_string(media.height())); + if (media.location().empty()) + args.push_back("[]"); + else + args.push_back("[" + std::to_string(media.location().lat) + "," + + std::to_string(media.location().lng) + "]"); + args.push_back( + "new Date(" + js::quote( + media.date().to_format("%Y, %d, %m, %H, %M, %S")) + ")"); + switch (media.type()) { + case Travel::Media::Type::IMAGE: + switch (media.rotation()) { + case Rotation::UNKNOWN: + case Rotation::NONE: + args.push_back(js::quote("")); + break; + case Rotation::MIRRORED: + args.push_back(js::quote("scaleX(-1)")); + break; + case Rotation::ROTATED_90: + args.push_back(js::quote("scaleX(-1) rotate(-90deg)")); + break; + case Rotation::ROTATED_90_MIRRORED: + args.push_back(js::quote("rotate(-90deg)")); + break; + case Rotation::ROTATED_180: + args.push_back(js::quote("scaleY(-1)")); + break; + case Rotation::ROTATED_180_MIRRORED: + args.push_back(js::quote("scaleX(-1) scaleY(-1)")); + break; + case Rotation::ROTATED_270: + args.push_back(js::quote("scaleX(-1) rotate(90deg)")); + break; + case Rotation::ROTATED_270_MIRRORED: + args.push_back(js::quote("rotate(90deg)")); + break; + } + content += "media.push(new Image("; + str::join(args, ", ", content); + content += "));\n"; + break; + case Travel::Media::Type::VIDEO: + args.push_back(std::to_string(media.length())); + content += "media.push(new Video("; + str::join(args, ", ", content); + content += "));\n"; + break; + } + } + script->add(std::move(content)); + site_trip.viewer_->add_script(std::move(script)); + } + + auto* body = site_trip.viewer_->body(); + + auto* nav = body->add_tag("div")->attr("id", "header")->add_tag("ul"); + { + auto* li = nav->add_tag("li"); + li->add_tag("a", "D") + ->attr("id", "download") + ->attr("download", "") + ->attr("title", "Download [Shift-D]"); + li->add_tag("a", "P")->attr("id", "location") + ->attr("title", "Show location"); + } + nav->add_tag("li")->add_tag("a", "<") + ->attr("id", "previous") + ->attr("title", "Previous [Left Arrow]"); + nav->add_tag("li")->attr("id", "number"); + nav->add_tag("li")->add_tag("a", ">") + ->attr("id", "next") + ->attr("title", "Next [Right Arrow]"); + nav->add_tag("li")->add_tag("a", "X") + ->attr("id", "close") + ->attr("title", "Close [Escape]"); + body->add_tag("img")->attr("class", "content"); + body->add_tag("video")->attr("class", "content")->attr("controls", ""); + } + + void load_trip(Travel::Trip const& trip) { + Trip site_trip; + + load_trip_index(trip, site_trip); + load_trip_viewer(trip, site_trip); + + trip_.emplace(trip.id(), std::move(site_trip)); + } + + static std::string maps_url(Location location) { + std::string ret("https://google.com/maps?q="); + ret += std::to_string(location.lat); + ret.push_back(','); + ret += std::to_string(location.lng); + return ret; + } + + void hashed_media(std::string const& trip_id, + std::string const& media_id, + size_t thumbnail_index, + std::string const& hash, uint64_t size) { + if (hash.empty()) + return; + + auto trip_it = trip_.find(trip_id); + if (trip_it == trip_.end()) + return; + auto media_it = trip_it->second.media_.find(media_id); + if (media_it == trip_it->second.media_.end()) + return; + media_it->second.etag_ = "\"" + hash + "\""; + media_it->second.size_ = size; + auto thumbnail_it = trip_it->second.thumbnail_.find(thumbnail_index); + if (thumbnail_it == trip_it->second.thumbnail_.end()) + return; + thumbnail_it->second.etag_ = "\"" + hash + "-exif-thumb\""; + } + + std::unique_ptr<Transport::Response> create_empty_image( + Transport* transport) { + auto ret = transport->create_ok_data(kEmptyGif); + ret->add_header("Content-Type", "image/gif"); + ret->add_header("ETag", "\"bb229a48bee31f5d54ca12dc9bd960c63a671f" + "0d4be86a054c1d324a44499d96\""); + return ret; + } + + std::shared_ptr<Logger> logger_; + std::shared_ptr<TaskRunner> runner_; + std::shared_ptr<Travel> travel_; + size_t threads_{1}; + uint16_t instance_{0}; + std::string trips_title_; + std::unique_ptr<Document> trips_index_; + std::unordered_map<std::string, Trip> trip_; + std::shared_ptr<SendFile> media_sendfile_; + std::unique_ptr<Hasher> media_hasher_; + std::filesystem::path static_root_; + std::shared_ptr<SendFile> static_sendfile_; + std::unique_ptr<StaticFiles> static_; + WeakPtrOwner<SiteImpl> weak_ptr_owner_; +}; + +} // namespace + +std::unique_ptr<Site> Site::create(std::shared_ptr<Logger> logger, + std::shared_ptr<TaskRunner> runner, + std::shared_ptr<Travel> travel) { + return std::make_unique<SiteImpl>(std::move(logger), std::move(runner), + std::move(travel)); +} |
