#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 #include 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, std::shared_ptr runner, std::shared_ptr 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 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( url::escape(request->path(), url::EscapeFlags::KEEP_SLASH) + "/", 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 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 size_; explicit Media(std::filesystem::path path) : path_(std::move(path)) {} }; struct Trip { std::unique_ptr index_; std::unique_ptr viewer_; std::unordered_map media_; std::unordered_map thumbnail_; }; static void weak_loaded(std::shared_ptr> weak_ptr, uint16_t instance) { auto* ptr = weak_ptr->get(); if (ptr) ptr->loaded(instance); } static void weak_hashed_media(std::shared_ptr> 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())); body->add_tag("a", "X") ->attr("href", "..") ->attr("id", "close") ->attr("title", "Close"); 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 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("scaleX(-1) scaleY(-1)")); break; case Rotation::ROTATED_180_MIRRORED: args.push_back(js::quote("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") ->attr("id", "download") ->attr("download", "") ->attr("title", "Download [Shift-D]") ->add_tag("img") ->attr("src", "../img/download.svg"); li->add_tag("a") ->attr("id", "location") ->attr("title", "Show location") ->add_tag("img") ->attr("src", "../img/location.svg"); } 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 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_; std::shared_ptr runner_; std::shared_ptr travel_; size_t threads_{1}; uint16_t instance_{0}; std::string trips_title_; std::unique_ptr trips_index_; std::unordered_map trip_; std::shared_ptr media_sendfile_; std::unique_ptr media_hasher_; std::filesystem::path static_root_; std::shared_ptr static_sendfile_; std::unique_ptr static_; WeakPtrOwner weak_ptr_owner_; }; } // namespace std::unique_ptr Site::create(std::shared_ptr logger, std::shared_ptr runner, std::shared_ptr travel) { return std::make_unique(std::move(logger), std::move(runner), std::move(travel)); }