From 97a884018fc02d77aa4004ceec5c71fcacfb85e1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 2 Jan 2023 17:31:47 +0100 Subject: [PATCH] :sparkles: Move media mutations to commands --- backend/src/app/rpc.clj | 1 + backend/src/app/rpc/commands/media.clj | 274 +++++++++++++++++ backend/src/app/rpc/mutations/media.clj | 277 ++---------------- backend/test/backend_tests/helpers.clj | 1 + backend/test/backend_tests/rpc_file_test.clj | 2 +- backend/test/backend_tests/rpc_media_test.clj | 123 +++++++- .../src/app/main/data/workspace/media.cljs | 2 +- .../app/main/data/workspace/svg_upload.cljs | 7 +- 8 files changed, 427 insertions(+), 260 deletions(-) create mode 100644 backend/src/app/rpc/commands/media.clj diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 3700a9321..a77ddcc94 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -287,6 +287,7 @@ 'app.rpc.commands.management 'app.rpc.commands.verify-token 'app.rpc.commands.search + 'app.rpc.commands.media 'app.rpc.commands.teams 'app.rpc.commands.auth 'app.rpc.commands.ldap diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj new file mode 100644 index 000000000..e22a7bb85 --- /dev/null +++ b/backend/src/app/rpc/commands/media.clj @@ -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)}))) diff --git a/backend/src/app/rpc/mutations/media.clj b/backend/src/app/rpc/mutations/media.clj index dba890ed4..f66739549 100644 --- a/backend/src/app/rpc/mutations/media.clj +++ b/backend/src/app/rpc/mutations/media.clj @@ -6,280 +6,49 @@ (ns app.rpc.mutations.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.climit :as climit] - [app.rpc.commands.teams :as teams] - [app.storage :as sto] - [app.storage.tmp :as tmp] + [app.rpc.commands.files :as files] + [app.rpc.commands.media :as cmd.media] + [app.rpc.doc :as-alias doc] [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 ::profile-id ::us/uuid) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) + [clojure.spec.alpha :as s])) ;; --- Create File Media object (upload) -(declare create-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])) +(s/def ::upload-file-media-object ::cmd.media/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}] - (let [file (select-file pool file-id) - cfg (update cfg :storage media/configure-assets-storage)] - - (teams/check-edition-permissions! pool profile-id (:team-id file)) + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) (media/validate-media-type! 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 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)))) + (cmd.media/validate-content-size! content) + (cmd.media/create-file-media-object cfg params))) ;; --- 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-un [::profile-id ::file-id ::is-local ::url] - :opt-un [::id ::name])) +(s/def ::create-file-media-object-from-url ::cmd.media/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}] - (let [file (select-file pool file-id) - cfg (update cfg :storage media/configure-assets-storage)] - (teams/check-edition-permissions! pool profile-id (:team-id file)) - (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))))) + (let [cfg (update cfg :storage media/configure-assets-storage)] + (files/check-edition-permissions! pool profile-id file-id) + (#'cmd.media/create-file-media-object-from-url cfg params))) ;; --- Clone File Media object (Upload and create from url) -(declare clone-file-media-object) - -(s/def ::clone-file-media-object - (s/keys :req-un [::profile-id ::file-id ::is-local ::id])) +(s/def ::clone-file-media-object ::cmd.media/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}] (db/with-atomic [conn pool] - (let [file (select-file conn file-id)] - (teams/check-edition-permissions! conn profile-id (:team-id file)) - (-> (assoc cfg :conn conn) - (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)) + (files/check-edition-permissions! conn profile-id file-id) + (-> (assoc cfg :conn conn) + (cmd.media/clone-file-media-object params)))) diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 29c48e116..a14dd4374 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -332,6 +332,7 @@ (let [method-fn (get-in *system* [:app.rpc/methods :mutations type])] (try-on! (method-fn (-> data (dissoc ::type) + (assoc ::rpc/profile-id profile-id) (d/without-nils)))))) (defn query! diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index b2f6eba1d..7e906dfbb 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -142,9 +142,9 @@ (update-file [{:keys [profile-id file-id changes revn] :or {revn 0}}] (let [params {::th/type :update-file + ::rpc/profile-id profile-id :id file-id :session-id (uuid/random) - ::rpc/profile-id profile-id :revn revn :components-v2 true :changes changes} diff --git a/backend/test/backend_tests/rpc_media_test.clj b/backend/test/backend_tests/rpc_media_test.clj index 5fb6f9905..138ac4d6a 100644 --- a/backend/test/backend_tests/rpc_media_test.clj +++ b/backend/test/backend_tests/rpc_media_test.clj @@ -6,10 +6,11 @@ (ns backend-tests.rpc-media-test (:require - [backend-tests.helpers :as th] [app.common.uuid :as uuid] [app.db :as db] + [app.rpc :as-alias rpc] [app.storage :as sto] + [backend-tests.helpers :as th] [clojure.test :as t] [datoteka.core :as fs])) @@ -134,3 +135,123 @@ (t/is (= "image/jpeg" (:mtype result))) (t/is (uuid? (:media-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)))))) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 086a4bb95..8fd501f59 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -93,7 +93,7 @@ (->> (rx/from uris) (rx/filter (comp not svg-url?)) (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/from uris) diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 4f34e2d20..f299fa8d6 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -466,9 +466,10 @@ {:name (extract-name uri) :url uri})))) (rx/mapcat (fn [uri-data] - (->> (rp/mutation! (if (contains? uri-data :content) - :upload-file-media-object - :create-file-media-object-from-url) uri-data) + (->> (rp/command! (if (contains? uri-data :content) + :upload-file-media-object + :create-file-media-object-from-url) + uri-data) ;; When the image uploaded fail we skip the shape ;; returning `nil` will afterward not create the shape. (rx/catch #(rx/of nil))