diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 63077eec7..201e83062 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -214,6 +214,7 @@ 'app.rpc.commands.files-share 'app.rpc.commands.files-temp 'app.rpc.commands.files-update + 'app.rpc.commands.files-snapshot 'app.rpc.commands.files-thumbnails 'app.rpc.commands.ldap 'app.rpc.commands.management diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj new file mode 100644 index 000000000..b81d825de --- /dev/null +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -0,0 +1,136 @@ +;; 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.main :as-alias main] + [app.media :as media] + [app.rpc :as-alias rpc] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] + [app.storage :as sto] + [app.util.services :as sv] + [app.util.time :as dt])) + +(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"))) + +(defn get-file-snapshots + [{:keys [::db/conn]} {:keys [file-id limit start-at] + :or {limit Long/MAX_VALUE}}] + (let [query (str "select id, label, revn, created_at " + " from file_change " + " where file_id = ? " + " and created_at < ? " + " and label is not null " + " and data is not null " + " order by created_at desc " + " limit ?") + start-at (or start-at (dt/now)) + limit (min limit 20)] + + (->> (db/exec! conn [query 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 ::sto/storage] :as cfg} {:keys [file-id id]}] + (let [storage (media/configure-assets-storage storage conn) + params {:id id :file-id file-id} + options {:columns [:id :data :revn]} + snapshot (db/get* conn :file-change params options)] + + (when (and (some? snapshot) + (some? (:data snapshot))) + + (l/debug :hint "snapshot found" + :snapshot-id (:id snapshot) + :file-id file-id) + + (db/update! conn :file + {:data (:data snapshot)} + {:id file-id}) + + ;; clean object thumbnails + (let [sql (str "delete from file_object_thumbnail " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/del-object! storage media-id))) + + ;; clean object thumbnails + (let [sql (str "delete from file_thumbnail " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/del-object! storage media-id))) + + {:id (:id snapshot)}))) + +(def ^:private schema:restore-file-snapshot + [:map + [:file-id ::sm/uuid] + [:id ::sm/uuid]]) + +(sv/defmethod ::restore-file-snapshot + {::doc/added "1.20" + ::doc/skip true + ::sm/params schema:restore-file-snapshot} + [cfg {:keys [::rpc/profile-id] :as params}] + (check-authorized! cfg profile-id) + (db/tx-run! cfg #(restore-file-snapshot! % params))) + +(defn take-file-snapshot! + [{:keys [::db/conn]} {:keys [file-id label]}] + (when-let [file (db/get* conn :file {:id file-id})] + (let [id (uuid/next) + label (or label (str "Snapshot at " (dt/format-instant (dt/now) :rfc1123)))] + (l/debug :hint "persisting file snapshot" :file-id file-id :label label) + (db/insert! conn :file-change + {:id id + :revn (:revn file) + :data (:data file) + :features (:features file) + :file-id (:id file) + :label label}) + {:id id}))) + +(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 #(take-file-snapshot! % params))) + diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 6ce43b685..bd4dbfbc8 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -17,6 +17,7 @@ [app.media :as media] [app.rpc.commands.auth :as auth] [app.rpc.commands.profile :as profile] + [app.rpc.commands.files-snapshot :as fsnap] [app.srepl.fixes :as f] [app.srepl.helpers :as h] [app.storage :as sto] @@ -105,7 +106,6 @@ (db/delete! conn :http-session {:profile-id (:id profile)}) :blocked)))) - (defn enable-objects-map-feature-on-file! [system & {:keys [save? id]}] (letfn [(update-file [{:keys [features] :as file}] @@ -172,94 +172,28 @@ "An internal helper that persist the file snapshot using non-gc collectable file-changes entry." [system & {:keys [file-id label]}] - (let [label (or label (str "Snapshot at " (dt/format-instant (dt/now) :rfc1123))) - file-id (h/parse-uuid file-id) - id (uuid/next)] + (let [file-id (h/parse-uuid file-id)] (db/tx-run! system - (fn [{:keys [::db/conn]}] - (when-let [file (db/get* conn :file {:id file-id})] - (h/println! "=> persisting snapshot for" file-id) - (db/insert! conn :file-change - {:id id - :revn (:revn file) - :data (:data file) - :features (:features file) - :file-id (:id file) - :label label}) - id))))) + (fn [cfg] + (fsnap/take-file-snapshot! cfg {:file-id file-id :label label}))))) (defn restore-file-snapshot! - [system & {:keys [file-id id label]}] - (letfn [(restore-snapshot! [{:keys [::db/conn ::sto/storage]} file-id snapshot] - (when (and (some? snapshot) - (some? (:data snapshot))) + [system & {:keys [file-id id]}] + (db/tx-run! system + (fn [cfg] + (let [file-id (h/parse-uuid file-id) + id (h/parse-uuid id)] - (h/println! "-> snapshot found:" (:id snapshot)) - (h/println! "-> restoring it on file:" file-id) - (db/update! conn :file - {:data (:data snapshot)} - {:id file-id}) + (if (and (uuid? id) (uuid? file-id)) + (fsnap/restore-file-snapshot! cfg {:id id :file-id file-id}) + (println "=> invalid parameters")))))) - ;; clean object thumbnails - (let [sql (str "delete from file_object_thumbnail " - " where file_id=? returning media_id") - res (db/exec! conn [sql file-id])] - (doseq [media-id (into #{} (keep :media-id) res)] - (sto/del-object! storage media-id))))) - - (execute [{:keys [::db/conn] :as cfg}] - (let [file-id (h/parse-uuid file-id) - id (h/parse-uuid id) - cfg (update cfg ::sto/storage media/configure-assets-storage conn)] - - (cond - (and (uuid? id) (uuid? file-id)) - (let [params {:id id :file-id file-id} - options {:columns [:id :data :revn]} - snapshot (db/get* conn :file-change params options)] - (restore-snapshot! cfg file-id snapshot)) - - (uuid? file-id) - (let [params (cond-> {:file-id file-id} - (string? label) - (assoc :label label)) - options {:columns [:id :data :revn]} - snapshot (db/get* conn :file-change params options)] - (restore-snapshot! cfg file-id snapshot)) - - :else - (println "=> invalid parameters"))))] - - (db/tx-run! system execute))) (defn list-file-snapshots! - [system & {:keys [file-id limit chunk-size start-at] - :or {chunk-size 10 limit Long/MAX_VALUE}}] - - (letfn [(get-chunk [ds cursor] - (let [query (str "select id, label, revn, created_at " - " from file_change " - " where file_id = ? " - " and created_at < ? " - " and label is not null " - " and data is not null " - " order by created_at desc " - " limit ?") - file-id (if (string? file-id) - (d/parse-uuid file-id) - file-id) - rows (db/exec! ds [query file-id cursor chunk-size])] - [(some->> rows peek :created-at) (seq rows)])) - - (get-candidates [ds] - (->> (d/iteration (partial get-chunk ds) - :vf second - :kf first - :initk (or start-at (dt/now))) - (take limit)))] - - (db/tx-run! system (fn [system] - (->> (fsnap/get-file-snapshots - (map (fn [row] - (update row :created-at dt/format-instant :rfc1123))) + [system & {:keys [file-id limit]}] + (db/tx-run! system (fn [system] + (let [params {:file-id (h/parse-uuid file-id) + :limit limit}] + (->> (fsnap/get-file-snapshots system (d/without-nils params)) (print-table [:id :revn :created-at :label])))))) +