penpot/backend/src/app/media.clj
2026-08-25 11:55:10 +02:00

112 lines
4.5 KiB
Clojure

;; 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)))))