mirror of
https://github.com/penpot/penpot.git
synced 2025-07-23 08:57:29 +02:00
437 lines
15 KiB
Clojure
437 lines
15 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.binfile.v2
|
|
"A sqlite3 based binary file exportation with support for exportation
|
|
of entire team (or multiple teams) at once."
|
|
(:refer-clojure :exclude [read])
|
|
(:require
|
|
[app.binfile.common :as bfc]
|
|
[app.common.data :as d]
|
|
[app.common.features :as cfeat]
|
|
[app.common.logging :as l]
|
|
[app.common.transit :as t]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.db.sql :as sql]
|
|
[app.loggers.audit :as-alias audit]
|
|
[app.loggers.webhooks :as-alias webhooks]
|
|
[app.storage :as sto]
|
|
[app.storage.tmp :as tmp]
|
|
[app.util.events :as events]
|
|
[app.util.time :as dt]
|
|
[app.worker :as-alias wrk]
|
|
[clojure.set :as set]
|
|
[cuerdas.core :as str]
|
|
[datoteka.io :as io]
|
|
[promesa.util :as pu])
|
|
(:import
|
|
java.sql.DriverManager))
|
|
|
|
(set! *warn-on-reflection* true)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; LOW LEVEL API
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- create-database
|
|
([cfg]
|
|
(let [path (tmp/tempfile :prefix "penpot.binfile." :suffix ".sqlite")]
|
|
(create-database cfg path)))
|
|
([cfg path]
|
|
(let [db (DriverManager/getConnection (str "jdbc:sqlite:" path))]
|
|
(assoc cfg ::db db ::path path))))
|
|
|
|
(def ^:private
|
|
sql:create-kvdata-table
|
|
"CREATE TABLE kvdata (
|
|
tag text NOT NULL,
|
|
key text NOT NULL,
|
|
val text NOT NULL,
|
|
dat blob NULL
|
|
)")
|
|
|
|
(def ^:private
|
|
sql:create-kvdata-index
|
|
"CREATE INDEX kvdata__tag_key__idx
|
|
ON kvdata (tag, key)")
|
|
|
|
(defn- setup-schema!
|
|
[{:keys [::db]}]
|
|
(db/exec-one! db [sql:create-kvdata-table])
|
|
(db/exec-one! db [sql:create-kvdata-index]))
|
|
|
|
(defn- write!
|
|
[{:keys [::db]} tag k v & [data]]
|
|
(db/insert! db :kvdata
|
|
{:tag (d/name tag)
|
|
:key (str k)
|
|
:val (t/encode-str v {:type :json-verbose})
|
|
:dat data}
|
|
{::db/return-keys false}))
|
|
|
|
(defn- read-blob
|
|
[{:keys [::db]} tag k]
|
|
(let [obj (db/get db :kvdata
|
|
{:tag (d/name tag)
|
|
:key (str k)}
|
|
{::sql/columns [:dat]})]
|
|
(:dat obj)))
|
|
|
|
(defn- read-seq
|
|
([{:keys [::db]} tag]
|
|
(->> (db/query db :kvdata
|
|
{:tag (d/name tag)}
|
|
{::sql/columns [::val]})
|
|
(map :val)
|
|
(map t/decode-str)))
|
|
([{:keys [::db]} tag k]
|
|
(->> (db/query db :kvdata
|
|
{:tag (d/name tag)
|
|
:key (str k)}
|
|
{::sql/columns [::val]})
|
|
(map :val)
|
|
(map t/decode-str))))
|
|
|
|
(defn- read-obj
|
|
[{:keys [::db]} tag k]
|
|
(let [obj (db/get db :kvdata
|
|
{:tag (d/name tag)
|
|
:key (str k)}
|
|
{::sql/columns [:val]})]
|
|
(-> obj :val t/decode-str)))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; IMPORT/EXPORT IMPL
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^:private write-project!)
|
|
(declare ^:private write-file!)
|
|
|
|
(defn- write-team!
|
|
[cfg team-id]
|
|
|
|
(let [team (bfc/get-team cfg team-id)
|
|
fonts (bfc/get-fonts cfg team-id)]
|
|
|
|
(events/tap :progress
|
|
{:op :export
|
|
:section :write-team
|
|
:id team-id
|
|
:name (:name team)})
|
|
|
|
(l/trc :hint "write" :obj "team"
|
|
:id (str team-id)
|
|
:fonts (count fonts))
|
|
|
|
(when-let [photo-id (:photo-id team)]
|
|
(vswap! bfc/*state* update :storage-objects conj photo-id))
|
|
|
|
(vswap! bfc/*state* update :teams conj team-id)
|
|
(vswap! bfc/*state* bfc/collect-storage-objects fonts)
|
|
|
|
(write! cfg :team team-id team)
|
|
|
|
(doseq [{:keys [id] :as font} fonts]
|
|
(vswap! bfc/*state* update :team-font-variants conj id)
|
|
(write! cfg :team-font-variant id font))))
|
|
|
|
(defn- write-project!
|
|
[cfg project]
|
|
(events/tap :progress
|
|
{:op :export
|
|
:section :write-project
|
|
:id (:id project)
|
|
:name (:name project)})
|
|
(l/trc :hint "write" :obj "project" :id (str (:id project)))
|
|
(write! cfg :project (str (:id project)) project)
|
|
(vswap! bfc/*state* update :projects conj (:id project)))
|
|
|
|
(defn- write-file!
|
|
[cfg file-id]
|
|
(let [file (bfc/get-file cfg file-id)
|
|
thumbs (bfc/get-file-object-thumbnails cfg file-id)
|
|
media (bfc/get-file-media cfg file)
|
|
rels (bfc/get-files-rels cfg #{file-id})]
|
|
|
|
(events/tap :progress
|
|
{:op :export
|
|
:section :write-file
|
|
:id file-id
|
|
:name (:name file)})
|
|
|
|
(vswap! bfc/*state* (fn [state]
|
|
(-> state
|
|
(update :files conj file-id)
|
|
(update :file-media-objects into bfc/xf-map-id media)
|
|
(bfc/collect-storage-objects thumbs)
|
|
(bfc/collect-storage-objects media))))
|
|
|
|
(write! cfg :file file-id file)
|
|
(write! cfg :file-rels file-id rels)
|
|
|
|
(run! (partial write! cfg :file-media-object file-id) media)
|
|
(run! (partial write! cfg :file-object-thumbnail file-id) thumbs)
|
|
|
|
(when-let [thumb (bfc/get-file-thumbnail cfg file)]
|
|
(vswap! bfc/*state* bfc/collect-storage-objects [thumb])
|
|
(write! cfg :file-thumbnail file-id thumb))
|
|
|
|
(l/trc :hint "write" :obj "file"
|
|
:thumbnails (count thumbs)
|
|
:rels (count rels)
|
|
:media (count media))))
|
|
|
|
(defn- write-storage-object!
|
|
[{:keys [::sto/storage] :as cfg} id]
|
|
(let [sobj (sto/get-object storage id)
|
|
data (with-open [input (sto/get-object-data storage sobj)]
|
|
(io/read input))]
|
|
|
|
(l/trc :hint "write" :obj "storage-object" :id (str id) :size (:size sobj))
|
|
(write! cfg :storage-object id (meta sobj) data)))
|
|
|
|
(defn- read-storage-object!
|
|
[{:keys [::sto/storage ::bfc/timestamp] :as cfg} id]
|
|
(let [mdata (read-obj cfg :storage-object id)
|
|
data (read-blob cfg :storage-object id)
|
|
hash (sto/calculate-hash data)
|
|
|
|
content (-> (sto/content data)
|
|
(sto/wrap-with-hash hash))
|
|
|
|
params (-> mdata
|
|
(assoc ::sto/content content)
|
|
(assoc ::sto/deduplicate? true)
|
|
(assoc ::sto/touched-at timestamp))
|
|
|
|
sobject (sto/put-object! storage params)]
|
|
|
|
(vswap! bfc/*state* update :index assoc id (:id sobject))
|
|
|
|
(l/trc :hint "read" :obj "storage-object"
|
|
:id (str id)
|
|
:new-id (str (:id sobject))
|
|
:size (:size sobject))))
|
|
|
|
(defn read-team!
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} team-id]
|
|
(l/trc :hint "read" :obj "team" :id (str team-id))
|
|
|
|
(let [team (read-obj cfg :team team-id)
|
|
team (-> team
|
|
(update :id bfc/lookup-index)
|
|
(update :photo-id bfc/lookup-index)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp))]
|
|
|
|
(events/tap :progress
|
|
{:op :import
|
|
:section :read-team
|
|
:id team-id
|
|
:name (:name team)})
|
|
|
|
(db/insert! conn :team
|
|
(update team :features db/encode-pgarray conn "text")
|
|
::db/return-keys false)
|
|
|
|
(doseq [font (->> (read-seq cfg :team-font-variant)
|
|
(filter #(= team-id (:team-id %))))]
|
|
(let [font (-> font
|
|
(update :id bfc/lookup-index)
|
|
(update :team-id bfc/lookup-index)
|
|
(update :woff1-file-id bfc/lookup-index)
|
|
(update :woff2-file-id bfc/lookup-index)
|
|
(update :ttf-file-id bfc/lookup-index)
|
|
(update :otf-file-id bfc/lookup-index)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp))]
|
|
(db/insert! conn :team-font-variant font
|
|
::db/return-keys false)))
|
|
|
|
team))
|
|
|
|
(defn read-project!
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} project-id]
|
|
(l/trc :hint "read" :obj "project" :id (str project-id))
|
|
|
|
(let [project (read-obj cfg :project project-id)
|
|
project (-> project
|
|
(update :id bfc/lookup-index)
|
|
(update :team-id bfc/lookup-index)
|
|
(assoc :created-at timestamp)
|
|
(assoc :modified-at timestamp))]
|
|
|
|
(events/tap :progress
|
|
{:op :import
|
|
:section :read-project
|
|
:id project-id
|
|
:name (:name project)})
|
|
|
|
(db/insert! conn :project project
|
|
::db/return-keys false)))
|
|
|
|
(defn read-file!
|
|
[{:keys [::db/conn ::bfc/timestamp] :as cfg} file-id]
|
|
(l/trc :hint "read" :obj "file" :id (str file-id))
|
|
|
|
(let [file (-> (read-obj cfg :file file-id)
|
|
(update :id bfc/lookup-index)
|
|
(update :project-id bfc/lookup-index)
|
|
(bfc/process-file))]
|
|
|
|
(events/tap :progress
|
|
{:op :import
|
|
:section :read-file
|
|
:id file-id
|
|
:name (:name file)})
|
|
|
|
;; All features that are enabled and requires explicit migration are
|
|
;; added to the state for a posterior migration step.
|
|
(doseq [feature (-> (::bfc/features cfg)
|
|
(set/difference cfeat/no-migration-features)
|
|
(set/difference (:features file)))]
|
|
(vswap! bfc/*state* update :pending-to-migrate (fnil conj []) [feature (:id file)]))
|
|
|
|
(bfc/persist-file! cfg file))
|
|
|
|
(doseq [thumbnail (read-seq cfg :file-object-thumbnail file-id)]
|
|
(let [thumbnail (-> thumbnail
|
|
(update :file-id bfc/lookup-index)
|
|
(update :media-id bfc/lookup-index))
|
|
file-id (:file-id thumbnail)
|
|
|
|
thumbnail (update thumbnail :object-id
|
|
#(str/replace-first % #"^(.*?)/" (str file-id "/")))]
|
|
|
|
(db/insert! conn :file-tagged-object-thumbnail thumbnail
|
|
{::db/return-keys false})))
|
|
|
|
(doseq [rel (read-obj cfg :file-rels file-id)]
|
|
(let [rel (-> rel
|
|
(update :file-id bfc/lookup-index)
|
|
(update :library-file-id bfc/lookup-index)
|
|
(assoc :synced-at timestamp))]
|
|
(db/insert! conn :file-library-rel rel
|
|
::db/return-keys false)))
|
|
|
|
(doseq [media (read-seq cfg :file-media-object file-id)]
|
|
(let [media (-> media
|
|
(update :id bfc/lookup-index)
|
|
(update :file-id bfc/lookup-index)
|
|
(update :media-id bfc/lookup-index)
|
|
(update :thumbnail-id bfc/lookup-index))]
|
|
(db/insert! conn :file-media-object media
|
|
::db/return-keys false
|
|
::sql/on-conflict-do-nothing true))))
|
|
|
|
(def ^:private empty-summary
|
|
{:teams #{}
|
|
:files #{}
|
|
:projects #{}
|
|
:file-media-objects #{}
|
|
:team-font-variants #{}
|
|
:storage-objects #{}})
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; PUBLIC API
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn export-team!
|
|
[cfg team-id]
|
|
(let [id (uuid/next)
|
|
tp (dt/tpoint)
|
|
cfg (create-database cfg)]
|
|
|
|
(l/inf :hint "start"
|
|
:operation "export"
|
|
:id (str id)
|
|
:path (str (::path cfg)))
|
|
|
|
(try
|
|
(db/tx-run! cfg (fn [cfg]
|
|
(setup-schema! cfg)
|
|
(binding [bfc/*state* (volatile! empty-summary)]
|
|
(write-team! cfg team-id)
|
|
|
|
(run! (partial write-project! cfg)
|
|
(bfc/get-team-projects cfg team-id))
|
|
|
|
(run! (partial write-file! cfg)
|
|
(bfc/get-team-files-ids cfg team-id))
|
|
|
|
(run! (partial write-storage-object! cfg)
|
|
(-> bfc/*state* deref :storage-objects))
|
|
|
|
(write! cfg :manifest "team-id" team-id)
|
|
(write! cfg :manifest "objects" (deref bfc/*state*))
|
|
|
|
(::path cfg))))
|
|
(finally
|
|
(pu/close! (::db cfg))
|
|
|
|
(let [elapsed (tp)]
|
|
(l/inf :hint "end"
|
|
:operation "export"
|
|
:id (str id)
|
|
:elapsed (dt/format-duration elapsed)))))))
|
|
|
|
(defn import-team!
|
|
[cfg path]
|
|
(let [id (uuid/next)
|
|
tp (dt/tpoint)
|
|
|
|
cfg (-> (create-database cfg path)
|
|
(assoc ::bfc/timestamp (dt/now)))]
|
|
|
|
(l/inf :hint "start"
|
|
:operation "import"
|
|
:id (str id)
|
|
:path (str (::path cfg)))
|
|
|
|
(try
|
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
|
(db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"])
|
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
|
|
|
(binding [bfc/*state* (volatile! {:index {}})]
|
|
(let [objects (read-obj cfg :manifest "objects")]
|
|
|
|
;; We first process all storage objects, they have
|
|
;; deduplication so we can't rely on simple reindex. This
|
|
;; operation populates the index for all storage objects.
|
|
(run! (partial read-storage-object! cfg) (:storage-objects objects))
|
|
|
|
;; Populate index with all the incoming objects
|
|
(vswap! bfc/*state* update :index
|
|
(fn [index]
|
|
(-> index
|
|
(bfc/update-index (:teams objects))
|
|
(bfc/update-index (:projects objects))
|
|
(bfc/update-index (:files objects))
|
|
(bfc/update-index (:file-media-objects objects))
|
|
(bfc/update-index (:team-font-variants objects)))))
|
|
|
|
(let [team-id (read-obj cfg :manifest "team-id")
|
|
team (read-team! cfg team-id)
|
|
features (cfeat/get-team-enabled-features cf/flags team)
|
|
cfg (assoc cfg ::bfc/features features)]
|
|
|
|
(run! (partial read-project! cfg) (:projects objects))
|
|
(run! (partial read-file! cfg) (:files objects))
|
|
|
|
;; (run-pending-migrations! cfg)
|
|
|
|
team)))))
|
|
(finally
|
|
(pu/close! (::db cfg))
|
|
|
|
(let [elapsed (tp)]
|
|
(l/inf :hint "end"
|
|
:operation "import"
|
|
:id (str id)
|
|
:elapsed (dt/format-duration elapsed)))))))
|