;; This Source Code Form is subject to the terms of the Mozilla Public ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; ;; Copyright (c) KALEIDOS INC Sucursal en EspaƱa SL (ns app.http.assets "Assets related handlers." (:require [app.binfile.common :as bfc] [app.common.data :as d] [app.common.exceptions :as ex] [app.common.time :as ct] [app.common.uri :as u] [app.db :as db] [app.http.access-token :as actoken] [app.http.session :as session] [app.storage :as sto] [integrant.core :as ig] [yetti.response :as-alias yres])) (def ^:private default-cache-max-age (ct/duration {:hours 24})) (def ^:private default-signature-max-age (ct/duration {:hours 24 :minutes 15})) ;; Buckets that are legitimately public and do not require authentication. ;; These are used by public shared board viewing, profile photos in UI, ;; and embedded export/binfile flows. (def ^:private public-buckets #{"file-media-object" "file-object-thumbnail" "team-font-variant" "file-data-fragment" "organization"}) (defn get-id [{:keys [path-params]}] (or (some-> path-params :id d/parse-uuid) (ex/raise :type :not-found :hint "object not found"))) (defn- get-file-media-object [pool id] (db/get* pool :file-media-object {:id id} {::db/remove-deleted false})) (defn- serve-object-from-s3 [{:keys [::sto/storage ::signature-max-age ::cache-max-age] :as cfg} obj] (let [sig-max-age (or signature-max-age default-signature-max-age) cch-max-age (or cache-max-age default-cache-max-age) {:keys [host port] :as url} (sto/get-object-url storage obj {:max-age sig-max-age}) bucket (-> obj meta :bucket) headers (cond-> {"location" (str url) "x-host" (cond-> host port (str ":" port)) "x-mtype" (-> obj meta :content-type) "cache-control" (str "max-age=" (inst-ms cch-max-age))} (not (contains? public-buckets bucket)) (assoc "content-disposition" "attachment"))] {::yres/status 307 ::yres/headers headers})) (defn- serve-object-from-fs [{:keys [::path ::cache-max-age]} obj] (let [cch-max-age (or cache-max-age default-cache-max-age) purl (u/join (u/uri path) (sto/object->relative-path obj)) mdata (meta obj) bucket (:bucket mdata) headers (cond-> {"x-accel-redirect" (:path purl) "content-type" (:content-type mdata) "cache-control" (str "max-age=" (inst-ms cch-max-age))} (not (contains? public-buckets bucket)) (assoc "content-disposition" "attachment"))] {::yres/status 204 ::yres/headers headers})) (defn- serve-object "Helper function that returns the appropriate response depending on the storage object backend type." [cfg {:keys [backend] :as obj}] (case backend (:s3 :assets-s3) (serve-object-from-s3 cfg obj) (:fs :assets-fs) (serve-object-from-fs cfg obj))) (defn- requires-auth? "Check if the storage object requires authentication based on its bucket." [obj] (let [bucket (-> obj meta :bucket)] (not (contains? public-buckets bucket)))) (defn- authenticated? "Check if the request has an authenticated profile, either via session or access token." [request] (or (some? (::session/profile-id request)) (some? (::actoken/profile-id request)))) (defn objects-handler "Handler that serves storage objects by id. For non-public buckets (e.g. profile), requires authentication via session cookie or access token." [{:keys [::sto/storage] :as cfg} request] (let [id (get-id request) obj (sto/get-object storage id)] (cond (nil? obj) {::yres/status 404} (and (requires-auth? obj) (not (authenticated? request))) {::yres/status 401} :else (serve-object cfg obj)))) (defn- generic-handler "A generic handler helper/common code for file-media based handlers." [{:keys [::sto/storage] :as cfg} request kf] (let [pool (::db/pool storage) id (get-id request) mobj (get-file-media-object pool id)] (if (nil? mobj) {::yres/status 404} (let [file-id (:file-id mobj) profile-id (or (::session/profile-id request) (::actoken/profile-id request)) perms (bfc/get-file-permissions pool profile-id file-id)] (if-not (:can-read perms) {::yres/status 404} (let [sobj (sto/get-object storage (kf mobj))] (if sobj (serve-object cfg sobj) {::yres/status 404}))))))) (defn file-objects-handler "Handler that serves storage objects by file media id." [cfg request] (generic-handler cfg request :media-id)) (defn file-thumbnails-handler "Handler that serves storage objects by thumbnail-id and quick fallback to file-media-id if no thumbnail is available." [cfg request] (generic-handler cfg request #(or (:thumbnail-id %) (:media-id %)))) ;; --- Initialization (defmethod ig/assert-key ::routes [_ params] (assert (sto/valid-storage? (::sto/storage params)) "expected valid storage instance") (assert (session/manager? (::session/manager params)) "expected valid session manager") (assert (string? (::path params)))) (defmethod ig/init-key ::routes [_ cfg] ["/assets" {:middleware [[session/authz cfg] [actoken/authz cfg]]} ["/by-id/:id" {:handler (partial objects-handler cfg)}] ["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}] ["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]])