;; 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 SUBSIDIARY SL (ns app.media "Media & Font postprocessing. This namespace is the dispatch layer only. Processing implementations live in two separate namespaces, each owning their own defmulti: app.media.local — shell/ImageMagick/FontForge implementations app.media.remote — HTTP delegation to media-processor service Validation and schemas live in app.media.validation (leaf namespace, no circular dep). When adding a new :cmd type, add defmethods in BOTH local and remote." (:require [app.common.data :as d] [app.common.exceptions :as ex] [app.config :as cf] [app.db :as-alias db] [app.http.client :as http] [app.media.local :as media.local] [app.media.remote :as media.remote] [app.media.sanitize :as sanitize] [app.media.validation :as validation] [app.storage :as-alias sto] [app.storage.tmp :as tmp] [cuerdas.core :as str] [datoteka.io :as io])) (defn run [system params] (if (contains? cf/flags :remote-media-processing) (media.remote/process system params) (media.local/process system params))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMAGE HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn download-image "Download an image from the provided URI and return the media input object" [{:keys [::http/client]} uri] (letfn [(parse-and-validate [{:keys [status headers] :as response}] (let [size (some-> (get headers "content-length") d/parse-integer) mtype (get headers "content-type")] (when-not (<= 200 status 299) (ex/raise :type :validation :code :unable-to-download-image :hint (str/ffmt "unable to download image from '%': unexpected status code %" uri status))) (when-not size (ex/raise :type :validation :code :unknown-size :hint "seems like the url points to resource with unknown size")) (-> {:size size :mtype mtype} (validation/validate-media-type!) (validation/validate-media-size!))))] (let [{:keys [body] :as response} (try (http/req-with-redirects client {:method :get :uri uri} {:response-type :input-stream :max-redirects 3}) (catch java.net.ConnectException cause (ex/raise :type :validation :code :unable-to-download-image :hint (str/ffmt "unable to download image from '%': connection refused or host unreachable" uri) :cause cause)) (catch java.net.http.HttpConnectTimeoutException cause (ex/raise :type :validation :code :unable-to-download-image :hint (str/ffmt "unable to download image from '%': connection timeout" uri) :cause cause)) (catch java.net.http.HttpTimeoutException cause (ex/raise :type :validation :code :unable-to-download-image :hint (str/ffmt "unable to download image from '%': request timeout" uri) :cause cause)) (catch java.io.IOException cause (ex/raise :type :validation :code :unable-to-download-image :hint (str/ffmt "unable to download image from '%': I/O error" uri) :cause cause)))] (if body (with-open [body body] (let [{:keys [size mtype]} (parse-and-validate response) path (tmp/tempfile :prefix "penpot.media.download.") written (io/write* path body :size size)] (when (not= written size) (ex/raise :type :internal :code :mismatch-write-size :hint "unexpected state: unable to write to file")) ;; Sanitize: strip trailing data after image EOF markers (let [new-size (sanitize/truncate-after-eof path mtype)] {:path path :mtype mtype :size new-size}))) ;; No body - validation will raise appropriate error (parse-and-validate response)))))