mirror of
https://github.com/penpot/penpot.git
synced 2025-07-23 12:37:28 +02:00
420 lines
14 KiB
Clojure
420 lines
14 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 INC
|
|
|
|
(ns app.rpc.commands.management
|
|
"A collection of RPC methods for manage the files, projects and team organization."
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.pages.migrations :as pmg]
|
|
[app.common.spec :as us]
|
|
[app.common.uuid :as uuid]
|
|
[app.db :as db]
|
|
[app.loggers.webhooks :as-alias webhooks]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.binfile :as binfile]
|
|
[app.rpc.commands.files :as files]
|
|
[app.rpc.commands.projects :as proj]
|
|
[app.rpc.commands.teams :as teams :refer [create-project-role create-project]]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.util.blob :as blob]
|
|
[app.util.pointer-map :as pmap]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[clojure.spec.alpha :as s]
|
|
[clojure.walk :as walk]))
|
|
|
|
;; --- COMMAND: Duplicate File
|
|
|
|
(declare duplicate-file)
|
|
|
|
(s/def ::id ::us/uuid)
|
|
(s/def ::project-id ::us/uuid)
|
|
(s/def ::file-id ::us/uuid)
|
|
(s/def ::team-id ::us/uuid)
|
|
(s/def ::name ::us/string)
|
|
|
|
(s/def ::duplicate-file
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::file-id]
|
|
:opt-un [::name]))
|
|
|
|
(sv/defmethod ::duplicate-file
|
|
"Duplicate a single file in the same team."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(duplicate-file conn (assoc params :profile-id profile-id))))
|
|
|
|
(defn- remap-id
|
|
[item index key]
|
|
(cond-> item
|
|
(contains? item key)
|
|
(assoc key (get index (get item key) (get item key)))))
|
|
|
|
(defn- process-file
|
|
[conn {:keys [id] :as file} index]
|
|
(letfn [(process-form [form]
|
|
(cond-> form
|
|
;; Relink library items
|
|
(and (map? form)
|
|
(uuid? (:component-file form)))
|
|
(update :component-file #(get index % %))
|
|
|
|
(and (map? form)
|
|
(uuid? (:fill-color-ref-file form)))
|
|
(update :fill-color-ref-file #(get index % %))
|
|
|
|
(and (map? form)
|
|
(uuid? (:stroke-color-ref-file form)))
|
|
(update :stroke-color-ref-file #(get index % %))
|
|
|
|
(and (map? form)
|
|
(uuid? (:typography-ref-file form)))
|
|
(update :typography-ref-file #(get index % %))
|
|
|
|
;; Relink Image Shapes
|
|
(and (map? form)
|
|
(map? (:metadata form))
|
|
(= :image (:type form)))
|
|
(update-in [:metadata :id] #(get index % %))))
|
|
|
|
;; A function responsible to analyze all file data and
|
|
;; replace the old :component-file reference with the new
|
|
;; ones, using the provided file-index
|
|
(relink-shapes [data]
|
|
(walk/postwalk process-form data))
|
|
|
|
;; A function responsible of process the :media attr of file
|
|
;; data and remap the old ids with the new ones.
|
|
(relink-media [media]
|
|
(reduce-kv (fn [res k v]
|
|
(let [id (get index k)]
|
|
(if (uuid? id)
|
|
(-> res
|
|
(assoc id (assoc v :id id))
|
|
(dissoc k))
|
|
res)))
|
|
media
|
|
media))]
|
|
(-> file
|
|
(update :id #(get index %))
|
|
(update :data
|
|
(fn [data]
|
|
(binding [pmap/*load-fn* (partial files/load-pointer conn id)
|
|
pmap/*tracked* (atom {})]
|
|
(let [file-id (get index id)
|
|
data (-> data
|
|
(blob/decode)
|
|
(assoc :id file-id)
|
|
(pmg/migrate-data)
|
|
(update :pages-index relink-shapes)
|
|
(update :components relink-shapes)
|
|
(update :media relink-media)
|
|
(d/without-nils)
|
|
(files/process-pointers pmap/clone)
|
|
(blob/encode))]
|
|
(files/persist-pointers! conn file-id)
|
|
data)))))))
|
|
|
|
(def sql:retrieve-used-libraries
|
|
"select flr.*
|
|
from file_library_rel as flr
|
|
inner join file as l on (flr.library_file_id = l.id)
|
|
where flr.file_id = ?
|
|
and l.deleted_at is null")
|
|
|
|
(def sql:retrieve-used-media-objects
|
|
"select fmo.*
|
|
from file_media_object as fmo
|
|
inner join storage_object as so on (fmo.media_id = so.id)
|
|
where fmo.file_id = ?
|
|
and so.deleted_at is null")
|
|
|
|
(defn duplicate-file*
|
|
[conn {:keys [profile-id file index project-id name flibs fmeds]} {:keys [reset-shared-flag]}]
|
|
(let [flibs (or flibs (db/exec! conn [sql:retrieve-used-libraries (:id file)]))
|
|
fmeds (or fmeds (db/exec! conn [sql:retrieve-used-media-objects (:id file)]))
|
|
|
|
;; memo uniform creation/modification date
|
|
now (dt/now)
|
|
ignore (dt/plus now (dt/duration {:seconds 5}))
|
|
|
|
;; add to the index all file media objects.
|
|
index (reduce #(assoc %1 (:id %2) (uuid/next)) index fmeds)
|
|
|
|
flibs-xf (comp
|
|
(map #(remap-id % index :file-id))
|
|
(map #(remap-id % index :library-file-id))
|
|
(map #(assoc % :synced-at now))
|
|
(map #(assoc % :created-at now)))
|
|
|
|
;; remap all file-library-rel row
|
|
flibs (sequence flibs-xf flibs)
|
|
|
|
fmeds-xf (comp
|
|
(map #(assoc % :id (get index (:id %))))
|
|
(map #(assoc % :created-at now))
|
|
(map #(remap-id % index :file-id)))
|
|
|
|
;; remap all file-media-object rows
|
|
fmeds (sequence fmeds-xf fmeds)
|
|
|
|
file (cond-> file
|
|
(some? project-id)
|
|
(assoc :project-id project-id)
|
|
|
|
(some? name)
|
|
(assoc :name name)
|
|
|
|
(true? reset-shared-flag)
|
|
(assoc :is-shared false))
|
|
|
|
file (-> file
|
|
(assoc :created-at now)
|
|
(assoc :modified-at now)
|
|
(assoc :ignore-sync-until ignore))
|
|
|
|
file (process-file conn file index)]
|
|
|
|
(db/insert! conn :file file)
|
|
(db/insert! conn :file-profile-rel
|
|
{:file-id (:id file)
|
|
:profile-id profile-id
|
|
:is-owner true
|
|
:is-admin true
|
|
:can-edit true})
|
|
|
|
(doseq [params flibs]
|
|
(db/insert! conn :file-library-rel params))
|
|
|
|
(doseq [params fmeds]
|
|
(db/insert! conn :file-media-object params))
|
|
|
|
file))
|
|
|
|
(defn duplicate-file
|
|
[conn {:keys [profile-id file-id] :as params}]
|
|
(let [file (db/get-by-id conn :file file-id)
|
|
index {file-id (uuid/next)}
|
|
params (assoc params :index index :file file)]
|
|
(proj/check-edition-permissions! conn profile-id (:project-id file))
|
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
|
(-> (duplicate-file* conn params {:reset-shared-flag true})
|
|
(update :data blob/decode)
|
|
(update :features db/decode-pgarray #{}))))
|
|
|
|
;; --- COMMAND: Duplicate Project
|
|
|
|
(declare duplicate-project)
|
|
|
|
(s/def ::duplicate-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::project-id]
|
|
:opt-un [::name]))
|
|
|
|
(sv/defmethod ::duplicate-project
|
|
"Duplicate an entire project with all the files"
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} params]
|
|
(db/with-atomic [conn pool]
|
|
(duplicate-project conn (assoc params :profile-id (::rpc/profile-id params)))))
|
|
|
|
(defn duplicate-project
|
|
[conn {:keys [profile-id project-id name] :as params}]
|
|
|
|
;; Defer all constraints
|
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
|
|
|
(let [project (db/get-by-id conn :project project-id)
|
|
|
|
files (db/query conn :file
|
|
{:project-id (:id project)
|
|
:deleted-at nil}
|
|
{:columns [:id]})
|
|
|
|
project (cond-> project
|
|
(string? name)
|
|
(assoc :name name)
|
|
|
|
:always
|
|
(assoc :id (uuid/next)))]
|
|
|
|
;; Check if the source team-id allow creating new project for current user
|
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
|
|
|
;; create the duplicated project and assign the current profile as
|
|
;; a project owner
|
|
(create-project conn project)
|
|
(create-project-role conn profile-id (:id project) :owner)
|
|
|
|
;; duplicate all files
|
|
(let [index (reduce #(assoc %1 (:id %2) (uuid/next)) {} files)
|
|
params (-> params
|
|
(dissoc :name)
|
|
(assoc :project-id (:id project))
|
|
(assoc :index index))]
|
|
(doseq [{:keys [id]} files]
|
|
(let [file (db/get-by-id conn :file id)
|
|
params (assoc params :file file)
|
|
opts {:reset-shared-flag false}]
|
|
(duplicate-file* conn params opts))))
|
|
|
|
;; return the created project
|
|
project))
|
|
|
|
;; --- COMMAND: Move file
|
|
|
|
(def sql:retrieve-files
|
|
"select id, project_id from file where id = ANY(?)")
|
|
|
|
(def sql:move-files
|
|
"update file set project_id = ? where id = ANY(?)")
|
|
|
|
(def sql:delete-broken-relations
|
|
"with broken as (
|
|
(select * from file_library_rel as flr
|
|
inner join file as f on (flr.file_id = f.id)
|
|
inner join project as p on (f.project_id = p.id)
|
|
inner join file as lf on (flr.library_file_id = lf.id)
|
|
inner join project as lp on (lf.project_id = lp.id)
|
|
where p.id = ANY(?)
|
|
and lp.team_id != p.team_id)
|
|
)
|
|
delete from file_library_rel as rel
|
|
using broken as br
|
|
where rel.file_id = br.file_id
|
|
and rel.library_file_id = br.library_file_id")
|
|
|
|
(defn move-files
|
|
[conn {:keys [profile-id ids project-id] :as params}]
|
|
|
|
(let [fids (db/create-array conn "uuid" ids)
|
|
files (db/exec! conn [sql:retrieve-files fids])
|
|
source (into #{} (map :project-id) files)
|
|
pids (->> (conj source project-id)
|
|
(db/create-array conn "uuid"))]
|
|
|
|
;; Check if we have permissions on the destination project
|
|
(proj/check-edition-permissions! conn profile-id project-id)
|
|
|
|
;; Check if we have permissions on all source projects
|
|
(doseq [project-id source]
|
|
(proj/check-edition-permissions! conn profile-id project-id))
|
|
|
|
(when (contains? source project-id)
|
|
(ex/raise :type :validation
|
|
:code :cant-move-to-same-project
|
|
:hint "Unable to move a file to the same project"))
|
|
|
|
;; move all files to the project
|
|
(db/exec-one! conn [sql:move-files project-id fids])
|
|
|
|
;; delete possible broken relations on moved files
|
|
(db/exec-one! conn [sql:delete-broken-relations pids])
|
|
|
|
nil))
|
|
|
|
(s/def ::ids (s/every ::us/uuid :kind set?))
|
|
(s/def ::move-files
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::ids ::project-id]))
|
|
|
|
(sv/defmethod ::move-files
|
|
"Move a set of files from one project to other."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(move-files conn (assoc params :profile-id profile-id))))
|
|
|
|
;; --- COMMAND: Move project
|
|
|
|
(defn move-project
|
|
[conn {:keys [profile-id team-id project-id] :as params}]
|
|
(let [project (db/get-by-id conn :project project-id {:columns [:id :team-id]})
|
|
pids (->> (db/query conn :project {:team-id (:team-id project)} {:columns [:id]})
|
|
(map :id)
|
|
(db/create-array conn "uuid"))]
|
|
|
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
|
(teams/check-edition-permissions! conn profile-id team-id)
|
|
|
|
(when (= team-id (:team-id project))
|
|
(ex/raise :type :validation
|
|
:code :cant-move-to-same-team
|
|
:hint "Unable to move a project to same team"))
|
|
|
|
;; move project to the destination team
|
|
(db/update! conn :project
|
|
{:team-id team-id}
|
|
{:id project-id})
|
|
|
|
;; delete possible broken relations on moved files
|
|
(db/exec-one! conn [sql:delete-broken-relations pids])
|
|
|
|
nil))
|
|
|
|
|
|
(s/def ::move-project
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::team-id ::project-id]))
|
|
|
|
(sv/defmethod ::move-project
|
|
"Move projects between teams."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(move-project conn (assoc params :profile-id profile-id))))
|
|
|
|
;; --- COMMAND: Clone Template
|
|
|
|
(declare clone-template)
|
|
|
|
(s/def ::template-id ::us/not-empty-string)
|
|
(s/def ::clone-template
|
|
(s/keys :req [::rpc/profile-id]
|
|
:req-un [::project-id ::template-id]))
|
|
|
|
(sv/defmethod ::clone-template
|
|
"Clone into the specified project the template by its id."
|
|
{::doc/added "1.16"
|
|
::webhooks/event? true}
|
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
|
(db/with-atomic [conn pool]
|
|
(-> (assoc cfg :conn conn)
|
|
(clone-template (assoc params :profile-id profile-id)))))
|
|
|
|
(defn- clone-template
|
|
[{:keys [conn templates] :as cfg} {:keys [profile-id template-id project-id]}]
|
|
(let [template (d/seek #(= (:id %) template-id) templates)
|
|
project (db/get-by-id conn :project project-id {:columns [:id :team-id]})]
|
|
|
|
(teams/check-edition-permissions! conn profile-id (:team-id project))
|
|
|
|
(when-not template
|
|
(ex/raise :type :not-found
|
|
:code :template-not-found
|
|
:hint "template not found"))
|
|
|
|
(-> cfg
|
|
(assoc ::binfile/input (:path template))
|
|
(assoc ::binfile/project-id (:id project))
|
|
(assoc ::binfile/ignore-index-errors? true)
|
|
(assoc ::binfile/migrate? true)
|
|
(binfile/import!))))
|
|
|
|
|
|
;; --- COMMAND: Retrieve list of builtin templates
|
|
|
|
(s/def ::retrieve-list-of-builtin-templates any?)
|
|
|
|
(sv/defmethod ::retrieve-list-of-builtin-templates
|
|
[cfg _params]
|
|
(mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg)))
|