mirror of
https://github.com/penpot/penpot.git
synced 2025-07-28 10:37:29 +02:00
213 lines
7.2 KiB
Clojure
213 lines
7.2 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.files-snapshot
|
|
(:require
|
|
[app.common.exceptions :as ex]
|
|
[app.common.logging :as l]
|
|
[app.common.schema :as sm]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.db.sql :as-alias sql]
|
|
[app.features.fdata :as feat.fdata]
|
|
[app.main :as-alias main]
|
|
[app.rpc :as-alias rpc]
|
|
[app.rpc.commands.files :as files]
|
|
[app.rpc.commands.profile :as profile]
|
|
[app.rpc.doc :as-alias doc]
|
|
[app.storage :as sto]
|
|
[app.util.blob :as blob]
|
|
[app.util.pointer-map :as pmap]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[cuerdas.core :as str]))
|
|
|
|
(defn check-authorized!
|
|
[{:keys [::db/pool]} profile-id]
|
|
(when-not (or (= "devenv" (cf/get :host))
|
|
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
|
|
admins (or (cf/get :admins) #{})]
|
|
(contains? admins (:email profile))))
|
|
(ex/raise :type :authentication
|
|
:code :authentication-required
|
|
:hint "only admins allowed")))
|
|
|
|
(def sql:get-file-snapshots
|
|
"SELECT id, label, revn, created_at
|
|
FROM file_change
|
|
WHERE file_id = ?
|
|
AND created_at < ?
|
|
AND label IS NOT NULL
|
|
ORDER BY created_at DESC
|
|
LIMIT ?")
|
|
|
|
(defn get-file-snapshots
|
|
[{:keys [::db/conn]} {:keys [file-id limit start-at]
|
|
:or {limit Long/MAX_VALUE}}]
|
|
(let [start-at (or start-at (dt/now))
|
|
limit (min limit 20)]
|
|
(->> (db/exec! conn [sql:get-file-snapshots file-id start-at limit])
|
|
(mapv (fn [row]
|
|
(update row :created-at dt/format-instant :rfc1123))))))
|
|
|
|
(def ^:private schema:get-file-snapshots
|
|
[:map [:file-id ::sm/uuid]])
|
|
|
|
(sv/defmethod ::get-file-snapshots
|
|
{::doc/added "1.20"
|
|
::doc/skip true
|
|
::sm/params schema:get-file-snapshots}
|
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
(check-authorized! cfg profile-id)
|
|
(db/run! cfg get-file-snapshots params))
|
|
|
|
(defn restore-file-snapshot!
|
|
[{:keys [::db/conn] :as cfg} {:keys [file-id id]}]
|
|
(let [storage (sto/resolve cfg {::db/reuse-conn true})
|
|
file (files/get-minimal-file conn file-id {::db/for-update true})
|
|
snapshot (db/get* conn :file-change
|
|
{:file-id file-id
|
|
:id id}
|
|
{::db/for-share true})]
|
|
|
|
(when-not snapshot
|
|
(ex/raise :type :not-found
|
|
:code :snapshot-not-found
|
|
:hint "unable to find snapshot with the provided label"
|
|
:id id
|
|
:file-id file-id))
|
|
|
|
(let [snapshot (feat.fdata/resolve-file-data cfg snapshot)]
|
|
(when-not (:data snapshot)
|
|
(ex/raise :type :precondition
|
|
:code :snapshot-without-data
|
|
:hint "snapshot has no data"
|
|
:label (:label snapshot)
|
|
:file-id file-id))
|
|
|
|
(l/dbg :hint "restoring snapshot"
|
|
:file-id (str file-id)
|
|
:label (:label snapshot)
|
|
:snapshot-id (str (:id snapshot)))
|
|
|
|
;; If the file was already offloaded, on restring the snapshot
|
|
;; we are going to replace the file data, so we need to touch
|
|
;; the old referenced storage object and avoid possible leaks
|
|
(when (feat.fdata/offloaded? file)
|
|
(sto/touch-object! storage (:data-ref-id file)))
|
|
|
|
(db/update! conn :file
|
|
{:data (:data snapshot)
|
|
:revn (inc (:revn file))
|
|
:data-backend nil
|
|
:data-ref-id nil
|
|
:has-media-trimmed false
|
|
:features (:features snapshot)}
|
|
{:id file-id})
|
|
|
|
;; clean object thumbnails
|
|
(let [sql (str "update file_tagged_object_thumbnail "
|
|
" set deleted_at = now() "
|
|
" where file_id=? returning media_id")
|
|
res (db/exec! conn [sql file-id])]
|
|
(doseq [media-id (into #{} (keep :media-id) res)]
|
|
(sto/touch-object! storage media-id)))
|
|
|
|
;; clean file thumbnails
|
|
(let [sql (str "update file_thumbnail "
|
|
" set deleted_at = now() "
|
|
" where file_id=? returning media_id")
|
|
res (db/exec! conn [sql file-id])]
|
|
(doseq [media-id (into #{} (keep :media-id) res)]
|
|
(sto/touch-object! storage media-id)))
|
|
|
|
{:id (:id snapshot)
|
|
:label (:label snapshot)})))
|
|
|
|
(defn- resolve-snapshot-by-label
|
|
[conn file-id label]
|
|
(->> (db/query conn :file-change
|
|
{:file-id file-id
|
|
:label label}
|
|
{::sql/order-by [[:created-at :desc]]
|
|
::sql/columns [:file-id :id :label]})
|
|
(first)))
|
|
|
|
(def ^:private
|
|
schema:restore-file-snapshot
|
|
[:and
|
|
[:map
|
|
[:file-id ::sm/uuid]
|
|
[:id {:optional true} ::sm/uuid]
|
|
[:label {:optional true} :string]]
|
|
[::sm/contains-any #{:id :label}]])
|
|
|
|
(sv/defmethod ::restore-file-snapshot
|
|
{::doc/added "1.20"
|
|
::doc/skip true
|
|
::sm/params schema:restore-file-snapshot}
|
|
[cfg {:keys [::rpc/profile-id file-id id label] :as params}]
|
|
(check-authorized! cfg profile-id)
|
|
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
|
(let [params (cond-> params
|
|
(and (not id) (string? label))
|
|
(merge (resolve-snapshot-by-label conn file-id label)))]
|
|
(restore-file-snapshot! cfg params)))))
|
|
|
|
(defn- get-file
|
|
[cfg file-id]
|
|
(let [file (->> (db/get cfg :file {:id file-id})
|
|
(feat.fdata/resolve-file-data cfg))]
|
|
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
|
(-> file
|
|
(update :data blob/decode)
|
|
(update :data feat.fdata/process-pointers deref)
|
|
(update :data feat.fdata/process-objects (partial into {}))
|
|
(update :data blob/encode)))))
|
|
|
|
(defn take-file-snapshot!
|
|
[cfg {:keys [file-id label]}]
|
|
(let [file (get-file cfg file-id)
|
|
id (uuid/next)]
|
|
|
|
(l/debug :hint "creating file snapshot"
|
|
:file-id (str file-id)
|
|
:label label)
|
|
|
|
(db/insert! cfg :file-change
|
|
{:id id
|
|
:revn (:revn file)
|
|
:data (:data file)
|
|
:features (:features file)
|
|
:file-id (:id file)
|
|
:label label}
|
|
{::db/return-keys false})
|
|
|
|
{:id id :label label}))
|
|
|
|
(defn generate-snapshot-label
|
|
[]
|
|
(let [ts (-> (dt/now)
|
|
(dt/format-instant)
|
|
(str/replace #"[T:\.]" "-")
|
|
(str/rtrim "Z"))]
|
|
(str "snapshot-" ts)))
|
|
|
|
(def ^:private schema:take-file-snapshot
|
|
[:map [:file-id ::sm/uuid]])
|
|
|
|
(sv/defmethod ::take-file-snapshot
|
|
{::doc/added "1.20"
|
|
::doc/skip true
|
|
::sm/params schema:take-file-snapshot}
|
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
|
(check-authorized! cfg profile-id)
|
|
(db/tx-run! cfg (fn [cfg]
|
|
(let [params (update params :label (fn [label]
|
|
(or label (generate-snapshot-label))))]
|
|
(take-file-snapshot! cfg params)))))
|
|
|