mirror of
https://github.com/penpot/penpot.git
synced 2025-05-11 02:16:37 +02:00
✨ Move media mutations to commands
This commit is contained in:
parent
1718f49a90
commit
97a884018f
8 changed files with 427 additions and 260 deletions
|
@ -287,6 +287,7 @@
|
||||||
'app.rpc.commands.management
|
'app.rpc.commands.management
|
||||||
'app.rpc.commands.verify-token
|
'app.rpc.commands.verify-token
|
||||||
'app.rpc.commands.search
|
'app.rpc.commands.search
|
||||||
|
'app.rpc.commands.media
|
||||||
'app.rpc.commands.teams
|
'app.rpc.commands.teams
|
||||||
'app.rpc.commands.auth
|
'app.rpc.commands.auth
|
||||||
'app.rpc.commands.ldap
|
'app.rpc.commands.ldap
|
||||||
|
|
274
backend/src/app/rpc/commands/media.clj
Normal file
274
backend/src/app/rpc/commands/media.clj
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
;; 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
|
||||||
|
|
||||||
|
(ns app.rpc.commands.media
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.media :as cm]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.http.client :as http]
|
||||||
|
[app.media :as media]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.storage.tmp :as tmp]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[datoteka.io :as io]
|
||||||
|
[promesa.core :as p]
|
||||||
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
|
(def default-max-file-size
|
||||||
|
(* 1024 1024 10)) ; 10 MiB
|
||||||
|
|
||||||
|
(def thumbnail-options
|
||||||
|
{:width 100
|
||||||
|
:height 100
|
||||||
|
:quality 85
|
||||||
|
:format :jpeg})
|
||||||
|
|
||||||
|
(s/def ::id ::us/uuid)
|
||||||
|
(s/def ::name ::us/string)
|
||||||
|
(s/def ::file-id ::us/uuid)
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
|
||||||
|
(defn validate-content-size!
|
||||||
|
[content]
|
||||||
|
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :media-max-file-size-reached
|
||||||
|
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
||||||
|
(:size content)
|
||||||
|
default-max-file-size))))
|
||||||
|
|
||||||
|
;; --- Create File Media object (upload)
|
||||||
|
|
||||||
|
(declare create-file-media-object)
|
||||||
|
|
||||||
|
(s/def ::content ::media/upload)
|
||||||
|
(s/def ::is-local ::us/boolean)
|
||||||
|
|
||||||
|
(s/def ::upload-file-media-object
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::name ::content]
|
||||||
|
:opt-un [::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::upload-file-media-object
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
|
||||||
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
(media/validate-media-type! content)
|
||||||
|
(validate-content-size! content)
|
||||||
|
|
||||||
|
(create-file-media-object cfg params)))
|
||||||
|
|
||||||
|
(defn- big-enough-for-thumbnail?
|
||||||
|
"Checks if the provided image info is big enough for
|
||||||
|
create a separate thumbnail storage object."
|
||||||
|
[info]
|
||||||
|
(or (> (:width info) (:width thumbnail-options))
|
||||||
|
(> (:height info) (:height thumbnail-options))))
|
||||||
|
|
||||||
|
(defn- svg-image?
|
||||||
|
[info]
|
||||||
|
(= (:mtype info) "image/svg+xml"))
|
||||||
|
|
||||||
|
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
||||||
|
;; because postgresql does not returns anything if no update is
|
||||||
|
;; performed, the `do update` does the trick.
|
||||||
|
|
||||||
|
(def sql:create-file-media-object
|
||||||
|
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
||||||
|
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
on conflict (id) do update set created_at=file_media_object.created_at
|
||||||
|
returning *")
|
||||||
|
|
||||||
|
;; NOTE: the following function executes without a transaction, this
|
||||||
|
;; means that if something fails in the middle of this function, it
|
||||||
|
;; will probably leave leaked/unreferenced objects in the database and
|
||||||
|
;; probably in the storage layer. For handle possible object leakage,
|
||||||
|
;; we create all media objects marked as touched, this ensures that if
|
||||||
|
;; something fails, all leaked (already created storage objects) will
|
||||||
|
;; be eventually marked as deleted by the touched-gc task.
|
||||||
|
;;
|
||||||
|
;; The touched-gc task, performs periodic analysis of all touched
|
||||||
|
;; storage objects and check references of it. This is the reason why
|
||||||
|
;; `reference` metadata exists: it indicates the name of the table
|
||||||
|
;; witch holds the reference to storage object (it some kind of
|
||||||
|
;; inverse, soft referential integrity).
|
||||||
|
|
||||||
|
(defn create-file-media-object
|
||||||
|
[{:keys [storage pool climit executor]}
|
||||||
|
{:keys [id file-id is-local name content]}]
|
||||||
|
(letfn [;; Function responsible to retrieve the file information, as
|
||||||
|
;; it is synchronous operation it should be wrapped into
|
||||||
|
;; with-dispatch macro.
|
||||||
|
(get-info [content]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run {:cmd :info :input content})))
|
||||||
|
|
||||||
|
;; Function responsible of calculating cryptographyc hash of
|
||||||
|
;; the provided data.
|
||||||
|
(calculate-hash [data]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(sto/calculate-hash data)))
|
||||||
|
|
||||||
|
;; Function responsible of generating thumnail. As it is synchronous
|
||||||
|
;; opetation, it should be wrapped into with-dispatch macro
|
||||||
|
(generate-thumbnail [info]
|
||||||
|
(climit/with-dispatch (:process-image climit)
|
||||||
|
(media/run (assoc thumbnail-options
|
||||||
|
:cmd :generic-thumbnail
|
||||||
|
:input info))))
|
||||||
|
|
||||||
|
(create-thumbnail [info]
|
||||||
|
(when (and (not (svg-image? info))
|
||||||
|
(big-enough-for-thumbnail? info))
|
||||||
|
(p/let [thumb (generate-thumbnail info)
|
||||||
|
hash (calculate-hash (:data thumb))
|
||||||
|
content (-> (sto/content (:data thumb) (:size thumb))
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage
|
||||||
|
{::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (dt/now)
|
||||||
|
:content-type (:mtype thumb)
|
||||||
|
:bucket "file-media-object"}))))
|
||||||
|
|
||||||
|
(create-image [info]
|
||||||
|
(p/let [data (:path info)
|
||||||
|
hash (calculate-hash data)
|
||||||
|
content (-> (sto/content data)
|
||||||
|
(sto/wrap-with-hash hash))]
|
||||||
|
(sto/put-object! storage
|
||||||
|
{::sto/content content
|
||||||
|
::sto/deduplicate? true
|
||||||
|
::sto/touched-at (dt/now)
|
||||||
|
:content-type (:mtype info)
|
||||||
|
:bucket "file-media-object"})))
|
||||||
|
|
||||||
|
(insert-into-database [info image thumb]
|
||||||
|
(px/with-dispatch executor
|
||||||
|
(db/exec-one! pool [sql:create-file-media-object
|
||||||
|
(or id (uuid/next))
|
||||||
|
file-id is-local name
|
||||||
|
(:id image)
|
||||||
|
(:id thumb)
|
||||||
|
(:width info)
|
||||||
|
(:height info)
|
||||||
|
(:mtype info)])))]
|
||||||
|
|
||||||
|
(p/let [info (get-info content)
|
||||||
|
thumb (create-thumbnail info)
|
||||||
|
image (create-image info)]
|
||||||
|
(insert-into-database info image thumb))))
|
||||||
|
|
||||||
|
;; --- Create File Media Object (from URL)
|
||||||
|
|
||||||
|
(declare ^:private create-file-media-object-from-url)
|
||||||
|
|
||||||
|
(s/def ::create-file-media-object-from-url
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::url]
|
||||||
|
:opt-un [::id ::name]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
(create-file-media-object-from-url cfg params)))
|
||||||
|
|
||||||
|
(defn- create-file-media-object-from-url
|
||||||
|
[cfg {:keys [url name] :as params}]
|
||||||
|
(letfn [(parse-and-validate-size [headers]
|
||||||
|
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||||
|
mtype (get headers "content-type")
|
||||||
|
format (cm/mtype->format mtype)
|
||||||
|
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||||
|
|
||||||
|
(when-not size
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :unknown-size
|
||||||
|
:hint "seems like the url points to resource with unknown size"))
|
||||||
|
|
||||||
|
(when (> size max-size)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :file-too-large
|
||||||
|
:hint (str/ffmt "the file size % is greater than the maximum %"
|
||||||
|
size
|
||||||
|
default-max-file-size)))
|
||||||
|
|
||||||
|
(when (nil? format)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :media-type-not-allowed
|
||||||
|
:hint "seems like the url points to an invalid media object"))
|
||||||
|
|
||||||
|
{:size size
|
||||||
|
:mtype mtype
|
||||||
|
:format format}))
|
||||||
|
|
||||||
|
(download-media [uri]
|
||||||
|
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
||||||
|
(p/then process-response)))
|
||||||
|
|
||||||
|
(process-response [{:keys [body headers] :as response}]
|
||||||
|
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
||||||
|
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||||
|
written (io/write-to-file! body path :size size)]
|
||||||
|
|
||||||
|
(when (not= written size)
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :mismatch-write-size
|
||||||
|
:hint "unexpected state: unable to write to file"))
|
||||||
|
|
||||||
|
{:filename "tempfile"
|
||||||
|
:size size
|
||||||
|
:path path
|
||||||
|
:mtype mtype}))]
|
||||||
|
|
||||||
|
(p/let [content (download-media url)]
|
||||||
|
(->> (merge params {:content content :name (or name (:filename content))})
|
||||||
|
(create-file-media-object cfg)))))
|
||||||
|
|
||||||
|
;; --- Clone File Media object (Upload and create from url)
|
||||||
|
|
||||||
|
(declare clone-file-media-object)
|
||||||
|
|
||||||
|
(s/def ::clone-file-media-object
|
||||||
|
(s/keys :req [::rpc/profile-id]
|
||||||
|
:req-un [::file-id ::is-local ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::clone-file-media-object
|
||||||
|
{::doc/added "1.17"}
|
||||||
|
[{:keys [pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
(-> (assoc cfg :conn conn)
|
||||||
|
(clone-file-media-object params))))
|
||||||
|
|
||||||
|
(defn clone-file-media-object
|
||||||
|
[{:keys [conn]} {:keys [id file-id is-local]}]
|
||||||
|
(let [mobj (db/get-by-id conn :file-media-object id)]
|
||||||
|
(db/insert! conn :file-media-object
|
||||||
|
{:id (uuid/next)
|
||||||
|
:file-id file-id
|
||||||
|
:is-local is-local
|
||||||
|
:name (:name mobj)
|
||||||
|
:media-id (:media-id mobj)
|
||||||
|
:thumbnail-id (:thumbnail-id mobj)
|
||||||
|
:width (:width mobj)
|
||||||
|
:height (:height mobj)
|
||||||
|
:mtype (:mtype mobj)})))
|
|
@ -6,280 +6,49 @@
|
||||||
|
|
||||||
(ns app.rpc.mutations.media
|
(ns app.rpc.mutations.media
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.media :as cm]
|
|
||||||
[app.common.spec :as us]
|
|
||||||
[app.common.uuid :as uuid]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.client :as http]
|
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.media :as cmd.media]
|
||||||
[app.storage :as sto]
|
[app.rpc.doc :as-alias doc]
|
||||||
[app.storage.tmp :as tmp]
|
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[clojure.spec.alpha :as s]))
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[cuerdas.core :as str]
|
|
||||||
[datoteka.io :as io]
|
|
||||||
[promesa.core :as p]
|
|
||||||
[promesa.exec :as px]))
|
|
||||||
|
|
||||||
(def default-max-file-size (* 1024 1024 10)) ; 10 MiB
|
|
||||||
|
|
||||||
(def thumbnail-options
|
|
||||||
{:width 100
|
|
||||||
:height 100
|
|
||||||
:quality 85
|
|
||||||
:format :jpeg})
|
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
|
||||||
(s/def ::name ::us/string)
|
|
||||||
(s/def ::profile-id ::us/uuid)
|
|
||||||
(s/def ::file-id ::us/uuid)
|
|
||||||
(s/def ::team-id ::us/uuid)
|
|
||||||
|
|
||||||
;; --- Create File Media object (upload)
|
;; --- Create File Media object (upload)
|
||||||
|
|
||||||
(declare create-file-media-object)
|
(s/def ::upload-file-media-object ::cmd.media/upload-file-media-object)
|
||||||
(declare select-file)
|
|
||||||
|
|
||||||
(s/def ::content ::media/upload)
|
|
||||||
(s/def ::is-local ::us/boolean)
|
|
||||||
|
|
||||||
(s/def ::upload-file-media-object
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::name ::content]
|
|
||||||
:opt-un [::id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::upload-file-media-object
|
(sv/defmethod ::upload-file-media-object
|
||||||
|
{::doc/added "1.2"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id content] :as params}]
|
||||||
(let [file (select-file pool file-id)
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
cfg (update cfg :storage media/configure-assets-storage)]
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
|
|
||||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
|
||||||
(media/validate-media-type! content)
|
(media/validate-media-type! content)
|
||||||
|
(cmd.media/validate-content-size! content)
|
||||||
(when (> (:size content) (cf/get :media-max-file-size default-max-file-size))
|
(cmd.media/create-file-media-object cfg params)))
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :media-max-file-size-reached
|
|
||||||
:hint (str/ffmt "the uploaded file size % is greater than the maximum %"
|
|
||||||
(:size content)
|
|
||||||
default-max-file-size)))
|
|
||||||
|
|
||||||
(create-file-media-object cfg params)))
|
|
||||||
|
|
||||||
(defn- big-enough-for-thumbnail?
|
|
||||||
"Checks if the provided image info is big enough for
|
|
||||||
create a separate thumbnail storage object."
|
|
||||||
[info]
|
|
||||||
(or (> (:width info) (:width thumbnail-options))
|
|
||||||
(> (:height info) (:height thumbnail-options))))
|
|
||||||
|
|
||||||
(defn- svg-image?
|
|
||||||
[info]
|
|
||||||
(= (:mtype info) "image/svg+xml"))
|
|
||||||
|
|
||||||
;; NOTE: we use the `on conflict do update` instead of `do nothing`
|
|
||||||
;; because postgresql does not returns anything if no update is
|
|
||||||
;; performed, the `do update` does the trick.
|
|
||||||
|
|
||||||
(def sql:create-file-media-object
|
|
||||||
"insert into file_media_object (id, file_id, is_local, name, media_id, thumbnail_id, width, height, mtype)
|
|
||||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
on conflict (id) do update set created_at=file_media_object.created_at
|
|
||||||
returning *")
|
|
||||||
|
|
||||||
;; NOTE: the following function executes without a transaction, this
|
|
||||||
;; means that if something fails in the middle of this function, it
|
|
||||||
;; will probably leave leaked/unreferenced objects in the database and
|
|
||||||
;; probably in the storage layer. For handle possible object leakage,
|
|
||||||
;; we create all media objects marked as touched, this ensures that if
|
|
||||||
;; something fails, all leaked (already created storage objects) will
|
|
||||||
;; be eventually marked as deleted by the touched-gc task.
|
|
||||||
;;
|
|
||||||
;; The touched-gc task, performs periodic analysis of all touched
|
|
||||||
;; storage objects and check references of it. This is the reason why
|
|
||||||
;; `reference` metadata exists: it indicates the name of the table
|
|
||||||
;; witch holds the reference to storage object (it some kind of
|
|
||||||
;; inverse, soft referential integrity).
|
|
||||||
|
|
||||||
(defn create-file-media-object
|
|
||||||
[{:keys [storage pool climit executor] :as cfg}
|
|
||||||
{:keys [id file-id is-local name content] :as params}]
|
|
||||||
(letfn [;; Function responsible to retrieve the file information, as
|
|
||||||
;; it is synchronous operation it should be wrapped into
|
|
||||||
;; with-dispatch macro.
|
|
||||||
(get-info [content]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run {:cmd :info :input content})))
|
|
||||||
|
|
||||||
;; Function responsible of calculating cryptographyc hash of
|
|
||||||
;; the provided data.
|
|
||||||
(calculate-hash [data]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(sto/calculate-hash data)))
|
|
||||||
|
|
||||||
;; Function responsible of generating thumnail. As it is synchronous
|
|
||||||
;; opetation, it should be wrapped into with-dispatch macro
|
|
||||||
(generate-thumbnail [info]
|
|
||||||
(climit/with-dispatch (:process-image climit)
|
|
||||||
(media/run (assoc thumbnail-options
|
|
||||||
:cmd :generic-thumbnail
|
|
||||||
:input info))))
|
|
||||||
|
|
||||||
(create-thumbnail [info]
|
|
||||||
(when (and (not (svg-image? info))
|
|
||||||
(big-enough-for-thumbnail? info))
|
|
||||||
(p/let [thumb (generate-thumbnail info)
|
|
||||||
hash (calculate-hash (:data thumb))
|
|
||||||
content (-> (sto/content (:data thumb) (:size thumb))
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage
|
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype thumb)
|
|
||||||
:bucket "file-media-object"}))))
|
|
||||||
|
|
||||||
(create-image [info]
|
|
||||||
(p/let [data (:path info)
|
|
||||||
hash (calculate-hash data)
|
|
||||||
content (-> (sto/content data)
|
|
||||||
(sto/wrap-with-hash hash))]
|
|
||||||
(sto/put-object! storage
|
|
||||||
{::sto/content content
|
|
||||||
::sto/deduplicate? true
|
|
||||||
::sto/touched-at (dt/now)
|
|
||||||
:content-type (:mtype info)
|
|
||||||
:bucket "file-media-object"})))
|
|
||||||
|
|
||||||
(insert-into-database [info image thumb]
|
|
||||||
(px/with-dispatch executor
|
|
||||||
(db/exec-one! pool [sql:create-file-media-object
|
|
||||||
(or id (uuid/next))
|
|
||||||
file-id is-local name
|
|
||||||
(:id image)
|
|
||||||
(:id thumb)
|
|
||||||
(:width info)
|
|
||||||
(:height info)
|
|
||||||
(:mtype info)])))]
|
|
||||||
|
|
||||||
(p/let [info (get-info content)
|
|
||||||
thumb (create-thumbnail info)
|
|
||||||
image (create-image info)]
|
|
||||||
(insert-into-database info image thumb))))
|
|
||||||
|
|
||||||
;; --- Create File Media Object (from URL)
|
;; --- Create File Media Object (from URL)
|
||||||
|
|
||||||
(declare ^:private create-file-media-object-from-url)
|
(s/def ::create-file-media-object-from-url ::cmd.media/create-file-media-object-from-url)
|
||||||
|
|
||||||
(s/def ::create-file-media-object-from-url
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::url]
|
|
||||||
:opt-un [::id ::name]))
|
|
||||||
|
|
||||||
(sv/defmethod ::create-file-media-object-from-url
|
(sv/defmethod ::create-file-media-object-from-url
|
||||||
|
{::doc/added "1.3"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(let [file (select-file pool file-id)
|
(let [cfg (update cfg :storage media/configure-assets-storage)]
|
||||||
cfg (update cfg :storage media/configure-assets-storage)]
|
(files/check-edition-permissions! pool profile-id file-id)
|
||||||
(teams/check-edition-permissions! pool profile-id (:team-id file))
|
(#'cmd.media/create-file-media-object-from-url cfg params)))
|
||||||
(create-file-media-object-from-url cfg params)))
|
|
||||||
|
|
||||||
(defn- create-file-media-object-from-url
|
|
||||||
[cfg {:keys [url name] :as params}]
|
|
||||||
(letfn [(parse-and-validate-size [headers]
|
|
||||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
|
||||||
mtype (get headers "content-type")
|
|
||||||
format (cm/mtype->format mtype)
|
|
||||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
|
||||||
|
|
||||||
(when-not size
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unknown-size
|
|
||||||
:hint "seems like the url points to resource with unknown size"))
|
|
||||||
|
|
||||||
(when (> size max-size)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :file-too-large
|
|
||||||
:hint (str/ffmt "the file size % is greater than the maximum %"
|
|
||||||
size
|
|
||||||
default-max-file-size)))
|
|
||||||
|
|
||||||
(when (nil? format)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :media-type-not-allowed
|
|
||||||
:hint "seems like the url points to an invalid media object"))
|
|
||||||
|
|
||||||
{:size size
|
|
||||||
:mtype mtype
|
|
||||||
:format format}))
|
|
||||||
|
|
||||||
(download-media [uri]
|
|
||||||
(-> (http/req! cfg {:method :get :uri uri} {:response-type :input-stream})
|
|
||||||
(p/then process-response)))
|
|
||||||
|
|
||||||
(process-response [{:keys [body headers] :as response}]
|
|
||||||
(let [{:keys [size mtype]} (parse-and-validate-size headers)
|
|
||||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
|
||||||
written (io/write-to-file! body path :size size)]
|
|
||||||
|
|
||||||
(when (not= written size)
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :mismatch-write-size
|
|
||||||
:hint "unexpected state: unable to write to file"))
|
|
||||||
|
|
||||||
{:filename "tempfile"
|
|
||||||
:size size
|
|
||||||
:path path
|
|
||||||
:mtype mtype}))]
|
|
||||||
|
|
||||||
(p/let [content (download-media url)]
|
|
||||||
(->> (merge params {:content content :name (or name (:filename content))})
|
|
||||||
(create-file-media-object cfg)))))
|
|
||||||
|
|
||||||
;; --- Clone File Media object (Upload and create from url)
|
;; --- Clone File Media object (Upload and create from url)
|
||||||
|
|
||||||
(declare clone-file-media-object)
|
(s/def ::clone-file-media-object ::cmd.media/clone-file-media-object)
|
||||||
|
|
||||||
(s/def ::clone-file-media-object
|
|
||||||
(s/keys :req-un [::profile-id ::file-id ::is-local ::id]))
|
|
||||||
|
|
||||||
(sv/defmethod ::clone-file-media-object
|
(sv/defmethod ::clone-file-media-object
|
||||||
|
{::doc/added "1.2"
|
||||||
|
::doc/deprecated "1.17"}
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [file (select-file conn file-id)]
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
(-> (assoc cfg :conn conn)
|
||||||
(-> (assoc cfg :conn conn)
|
(cmd.media/clone-file-media-object params))))
|
||||||
(clone-file-media-object params)))))
|
|
||||||
|
|
||||||
(defn clone-file-media-object
|
|
||||||
[{:keys [conn] :as cfg} {:keys [id file-id is-local]}]
|
|
||||||
(let [mobj (db/get-by-id conn :file-media-object id)]
|
|
||||||
(db/insert! conn :file-media-object
|
|
||||||
{:id (uuid/next)
|
|
||||||
:file-id file-id
|
|
||||||
:is-local is-local
|
|
||||||
:name (:name mobj)
|
|
||||||
:media-id (:media-id mobj)
|
|
||||||
:thumbnail-id (:thumbnail-id mobj)
|
|
||||||
:width (:width mobj)
|
|
||||||
:height (:height mobj)
|
|
||||||
:mtype (:mtype mobj)})))
|
|
||||||
|
|
||||||
;; --- HELPERS
|
|
||||||
|
|
||||||
(def ^:private
|
|
||||||
sql:select-file
|
|
||||||
"select file.*,
|
|
||||||
project.team_id as team_id
|
|
||||||
from file
|
|
||||||
inner join project on (project.id = file.project_id)
|
|
||||||
where file.id = ?")
|
|
||||||
|
|
||||||
(defn- select-file
|
|
||||||
[conn id]
|
|
||||||
(let [row (db/exec-one! conn [sql:select-file id])]
|
|
||||||
(when-not row
|
|
||||||
(ex/raise :type :not-found))
|
|
||||||
row))
|
|
||||||
|
|
|
@ -332,6 +332,7 @@
|
||||||
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
|
(let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
|
||||||
(try-on! (method-fn (-> data
|
(try-on! (method-fn (-> data
|
||||||
(dissoc ::type)
|
(dissoc ::type)
|
||||||
|
(assoc ::rpc/profile-id profile-id)
|
||||||
(d/without-nils))))))
|
(d/without-nils))))))
|
||||||
|
|
||||||
(defn query!
|
(defn query!
|
||||||
|
|
|
@ -142,9 +142,9 @@
|
||||||
|
|
||||||
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
|
(update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}]
|
||||||
(let [params {::th/type :update-file
|
(let [params {::th/type :update-file
|
||||||
|
::rpc/profile-id profile-id
|
||||||
:id file-id
|
:id file-id
|
||||||
:session-id (uuid/random)
|
:session-id (uuid/random)
|
||||||
::rpc/profile-id profile-id
|
|
||||||
:revn revn
|
:revn revn
|
||||||
:components-v2 true
|
:components-v2 true
|
||||||
:changes changes}
|
:changes changes}
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
|
|
||||||
(ns backend-tests.rpc-media-test
|
(ns backend-tests.rpc-media-test
|
||||||
(:require
|
(:require
|
||||||
[backend-tests.helpers :as th]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
[app.storage :as sto]
|
[app.storage :as sto]
|
||||||
|
[backend-tests.helpers :as th]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
[datoteka.core :as fs]))
|
[datoteka.core :as fs]))
|
||||||
|
|
||||||
|
@ -134,3 +135,123 @@
|
||||||
(t/is (= "image/jpeg" (:mtype result)))
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
(t/is (uuid? (:media-id result)))
|
(t/is (uuid? (:media-id result)))
|
||||||
(t/is (uuid? (:thumbnail-id result))))))
|
(t/is (uuid? (:thumbnail-id result))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest media-object-from-url-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg"
|
||||||
|
params {::th/type :create-file-media-object-from-url
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:url url}
|
||||||
|
out (th/command! params)]
|
||||||
|
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (= 1024 (:width result)))
|
||||||
|
(t/is (= 683 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? media-id))
|
||||||
|
(t/is (uuid? thumbnail-id))
|
||||||
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
|
mobj1 @(sto/get-object storage media-id)
|
||||||
|
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||||
|
(t/is (sto/storage-object? mobj1))
|
||||||
|
(t/is (sto/storage-object? mobj2))
|
||||||
|
(t/is (= 122785 (:size mobj1)))
|
||||||
|
;; This is because in ubuntu 21.04 generates different
|
||||||
|
;; thumbnail that in ubuntu 22.04. This hack should be removed
|
||||||
|
;; when we all use the ubuntu 22.04 devenv image.
|
||||||
|
(t/is (or (= 3302 (:size mobj2))
|
||||||
|
(= 3303 (:size mobj2))))))))
|
||||||
|
|
||||||
|
(t/deftest media-object-upload-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
mfile {:filename "sample.jpg"
|
||||||
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
|
||||||
|
params {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile}
|
||||||
|
out (th/command! params)]
|
||||||
|
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? (:error out)))
|
||||||
|
(let [{:keys [media-id thumbnail-id] :as result} (:result out)]
|
||||||
|
(t/is (= (:id file) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? media-id))
|
||||||
|
(t/is (uuid? thumbnail-id))
|
||||||
|
(let [storage (:app.storage/storage th/*system*)
|
||||||
|
mobj1 @(sto/get-object storage media-id)
|
||||||
|
mobj2 @(sto/get-object storage thumbnail-id)]
|
||||||
|
(t/is (sto/storage-object? mobj1))
|
||||||
|
(t/is (sto/storage-object? mobj2))
|
||||||
|
(t/is (= 312043 (:size mobj1)))
|
||||||
|
(t/is (= 3887 (:size mobj2)))))
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest media-object-upload-idempotency-command
|
||||||
|
(let [prof (th/create-profile* 1)
|
||||||
|
proj (th/create-project* 1 {:profile-id (:id prof)
|
||||||
|
:team-id (:default-team-id prof)})
|
||||||
|
file (th/create-file* 1 {:profile-id (:id prof)
|
||||||
|
:project-id (:default-project-id prof)
|
||||||
|
:is-shared false})
|
||||||
|
mfile {:filename "sample.jpg"
|
||||||
|
:path (th/tempfile "backend_tests/test_files/sample.jpg")
|
||||||
|
:mtype "image/jpeg"
|
||||||
|
:size 312043}
|
||||||
|
|
||||||
|
params {::th/type :upload-file-media-object
|
||||||
|
::rpc/profile-id (:id prof)
|
||||||
|
:file-id (:id file)
|
||||||
|
:is-local true
|
||||||
|
:name "testfile"
|
||||||
|
:content mfile
|
||||||
|
:id (uuid/next)}]
|
||||||
|
|
||||||
|
;; First try
|
||||||
|
(let [{:keys [result error] :as out} (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (= (:id params) (:id result)))
|
||||||
|
(t/is (= (:file-id params) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? (:media-id result)))
|
||||||
|
(t/is (uuid? (:thumbnail-id result))))
|
||||||
|
|
||||||
|
;; Second try
|
||||||
|
(let [{:keys [result error] :as out} (th/command! params)]
|
||||||
|
;; (th/print-result! out)
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (= (:id params) (:id result)))
|
||||||
|
(t/is (= (:file-id params) (:file-id result)))
|
||||||
|
(t/is (= 800 (:width result)))
|
||||||
|
(t/is (= 800 (:height result)))
|
||||||
|
(t/is (= "image/jpeg" (:mtype result)))
|
||||||
|
(t/is (uuid? (:media-id result)))
|
||||||
|
(t/is (uuid? (:thumbnail-id result))))))
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
(rx/filter (comp not svg-url?))
|
(rx/filter (comp not svg-url?))
|
||||||
(rx/map prepare)
|
(rx/map prepare)
|
||||||
(rx/mapcat #(rp/mutation! :create-file-media-object-from-url %))
|
(rx/mapcat #(rp/command! :create-file-media-object-from-url %))
|
||||||
(rx/do on-image))
|
(rx/do on-image))
|
||||||
|
|
||||||
(->> (rx/from uris)
|
(->> (rx/from uris)
|
||||||
|
|
|
@ -466,9 +466,10 @@
|
||||||
{:name (extract-name uri)
|
{:name (extract-name uri)
|
||||||
:url uri}))))
|
:url uri}))))
|
||||||
(rx/mapcat (fn [uri-data]
|
(rx/mapcat (fn [uri-data]
|
||||||
(->> (rp/mutation! (if (contains? uri-data :content)
|
(->> (rp/command! (if (contains? uri-data :content)
|
||||||
:upload-file-media-object
|
:upload-file-media-object
|
||||||
:create-file-media-object-from-url) uri-data)
|
:create-file-media-object-from-url)
|
||||||
|
uri-data)
|
||||||
;; When the image uploaded fail we skip the shape
|
;; When the image uploaded fail we skip the shape
|
||||||
;; returning `nil` will afterward not create the shape.
|
;; returning `nil` will afterward not create the shape.
|
||||||
(rx/catch #(rx/of nil))
|
(rx/catch #(rx/of nil))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue