summaryrefslogtreecommitdiff
path: root/src/site.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/site.cc')
-rw-r--r--src/site.cc488
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));
+}