♻️ Refactor file storage

Make it more scallable and make it easily extensible
This commit is contained in:
Andrey Antukh
2025-07-10 17:26:42 +02:00
parent 27bed84543
commit 5717708b56
56 changed files with 2291 additions and 2426 deletions

View File

@@ -30,8 +30,8 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.main :as main] [app.main :as main]
[app.srepl.helpers :as srepl.helpers] [app.srepl.helpers :as h]
[app.srepl.main :as srepl] [app.srepl.main :refer :all]
[app.util.blob :as blob] [app.util.blob :as blob]
[clj-async-profiler.core :as prof] [clj-async-profiler.core :as prof]
[clojure.contrib.humanize :as hum] [clojure.contrib.humanize :as hum]

80
backend/scripts/_env Normal file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bash
export PENPOT_MANAGEMENT_API_SHARED_KEY=super-secret-management-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-login-with-ldap \
enable-login-with-password
enable-login-with-oidc \
enable-login-with-google \
enable-login-with-github \
enable-login-with-gitlab \
enable-backend-worker \
enable-backend-asserts \
disable-feature-fdata-pointer-map \
enable-feature-fdata-objects-map \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
disable-secure-session-cookies \
enable-smtp \
enable-prepl-server \
enable-urepl-server \
enable-rpc-climit \
enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \
enable-auto-file-snapshot \
enable-webhooks \
enable-access-tokens \
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
# Setup default upload media file size to 100MiB
export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:+UseShenandoahGC \
-XX:+UseCompactObjectHeaders \
-XX:+UnlockExperimentalVMOptions \
-XX:ShenandoahGCMode=generational \
-XX:-OmitStackTraceInFastThrow \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
function setup_minio() {
# Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q
mc admin user add penpot-s3 penpot-devenv penpot-devenv -q
mc admin user info penpot-s3 penpot-devenv |grep -F -q "readwrite"
if [ "$?" = "1" ]; then
mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q
fi
mc mb penpot-s3/penpot -p -q
}

View File

@@ -1,115 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PENPOT_SECRET_KEY=super-secret-devenv-key SCRIPT_DIR=$(dirname $0);
export PENPOT_HOST=devenv source $SCRIPT_DIR/_env;
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-login-with-ldap \
enable-login-with-password
enable-login-with-oidc \
enable-login-with-google \
enable-login-with-github \
enable-login-with-gitlab \
enable-backend-worker \
enable-backend-asserts \
disable-feature-fdata-pointer-map \
enable-feature-fdata-objects-map \
enable-audit-log \
enable-transit-readable-response \
enable-demo-users \
disable-secure-session-cookies \
enable-smtp \
enable-prepl-server \
enable-urepl-server \
enable-rpc-climit \
enable-rpc-rlimit \
enable-quotes \
enable-soft-rpc-rlimit \
enable-auto-file-snapshot \
enable-webhooks \
enable-access-tokens \
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
# Setup default upload media file size to 100MiB
export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot"
# export PENPOT_DATABASE_USERNAME="penpot"
# export PENPOT_DATABASE_PASSWORD="penpot"
# export PENPOT_DATABASE_READONLY=true
# export PENPOT_DATABASE_URI="postgresql://172.17.0.1:5432/penpot_pre"
# export PENPOT_DATABASE_USERNAME="penpot_pre"
# export PENPOT_DATABASE_PASSWORD="penpot_pre"
# export PENPOT_LOGGERS_LOKI_URI="http://172.17.0.1:3100/loki/api/v1/push"
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit"
# Initialize MINIO config # Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q setup_minio;
mc admin user add penpot-s3 penpot-devenv penpot-devenv -q
mc admin user info penpot-s3 penpot-devenv |grep -F -q "readwrite"
if [ "$?" = "1" ]; then
mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q
fi
mc mb penpot-s3/penpot -p -q
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_OBJECTS_STORAGE_FS_DIRECTORY="assets"
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:+UseShenandoahGC \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockExperimentalVMOptions \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints \
-XX:ShenandoahGCMode=generational \
-XX:+UseCompactObjectHeaders \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
export JAVA_OPTS="$JAVA_OPTS -Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
export OPTIONS="-A:jmx-remote -A:dev" export OPTIONS="-A:jmx-remote -A:dev"
# Setup HEAP
# export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m"
# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch"
# Increase virtual thread pool size
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
# Disable C2 Compiler
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
# Disable all compilers
# export OPTIONS="$OPTIONS -J-Xint"
# Setup GC
# export OPTIONS="$OPTIONS -J-XX:+UseG1GC"
# Setup GC
# export OPTIONS="$OPTIONS -J-XX:+UseZGC"
export OPTIONS_EVAL="nil" export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)"

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
source /home/penpot/environ
export PENPOT_FLAGS="$PENPOT_FLAGS disable-backend-worker"
export OPTIONS="
-A:jmx-remote -A:dev \
-J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-J-Djdk.attach.allowAttachSelf \
-J-Dlog4j2.configurationFile=log4j2-experiments.xml \
-J-XX:-OmitStackTraceInFastThrow \
-J-XX:+UnlockDiagnosticVMOptions \
-J-XX:+DebugNonSafepoints \
-J-Djdk.tracePinnedThreads=full \
-J-XX:+UseTransparentHugePages \
-J-XX:ReservedCodeCacheSize=1g \
-J-Dpolyglot.engine.WarnInterpreterOnly=false \
-J--enable-preview";
# Setup HEAP
export OPTIONS="$OPTIONS -J-Xms320g -J-Xmx320g -J-XX:+AlwaysPreTouch"
export PENPOT_HTTP_SERVER_IO_THREADS=2
export PENPOT_HTTP_SERVER_WORKER_THREADS=2
# Increase virtual thread pool size
# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16"
# Disable C2 Compiler
# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1"
# Disable all compilers
# export OPTIONS="$OPTIONS -J-Xint"
# Setup GC
export OPTIONS="$OPTIONS -J-XX:+UseG1GC -J-Xlog:gc:logs/gc.log"
# Setup GC
#export OPTIONS="$OPTIONS -J-XX:+UseZGC -J-XX:+ZGenerational -J-Xlog:gc:logs/gc.log"
# Enable ImageMagick v7.x support
# export OPTIONS="-J-Dim4java.useV7=true $OPTIONS";
export OPTIONS_EVAL="nil"
# export OPTIONS_EVAL="(set! *warn-on-reflection* true)"
set -ex
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main

View File

@@ -1,44 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PENPOT_SECRET_KEY=super-secret-devenv-key SCRIPT_DIR=$(dirname $0);
export PENPOT_HOST=devenv
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-backend-asserts \
enable-feature-fdata-pointer-map \
enable-feature-fdata-objects-map \
enable-file-snapshot \
enable-tiered-file-data-storage";
export JAVA_OPTS=" source $SCRIPT_DIR/_env;
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ export OPTIONS="-A:dev"
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv.xml \
-XX:+EnableDynamicAgentLoading \
-XX:-OmitStackTraceInFastThrow \
-XX:+UnlockDiagnosticVMOptions \
-XX:+DebugNonSafepoints";
export CLOJURE_OPTIONS="-A:dev"
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
# Setup default upload media file size to 100MiB
export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
entrypoint=${1:-app.main}; entrypoint=${1:-app.main};
shift 1; shift 1;
set -ex set -ex
clojure $CLOJURE_OPTIONS -A:dev -M -m $entrypoint "$@"; exec clojure $OPTIONS -A:dev -M -m $entrypoint "$@";

View File

@@ -1,70 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PENPOT_SECRET_KEY=super-secret-devenv-key SCRIPT_DIR=$(dirname $0);
export PENPOT_MANAGEMENT_API_SHARED_KEY=super-secret-management-api-key source $SCRIPT_DIR/_env;
export PENPOT_HOST=devenv
export PENPOT_FLAGS="\
$PENPOT_FLAGS \
enable-prepl-server \
enable-urepl-server \
enable-nrepl-server \
enable-webhooks \
enable-backend-asserts \
enable-audit-log \
enable-login-with-ldap \
enable-transit-readable-response \
enable-demo-users \
disable-feature-fdata-pointer-map \
enable-feature-fdata-objects-map \
disable-secure-session-cookies \
enable-rpc-climit \
enable-smtp \
enable-quotes \
enable-file-snapshot \
enable-access-tokens \
disable-tiered-file-data-storage \
enable-file-validation \
enable-file-schema-validation \
enable-subscriptions";
# Default deletion delay for devenv
export PENPOT_DELETION_DELAY="24h"
# Setup default upload media file size to 100MiB
export PENPOT_MEDIA_MAX_FILE_SIZE=104857600
# Setup default multipart upload size to 300MiB
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
# Initialize MINIO config # Initialize MINIO config
mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q setup_minio;
mc admin user add penpot-s3 penpot-devenv penpot-devenv -q
mc admin user info penpot-s3 penpot-devenv |grep -F -q "readwrite"
if [ "$?" = "1" ]; then
mc admin policy attach penpot-s3 readwrite --user=penpot-devenv -q
fi
mc mb penpot-s3/penpot -p -q
export AWS_ACCESS_KEY_ID=penpot-devenv
export AWS_SECRET_ACCESS_KEY=penpot-devenv
export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
entrypoint=${1:-app.main};
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \
-Dlog4j2.configurationFile=log4j2-devenv.xml \
-Djdk.tracePinnedThreads=full \
-Dim4java.useV7=true \
-XX:-OmitStackTraceInFastThrow \
--sun-misc-unsafe-memory-access=allow \
--enable-preview \
--enable-native-access=ALL-UNNAMED";
export OPTIONS="-A:jmx-remote -A:dev"
shift 1;
set -ex set -ex
clojure $OPTIONS -M -m $entrypoint; exec clojure -A:jmx-remote -A:dev -M -m app.main "$@";

View File

@@ -141,13 +141,11 @@
([index coll attr] ([index coll attr]
(reduce #(index-object %1 %2 attr) index coll))) (reduce #(index-object %1 %2 attr) index coll)))
(defn decode-row (defn- decode-row-features
[{:keys [data changes features] :as row}] [{:keys [features] :as row}]
(when row (when row
(cond-> row (cond-> row
features (assoc :features (db/decode-pgarray features #{})) (db/pgarray? features) (assoc :features (db/decode-pgarray features #{})))))
changes (assoc :changes (blob/decode changes))
data (assoc :data (blob/decode data)))))
(def sql:get-minimal-file (def sql:get-minimal-file
"SELECT f.id, "SELECT f.id,
@@ -161,23 +159,124 @@
[cfg id & {:as opts}] [cfg id & {:as opts}]
(db/get-with-sql cfg [sql:get-minimal-file id] opts)) (db/get-with-sql cfg [sql:get-minimal-file id] opts))
(defn decode-file (def sql:get-file
"A general purpose file decoding function that resolves all external "SELECT f.id,
pointers, run migrations and return plain vanilla file map" f.project_id,
[cfg {:keys [id] :as file} & {:keys [migrate?] :or {migrate? true}}] f.created_at,
(binding [pmap/*load-fn* (partial fdata/load-pointer cfg id)] f.modified_at,
(let [file (->> file f.deleted_at,
(fmigr/resolve-applied-migrations cfg) f.name,
(fdata/resolve-file-data cfg)) f.is_shared,
libs (delay (get-resolved-file-libraries cfg file))] f.has_media_trimmed,
f.revn,
f.data AS legacy_data,
f.ignore_sync_until,
f.comment_thread_seqn,
f.features,
f.version,
f.vern,
p.team_id,
coalesce(fd.backend, 'legacy-db') AS backend,
fd.metadata AS metadata,
fd.data AS data
FROM file AS f
LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id)
INNER JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?")
(-> file (defn- migrate-file
(update :features db/decode-pgarray #{}) [{:keys [::db/conn] :as cfg} {:keys [read-only?]} {:keys [id] :as file}]
(update :data blob/decode) (binding [pmap/*load-fn* (partial fdata/load-pointer cfg id)
(update :data fdata/process-pointers deref) pmap/*tracked* (pmap/create-tracked)]
(update :data fdata/process-objects (partial into {})) (let [libs (delay (get-resolved-file-libraries cfg file))
(update :data assoc :id id) ;; For avoid unnecesary overhead of creating multiple
(cond-> migrate? (fmg/migrate-file libs)))))) ;; pointers and handly internally with objects map in their
;; worst case (when probably all shapes and all pointers
;; will be readed in any case), we just realize/resolve them
;; before applying the migration to the file.
file (-> (fdata/realize cfg file)
(fmg/migrate-file libs))]
(if (or read-only? (db/read-only? conn))
file
(do ;; When file is migrated, we break the rule of no
;; perform mutations on get operations and update the
;; file with all migrations applied
(update-file! cfg file)
(fmigr/resolve-applied-migrations cfg file))))))
(defn- get-file*
[{:keys [::db/conn] :as cfg} id
{:keys [migrate?
realize?
decode?
skip-locked?
include-deleted?
throw-if-not-exists?
lock-for-update?
lock-for-share?]
:or {lock-for-update? false
lock-for-share? false
migrate? true
decode? true
include-deleted? false
throw-if-not-exists? true
realize? false}
:as options}]
(assert (db/connection? conn) "expected cfg with valid connection")
(let [sql
(cond
lock-for-update?
(str sql:get-file " FOR UPDATE of f")
lock-for-share?
(str sql:get-file " FOR SHARE of f")
:else
sql:get-file)
sql
(if skip-locked?
(str sql " SKIP LOCKED")
sql)
file
(db/get-with-sql conn [sql id]
{::db/throw-if-not-exists false
::db/remove-deleted (not include-deleted?)})
file
(-> file
(d/update-when :features db/decode-pgarray #{})
(d/update-when :metadata fdata/decode-metadata))]
(if file
(let [file
(->> file
(fmigr/resolve-applied-migrations cfg)
(fdata/resolve-file-data cfg))
will-migrate?
(and migrate? (fmg/need-migration? file))]
(if decode?
(cond->> (fdata/decode-file-data cfg file)
(and realize? (not will-migrate?))
(fdata/realize cfg)
will-migrate?
(migrate-file cfg options))
file))
(when-not (or skip-locked? (not throw-if-not-exists?))
(ex/raise :type :not-found
:code :object-not-found
:hint "database object not found"
:table :file
:file-id id)))))
(defn get-file (defn get-file
"Get file, resolve all features and apply migrations. "Get file, resolve all features and apply migrations.
@@ -186,10 +285,7 @@
operations on file, because it removes the ovehead of lazy fetching operations on file, because it removes the ovehead of lazy fetching
and decoding." and decoding."
[cfg file-id & {:as opts}] [cfg file-id & {:as opts}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/run! cfg get-file* file-id opts))
(when-let [row (db/get* conn :file {:id file-id}
(assoc opts ::db/remove-deleted false))]
(decode-file cfg row opts)))))
(defn clean-file-features (defn clean-file-features
[file] [file]
@@ -213,12 +309,12 @@
(let [conn (db/get-connection cfg) (let [conn (db/get-connection cfg)
ids (db/create-array conn "uuid" ids)] ids (db/create-array conn "uuid" ids)]
(->> (db/exec! conn [sql:get-teams ids]) (->> (db/exec! conn [sql:get-teams ids])
(map decode-row)))) (map decode-row-features))))
(defn get-team (defn get-team
[cfg team-id] [cfg team-id]
(-> (db/get cfg :team {:id team-id}) (-> (db/get cfg :team {:id team-id})
(decode-row))) (decode-row-features)))
(defn get-fonts (defn get-fonts
[cfg team-id] [cfg team-id]
@@ -310,7 +406,6 @@
(do (do
(l/trc :hint "lookup index" (l/trc :hint "lookup index"
:file-id (str file-id) :file-id (str file-id)
:snap-id (str (:snapshot-id file))
:id (str id) :id (str id)
:result (str (get mobj :id))) :result (str (get mobj :id)))
(get mobj :id)) (get mobj :id))
@@ -327,7 +422,6 @@
(doseq [[old-id item] missing-index] (doseq [[old-id item] missing-index]
(l/dbg :hint "create missing references" (l/dbg :hint "create missing references"
:file-id (str file-id) :file-id (str file-id)
:snap-id (str (:snapshot-id file))
:old-id (str old-id) :old-id (str old-id)
:id (str (:id item))) :id (str (:id item)))
(db/insert! conn :file-media-object item (db/insert! conn :file-media-object item
@@ -338,12 +432,16 @@
(def sql:get-file-media (def sql:get-file-media
"SELECT * FROM file_media_object WHERE id = ANY(?)") "SELECT * FROM file_media_object WHERE id = ANY(?)")
(defn get-file-media*
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file}]
(let [used (cfh/collect-used-media data)
used (db/create-array conn "uuid" used)]
(->> (db/exec! conn [sql:get-file-media used])
(mapv (fn [row] (assoc row :file-id id))))))
(defn get-file-media (defn get-file-media
[cfg {:keys [data] :as file}] [cfg file]
(db/run! cfg (fn [{:keys [::db/conn]}] (db/run! cfg get-file-media* file))
(let [used (cfh/collect-used-media data)
used (db/create-array conn "uuid" used)]
(db/exec! conn [sql:get-file-media used])))))
(def ^:private sql:get-team-files-ids (def ^:private sql:get-team-files-ids
"SELECT f.id FROM file AS f "SELECT f.id FROM file AS f
@@ -474,7 +572,7 @@
;; all of them, not only the applied ;; all of them, not only the applied
(vary-meta dissoc ::fmg/migrated)))) (vary-meta dissoc ::fmg/migrated))))
(defn encode-file (defn- encode-file
[cfg {:keys [id features] :as file}] [cfg {:keys [id features] :as file}]
(let [file (if (and (contains? features "fdata/objects-map") (let [file (if (and (contains? features "fdata/objects-map")
(:data file)) (:data file))
@@ -497,13 +595,28 @@
(defn- file->params (defn- file->params
[file] [file]
(-> (select-keys file file-attrs) (-> (select-keys file file-attrs)
(assoc :data nil)
(dissoc :team-id) (dissoc :team-id)
(dissoc :migrations))) (dissoc :migrations)))
(defn file->file-data-params
[{:keys [id] :as file} & {:as opts}]
(let [created-at (or (:created-at file) (ct/now))
modified-at (or (:modified-at file) created-at)]
(d/without-nils
{:id id
:type "main"
:file-id id
:data (:data file)
:metadata (:metadata file)
:created-at created-at
:modified-at modified-at})))
(defn insert-file! (defn insert-file!
"Insert a new file into the database table. Expectes a not-encoded file. "Insert a new file into the database table. Expectes a not-encoded file.
Returns nil." Returns nil."
[{:keys [::db/conn] :as cfg} file & {:as opts}] [{:keys [::db/conn] :as cfg} file & {:as opts}]
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
(when (:migrations file) (when (:migrations file)
(fmigr/upsert-migrations! conn file)) (fmigr/upsert-migrations! conn file))
@@ -511,35 +624,43 @@
(let [file (encode-file cfg file)] (let [file (encode-file cfg file)]
(db/insert! conn :file (db/insert! conn :file
(file->params file) (file->params file)
{::db/return-keys false}) (assoc opts ::db/return-keys false))
(->> (file->file-data-params file)
(fdata/upsert! cfg))
nil)) nil))
(defn update-file! (defn update-file!
"Update an existing file on the database. Expects not encoded file." "Update an existing file on the database. Expects not encoded file."
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} & {:as opts}] [{:keys [::db/conn] :as cfg} {:keys [id] :as file} & {:as opts}]
(if (::reset-migrations opts false) (if (::reset-migrations? opts false)
(fmigr/reset-migrations! conn file) (fmigr/reset-migrations! conn file)
(fmigr/upsert-migrations! conn file)) (fmigr/upsert-migrations! conn file))
(let [file (let [file
(encode-file cfg file) (encode-file cfg file)
params file-params
(file->params (dissoc file :id))] (file->params (dissoc file :id))
(db/update! conn :file params file-data-params
(file->file-data-params file)]
(db/update! conn :file file-params
{:id id} {:id id}
{::db/return-keys false}) {::db/return-keys false})
(fdata/upsert! cfg file-data-params)
nil)) nil))
(defn save-file! (defn save-file!
"Applies all the final validations and perist the file, binfile "Applies all the final validations and perist the file, binfile
specific, should not be used outside of binfile domain. specific, should not be used outside of binfile domain.
Returns nil" Returns nil"
[{:keys [::timestamp] :as cfg} file & {:as opts}] [{:keys [::timestamp] :as cfg} file & {:as opts}]
(assert (ct/inst? timestamp) "expected valid timestamp") (assert (ct/inst? timestamp) "expected valid timestamp")
(let [file (-> file (let [file (-> file
@@ -564,7 +685,7 @@
(l/error :hint "file schema validation error" :cause result)))) (l/error :hint "file schema validation error" :cause result))))
(if (::overwrite cfg) (if (::overwrite cfg)
(update-file! cfg file (assoc opts ::reset-migrations true)) (update-file! cfg file (assoc opts ::reset-migrations? true))
(insert-file! cfg file opts)))) (insert-file! cfg file opts))))
(def ^:private sql:get-file-libraries (def ^:private sql:get-file-libraries
@@ -603,7 +724,7 @@
;; FIXME: :is-indirect set to false to all rows looks ;; FIXME: :is-indirect set to false to all rows looks
;; completly useless ;; completly useless
(map #(assoc % :is-indirect false)) (map #(assoc % :is-indirect false))
(map decode-row)) (map decode-row-features))
(db/exec! conn [sql:get-file-libraries file-id]))) (db/exec! conn [sql:get-file-libraries file-id])))
(defn get-resolved-file-libraries (defn get-resolved-file-libraries

View File

@@ -346,7 +346,7 @@
thumbnails (->> (bfc/get-file-object-thumbnails cfg file-id) thumbnails (->> (bfc/get-file-object-thumbnails cfg file-id)
(mapv #(dissoc % :file-id))) (mapv #(dissoc % :file-id)))
file (cond-> (bfc/get-file cfg file-id) file (cond-> (bfc/get-file cfg file-id :realize? true)
detach? detach?
(-> (ctf/detach-external-references file-id) (-> (ctf/detach-external-references file-id)
(dissoc :libraries)) (dissoc :libraries))

View File

@@ -153,7 +153,7 @@
(defn- write-file! (defn- write-file!
[cfg file-id] [cfg file-id]
(let [file (bfc/get-file cfg file-id) (let [file (bfc/get-file cfg file-id :realize? true)
thumbs (bfc/get-file-object-thumbnails cfg file-id) thumbs (bfc/get-file-object-thumbnails cfg file-id)
media (bfc/get-file-media cfg file) media (bfc/get-file-media cfg file)
rels (bfc/get-files-rels cfg #{file-id})] rels (bfc/get-files-rels cfg #{file-id})]

View File

@@ -224,9 +224,11 @@
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
"the `include-libraries` and `embed-assets` are mutally excluding options"))) "the `include-libraries` and `embed-assets` are mutally excluding options")))
(let [detach? (and (not embed-assets) (not include-libraries))] (let [detach? (and (not embed-assets) (not include-libraries))]
(db/tx-run! cfg (fn [cfg] (db/tx-run! cfg (fn [cfg]
(cond-> (bfc/get-file cfg file-id {::sql/for-update true}) (cond-> (bfc/get-file cfg file-id
{:realize? true
:lock-for-update? true})
detach? detach?
(-> (ctf/detach-external-references file-id) (-> (ctf/detach-external-references file-id)
(dissoc :libraries)) (dissoc :libraries))

View File

@@ -52,6 +52,8 @@
:redis-uri "redis://redis/0" :redis-uri "redis://redis/0"
:file-data-backend "legacy-db"
:objects-storage-backend "fs" :objects-storage-backend "fs"
:objects-storage-fs-directory "assets" :objects-storage-fs-directory "assets"
@@ -107,7 +109,8 @@
[:auto-file-snapshot-timeout {:optional true} ::ct/duration] [:auto-file-snapshot-timeout {:optional true} ::ct/duration]
[:media-max-file-size {:optional true} ::sm/int] [:media-max-file-size {:optional true} ::sm/int]
[:deletion-delay {:optional true} ::ct/duration] ;; REVIEW [:deletion-delay {:optional true} ::ct/duration]
[:file-clean-delay {:optional true} ::ct/duration]
[:telemetry-enabled {:optional true} ::sm/boolean] [:telemetry-enabled {:optional true} ::sm/boolean]
[:default-blob-version {:optional true} ::sm/int] [:default-blob-version {:optional true} ::sm/int]
[:allow-demo-users {:optional true} ::sm/boolean] [:allow-demo-users {:optional true} ::sm/boolean]
@@ -211,6 +214,8 @@
[:prepl-host {:optional true} :string] [:prepl-host {:optional true} :string]
[:prepl-port {:optional true} ::sm/int] [:prepl-port {:optional true} ::sm/int]
[:file-data-backend {:optional true} [:enum "db" "legacy-db" "storage"]]
[:media-directory {:optional true} :string] ;; REVIEW [:media-directory {:optional true} :string] ;; REVIEW
[:media-uri {:optional true} :string] [:media-uri {:optional true} :string]
[:assets-path {:optional true} :string] [:assets-path {:optional true} :string]
@@ -302,6 +307,11 @@
(or (c/get config :deletion-delay) (or (c/get config :deletion-delay)
(ct/duration {:days 7}))) (ct/duration {:days 7})))
(defn get-file-clean-delay
[]
(or (c/get config :file-clean-delay)
(ct/duration {:days 2})))
(defn get (defn get
"A configuration getter. Helps code be more testable." "A configuration getter. Helps code be more testable."
([key] ([key]

View File

@@ -298,7 +298,7 @@
(defn insert! (defn insert!
"A helper that builds an insert sql statement and executes it. By "A helper that builds an insert sql statement and executes it. By
default returns the inserted row with all the field; you can delimit default returns the inserted row with all the field; you can delimit
the returned columns with the `::columns` option." the returned columns with the `::sql/columns` option."
[ds table params & {:as opts}] [ds table params & {:as opts}]
(let [conn (get-connectable ds) (let [conn (get-connectable ds)
sql (sql/insert table params opts) sql (sql/insert table params opts)
@@ -406,15 +406,15 @@
:hint "database object not found")) :hint "database object not found"))
row)) row))
(defn get-with-sql (defn get-with-sql
[ds sql & {:as opts}] [ds sql & {:as opts}]
(let [rows (cond->> (exec! ds sql opts) (let [rows
(::remove-deleted opts true) (cond->> (exec! ds sql opts)
(remove is-row-deleted?) (::remove-deleted opts true)
(remove is-row-deleted?)
:always :always
(not-empty))] (not-empty))]
(when (and (not rows) (::throw-if-not-exists opts true)) (when (and (not rows) (::throw-if-not-exists opts true))
(ex/raise :type :not-found (ex/raise :type :not-found
@@ -423,7 +423,6 @@
(first rows))) (first rows)))
(def ^:private default-plan-opts (def ^:private default-plan-opts
(-> default-opts (-> default-opts
(assoc :fetch-size 1000) (assoc :fetch-size 1000)
@@ -578,10 +577,10 @@
[system f & params] [system f & params]
(cond (cond
(connection? system) (connection? system)
(run! {::conn system} f) (apply run! {::conn system} f params)
(pool? system) (pool? system)
(run! {::pool system} f) (apply run! {::pool system} f params)
(::conn system) (::conn system)
(apply f system params) (apply f system params)

View File

@@ -9,11 +9,11 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.files.helpers :as cfh]
[app.common.files.migrations :as fmg]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.objects-map :as omap] [app.common.types.objects-map :as omap]
[app.common.types.path :as path] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.storage :as sto] [app.storage :as sto]
@@ -21,18 +21,34 @@
[app.util.objects-map :as omap.legacy] [app.util.objects-map :as omap.legacy]
[app.util.pointer-map :as pmap])) [app.util.pointer-map :as pmap]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OFFLOAD
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn offloaded?
[file]
(= "objects-storage" (:data-backend file)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OBJECTS-MAP ;; OBJECTS-MAP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn process-objects
"Apply a function to all objects-map on the file. Usualy used for convert
the objects-map instances to plain maps"
[fdata update-fn]
(if (contains? fdata :pages-index)
(update fdata :pages-index d/update-vals
(fn [page]
(update page :objects
(fn [objects]
(if (or (omap/objects-map? objects)
(omap.legacy/objects-map? objects))
(update-fn objects)
objects)))))
fdata))
(defn realize-objects
"Process a file and remove all instances of objects map realizing them
to a plain data. Used in operation where is more efficient have the
whole file loaded in memory or we going to persist it in an
alterantive storage."
[_cfg file]
(update file :data process-objects (partial into {})))
(defn enable-objects-map (defn enable-objects-map
[file & _opts] [file & _opts]
(let [update-page (let [update-page
@@ -61,51 +77,186 @@
(update :data update-data) (update :data update-data)
(update :features disj "fdata/objects-map")))) (update :features disj "fdata/objects-map"))))
(defn process-objects ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
"Apply a function to all objects-map on the file. Usualy used for convert ;; STORAGE
the objects-map instances to plain maps" ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[fdata update-fn]
(if (contains? fdata :pages-index) (defmulti resolve-file-data
(update fdata :pages-index d/update-vals (fn [_cfg file] (get file :backend "legacy-db")))
(fn [page]
(update page :objects (defmethod resolve-file-data "legacy-db"
(fn [objects] [_cfg {:keys [legacy-data] :as file}]
(if (or (omap/objects-map? objects) (-> file
(omap.legacy/objects-map? objects)) (assoc :data legacy-data)
(update-fn objects) (dissoc :legacy-data)))
objects)))))
fdata)) (defmethod resolve-file-data "db"
[_cfg file]
(dissoc file :legacy-data))
(defmethod resolve-file-data "storage"
[cfg {:keys [metadata] :as file}]
(let [storage (sto/resolve cfg ::db/reuse-conn true)
ref-id (:storage-ref-id metadata)
data (->> (sto/get-object storage ref-id)
(sto/get-object-bytes storage))]
(-> file
(assoc :data data)
(dissoc :legacy-data))))
(defn decode-file-data
[_cfg {:keys [data] :as file}]
(cond-> file
(bytes? data)
(assoc :data (blob/decode data))))
(def ^:private sql:insert-file-data
"INSERT INTO file_data (file_id, id, created_at, modified_at, deleted_at,
type, backend, metadata, data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
(def ^:private sql:upsert-file-data
(str sql:insert-file-data
" ON CONFLICT (file_id, id)
DO UPDATE SET modified_at=?,
deleted_at=?,
backend=?,
metadata=?,
data=?"))
(defn- upsert-in-database
[cfg {:keys [id file-id created-at modified-at deleted-at type backend data metadata]}]
(let [created-at (or created-at (ct/now))
metadata (some-> metadata db/json)
modified-at (or modified-at created-at)]
(db/exec-one! cfg [sql:upsert-file-data
file-id id
created-at
modified-at
deleted-at
type
backend
metadata
data
modified-at
deleted-at
backend
metadata
data])))
(defn- handle-persistence
[cfg {:keys [type backend id file-id data] :as params}]
(cond
(= backend "storage")
(let [storage (sto/resolve cfg)
content (sto/content data)
sobject (sto/put-object! storage
{::sto/content content
::sto/touch true
:bucket "file-data"
:content-type "application/octet-stream"
:file-id file-id
:id id})
metadata {:storage-ref-id (:id sobject)}
params (-> params
(assoc :metadata metadata)
(assoc :data nil))]
(upsert-in-database cfg params))
(= backend "db")
(->> (dissoc params :metadata)
(upsert-in-database cfg))
(= backend "legacy-db")
(cond
(= type "main")
(db/update! cfg :file
{:data data}
{:id file-id}
{::db/return-keys false})
(= type "snapshot")
(db/update! cfg :file-change
{:data data}
{:file-id file-id :id id}
{::db/return-keys false})
(= type "fragment")
(upsert-in-database cfg
(-> (dissoc params :metadata)
(assoc :backend "db")))
:else
(throw (RuntimeException. "not implemented")))
:else
(throw (IllegalArgumentException.
(str "backend '" backend "' not supported")))))
(defn process-metadata
[cfg metadata]
(when-let [storage-id (:storage-ref-id metadata)]
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
(sto/touch-object! storage storage-id))))
(defn- default-backend
[backend]
(or backend (cf/get :file-data-backend)))
(def ^:private schema:metadata
[:map {:title "Metadata"}
[:storage-ref-id {:optional true} ::sm/uuid]])
(def decode-metadata-with-schema
(sm/decoder schema:metadata sm/json-transformer))
(defn decode-metadata
[metadata]
(some-> metadata
(db/decode-json-pgobject)
(decode-metadata-with-schema)))
(def ^:private schema:update-params
[:map {:closed true}
[:id ::sm/uuid]
[:type [:enum "main" "snapshot" "fragment"]]
[:file-id ::sm/uuid]
[:backend {:optional true} [:enum "db" "legacy-db" "storage"]]
[:metadata {:optional true} [:maybe schema:metadata]]
[:data {:optional true} bytes?]
[:created-at {:optional true} ::ct/inst]
[:modified-at {:optional true} [:maybe ::ct/inst]]
[:deleted-at {:optional true} [:maybe ::ct/inst]]])
(def ^:private check-update-params
(sm/check-fn schema:update-params :hint "invalid params received for update"))
(defn upsert!
"Create or update file data"
[cfg params & {:as opts}]
(let [params (-> (check-update-params params)
(update :backend default-backend))]
(some->> (:metadata params)
(process-metadata cfg))
(-> (handle-persistence cfg params)
(db/get-update-count)
(pos?))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; POINTER-MAP ;; POINTER-MAP
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-file-data
"Get file data given a file instance."
[system file]
(if (offloaded? file)
(let [storage (sto/resolve system ::db/reuse-conn true)]
(->> (sto/get-object storage (:data-ref-id file))
(sto/get-object-bytes storage)))
(:data file)))
(defn resolve-file-data
[system file]
(let [data (get-file-data system file)]
(assoc file :data data)))
(defn decode-file-data
[_system {:keys [data] :as file}]
(cond-> file
(bytes? data)
(assoc :data (blob/decode data))))
(defn load-pointer (defn load-pointer
"A database loader pointer helper" "A database loader pointer helper"
[system file-id id] [cfg file-id id]
(let [fragment (db/get* system :file-data-fragment (let [fragment (some-> (db/get* cfg :file-data
{:id id :file-id file-id} {:id id :file-id file-id :type "fragment"}
{::sql/columns [:data :data-backend :data-ref-id :id]})] {::sql/columns [:data :backend :id :metadata]})
(update :metadata decode-metadata))]
(l/trc :hint "load pointer" (l/trc :hint "load pointer"
:file-id (str file-id) :file-id (str file-id)
@@ -119,22 +270,21 @@
:file-id file-id :file-id file-id
:fragment-id id)) :fragment-id id))
(let [data (get-file-data system fragment)] (-> (resolve-file-data cfg fragment)
;; FIXME: conditional thread scheduling for decoding big objects (get :data)
(blob/decode data)))) (blob/decode))))
(defn persist-pointers! (defn persist-pointers!
"Persist all currently tracked pointer objects" "Persist all currently tracked pointer objects"
[system file-id] [cfg file-id]
(let [conn (db/get-connection system)] (doseq [[id item] @pmap/*tracked*]
(doseq [[id item] @pmap/*tracked*] (when (pmap/modified? item)
(when (pmap/modified? item) (l/trc :hint "persist pointer" :file-id (str file-id) :id (str id))
(l/trc :hint "persist pointer" :file-id (str file-id) :id (str id)) (let [content (-> item deref blob/encode)]
(let [content (-> item deref blob/encode)] (upsert! cfg {:id id
(db/insert! conn :file-data-fragment :file-id file-id
{:id id :type "fragment"
:file-id file-id :data content})))))
:data content}))))))
(defn process-pointers (defn process-pointers
"Apply a function to all pointers on the file. Usuly used for "Apply a function to all pointers on the file. Usuly used for
@@ -148,6 +298,14 @@
(d/update-vals update-fn') (d/update-vals update-fn')
(update :pages-index d/update-vals update-fn')))) (update :pages-index d/update-vals update-fn'))))
(defn realize-pointers
"Process a file and remove all instances of pointers realizing them to
a plain data. Used in operation where is more efficient have the
whole file loaded in memory."
[cfg {:keys [id] :as file}]
(binding [pmap/*load-fn* (partial load-pointer cfg id)]
(update file :data process-pointers deref)))
(defn get-used-pointer-ids (defn get-used-pointer-ids
"Given a file, return all pointer ids used in the data." "Given a file, return all pointer ids used in the data."
[fdata] [fdata]
@@ -167,47 +325,12 @@
(update :features conj "fdata/pointer-map"))) (update :features conj "fdata/pointer-map")))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PATH-DATA ;; GENERAL PURPOSE HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn enable-path-data (defn realize
"Enable the fdata/path-data feature on the file." "A helper that combines realize-pointers and realize-objects"
[file & _opts] [cfg file]
(letfn [(update-object [object] (->> file
(if (or (cfh/path-shape? object) (realize-pointers cfg)
(cfh/bool-shape? object)) (realize-objects cfg)))
(update object :content path/content)
object))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
(-> file
(update :data (fn [data]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(update :features conj "fdata/path-data"))))
(defn disable-path-data
[file & _opts]
(letfn [(update-object [object]
(if (or (cfh/path-shape? object)
(cfh/bool-shape? object))
(update object :content vec)
object))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
(when-let [conn db/*conn*]
(db/delete! conn :file-migration {:file-id (:id file)
:name "0003-convert-path-content"}))
(-> file
(update :data (fn [data]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(update :features disj "fdata/path-data")
(update :migrations disj "0003-convert-path-content")
(vary-meta update ::fmg/migrated disj "0003-convert-path-content"))))

View File

@@ -0,0 +1,444 @@
;; 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.features.file-snapshots
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.features :as-alias cfeat]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.fdata :as fdata]
[app.storage :as sto]
[app.util.blob :as blob]
[app.worker :as wrk]
[cuerdas.core :as str]))
(def sql:snapshots
"SELECT c.id,
c.label,
c.created_at,
c.updated_at AS modified_at,
c.deleted_at,
c.profile_id,
c.created_by,
c.locked_by,
c.revn,
c.features,
c.migrations,
c.version,
c.file_id,
c.data AS legacy_data,
fd.data AS data,
coalesce(fd.backend, 'legacy-db') AS backend,
fd.metadata AS metadata
FROM file_change AS c
LEFT JOIN file_data AS fd ON (fd.file_id = c.file_id
AND fd.id = c.id
AND fd.type = 'snapshot')
WHERE c.label IS NOT NULL")
(defn- decode-snapshot
[snapshot]
(some-> snapshot
(-> (d/update-when :metadata fdata/decode-metadata)
(d/update-when :migrations db/decode-pgarray [])
(d/update-when :features db/decode-pgarray #{}))))
(def ^:private sql:get-minimal-file
"SELECT f.id,
f.revn,
f.modified_at,
f.deleted_at,
fd.backend AS backend,
fd.metadata AS metadata
FROM file AS f
LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id)
WHERE f.id = ?")
(defn- get-minimal-file
[cfg id & {:as opts}]
(-> (db/get-with-sql cfg [sql:get-minimal-file id] opts)
(d/update-when :metadata fdata/decode-metadata)))
(def ^:private sql:get-snapshot-without-data
(str "WITH snapshots AS (" sql:snapshots ")"
"SELECT c.id,
c.label,
c.revn,
c.created_at,
c.modified_at,
c.deleted_at,
c.profile_id,
c.created_by,
c.locked_by,
c.features,
c.metadata,
c.migrations,
c.version,
c.file_id
FROM snapshots AS c
WHERE c.id = ?
AND CASE WHEN c.created_by = 'user'
THEN (c.deleted_at IS NULL)
WHEN c.created_by = 'system'
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
END"))
(defn get-minimal-snapshot
[cfg snapshot-id]
(let [now (ct/now)]
(-> (db/get-with-sql cfg [sql:get-snapshot-without-data snapshot-id now])
(decode-snapshot))))
(def ^:private sql:get-snapshot
(str sql:snapshots
" AND c.file_id = ?
AND c.id = ?
AND CASE WHEN c.created_by = 'user'
THEN (c.deleted_at IS NULL)
WHEN c.created_by = 'system'
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
END"))
(defn- get-snapshot
"Get snapshot with decoded data"
[cfg file-id snapshot-id]
(let [now (ct/now)]
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now])
(decode-snapshot)
(fdata/resolve-file-data cfg)
(fdata/decode-file-data cfg))))
(def ^:private sql:get-visible-snapshots
(str "WITH "
"snapshots1 AS ( " sql:snapshots "),"
"snapshots2 AS (
SELECT c.id,
c.label,
c.revn,
c.version,
c.created_at,
c.modified_at,
c.created_by,
c.locked_by,
c.profile_id,
c.deleted_at
FROM snapshots1 AS c
WHERE c.file_id = ?
), snapshots3 AS (
(SELECT * FROM snapshots2
WHERE created_by = 'system'
AND (deleted_at IS NULL OR
deleted_at >= ?::timestamptz)
LIMIT 500)
UNION ALL
(SELECT * FROM snapshots2
WHERE created_by = 'user'
AND deleted_at IS NULL
LIMIT 500)
)
SELECT * FROM snapshots3
ORDER BY created_at DESC"))
(defn get-visible-snapshots
"Return a list of snapshots fecheable from the API, it has a limited
set of fields and applies big but safe limits over all available
snapshots. It return a ordered vector by the snapshot date of
creation."
[cfg file-id]
(let [now (ct/now)]
(->> (db/exec! cfg [sql:get-visible-snapshots file-id now])
(mapv decode-snapshot))))
(def ^:private schema:decoded-file
[:map {:title "DecodedFile"}
[:id ::sm/uuid]
[:revn :int]
[:vern :int]
[:data :map]
[:version :int]
[:features ::cfeat/features]
[:migrations [::sm/set :string]]])
(def ^:private schema:snapshot
[:map {:title "Snapshot"}
[:id ::sm/uuid]
[:revn [::sm/int {:min 0}]]
[:version [::sm/int {:min 0}]]
[:features ::cfeat/features]
[:migrations [::sm/set ::sm/text]]
[:profile-id {:optional true} ::sm/uuid]
[:label ::sm/text]
[:file-id ::sm/uuid]
[:created-by [:enum "system" "user" "admin"]]
[:deleted-at {:optional true} ::ct/inst]
[:modified-at ::ct/inst]
[:created-at ::ct/inst]])
(def ^:private check-snapshot
(sm/check-fn schema:snapshot))
(def ^:private check-decoded-file
(sm/check-fn schema:decoded-file))
(defn- generate-snapshot-label
[]
(let [ts (-> (ct/now)
(ct/format-inst)
(str/replace #"[T:\.]" "-")
(str/rtrim "Z"))]
(str "snapshot-" ts)))
(def ^:private schema:create-params
[:map {:title "SnapshotCreateParams"}
[:profile-id ::sm/uuid]
[:created-by {:optional true} [:enum "user" "system"]]
[:label {:optional true} ::sm/text]
[:session-id {:optional true} ::sm/uuid]
[:modified-at {:optional true} ::ct/inst]
[:deleted-at {:optional true} ::ct/inst]])
(def ^:private check-create-params
(sm/check-fn schema:create-params))
(defn create!
"Create a file snapshot; expects a non-encoded file"
[cfg file & {:as params}]
(let [{:keys [label created-by deleted-at profile-id session-id]}
(check-create-params params)
file
(check-decoded-file file)
created-by
(or created-by "system")
snapshot-id
(uuid/next)
created-at
(ct/now)
deleted-at
(or deleted-at
(if (= created-by "system")
(ct/in-future (cf/get-deletion-delay))
nil))
label
(or label (generate-snapshot-label))
snapshot
(cond-> {:id snapshot-id
:revn (:revn file)
:version (:version file)
:file-id (:id file)
:features (:features file)
:migrations (:migrations file)
:label label
:created-at created-at
:modified-at created-at
:created-by created-by}
deleted-at
(assoc :deleted-at deleted-at)
:always
(check-snapshot))]
(db/insert! cfg :file-change
(-> snapshot
(update :features into-array)
(update :migrations into-array)
(assoc :updated-at created-at)
(assoc :profile-id profile-id)
(assoc :session-id session-id)
(dissoc :modified-at))
{::db/return-keys false})
(fdata/upsert! cfg
{:id snapshot-id
:file-id (:id file)
:type "snapshot"
:data (blob/encode (:data file))
:created-at created-at
:deleted-at deleted-at})
snapshot))
(def ^:private schema:update-params
[:map {:title "SnapshotUpdateParams"}
[:id ::sm/uuid]
[:file-id ::sm/uuid]
[:label ::sm/text]
[:modified-at {:optional true} ::ct/inst]])
(def ^:private check-update-params
(sm/check-fn schema:update-params))
(defn update!
[cfg params]
(let [{:keys [id file-id label modified-at]}
(check-update-params params)
modified-at
(or modified-at (ct/now))]
(db/update! cfg :file-data
{:deleted-at nil
:modified-at modified-at}
{:file-id file-id
:id id
:type "snapshot"}
{::db/return-keys false})
(-> (db/update! cfg :file-change
{:label label
:created-by "user"
:updated-at modified-at
:deleted-at nil}
{:file-id file-id
:id id}
{::db/return-keys false})
(db/get-update-count)
(pos?))))
(defn restore!
[{:keys [::db/conn] :as cfg} file-id snapshot-id]
(let [file (get-minimal-file conn file-id {::db/for-update true})
vern (rand-int Integer/MAX_VALUE)
storage
(sto/resolve cfg {::db/reuse-conn true})
snapshot
(get-snapshot cfg file-id snapshot-id)]
(when-not snapshot
(ex/raise :type :not-found
:code :snapshot-not-found
:hint "unable to find snapshot with the provided label"
:snapshot-id snapshot-id
:file-id file-id))
(when-not (:data snapshot)
(ex/raise :type :internal
:code :snapshot-without-data
:hint "snapshot has no data"
:label (:label snapshot)
:file-id file-id))
(let [;; If the snapshot has applied migrations stored, we reuse
;; them, if not, we take a safest set of migrations as
;; starting point. This is because, at the time of
;; implementing snapshots, migrations were not taken into
;; account so we need to make this backward compatible in
;; some way.
migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67))
file
(-> file
(update :revn inc)
(assoc :migrations migrations)
(assoc :data (:data snapshot))
(assoc :vern vern)
(assoc :version (:version snapshot))
(assoc :has-media-trimmed false)
(assoc :modified-at (:modified-at snapshot))
(assoc :features (:features snapshot)))]
(l/dbg :hint "restoring snapshot"
:file-id (str file-id)
:label (:label snapshot)
:snapshot-id (str (:id snapshot)))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(bfc/update-file! cfg file ::bfc/reset-migrations? true)
;; FIXME: this should be separated functions, we should not have
;; inline sql here.
;; 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)))
vern)))
(defn delete!
[cfg & {:keys [id file-id deleted-at]}]
(assert (uuid? id) "missing id")
(assert (uuid? file-id) "missing file-id")
(assert (ct/inst? deleted-at) "missing deleted-at")
(wrk/submit! {::db/conn (db/get-connection cfg)
::wrk/task :delete-object
::wrk/params {:object :snapshot
:deleted-at deleted-at
:file-id file-id
:id id}})
(db/update! cfg :file-change
{:deleted-at deleted-at}
{:id id :file-id file-id}
{::db/return-keys false})
true)
(def ^:private sql:get-snapshots
(str sql:snapshots " AND c.file_id = ?"))
(defn lock-by!
[conn id profile-id]
(-> (db/update! conn :file-change
{:locked-by profile-id}
{:id id}
{::db/return-keys false})
(db/get-update-count)
(pos?)))
(defn unlock!
[conn id]
(-> (db/update! conn :file-change
{:locked-by nil}
{:id id}
{::db/return-keys false})
(db/get-update-count)
(pos?)))
(defn reduce-snapshots
"Process the file snapshots using efficient reduction; the file
reduction comes with all snapshots, including maked as deleted"
[cfg file-id xform f init]
(let [conn (db/get-connection cfg)
xform (comp
(map (partial fdata/resolve-file-data cfg))
(map (partial fdata/decode-file-data cfg))
xform)]
(->> (db/plan conn [sql:get-snapshots file-id] {:fetch-size 1})
(transduce xform f init))))

View File

@@ -397,33 +397,6 @@
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))})))))) ::yres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))}))))))
(defn- reset-file-version
[cfg {:keys [params] :as request}]
(let [file-id (some-> params :file-id d/parse-uuid)
version (some-> params :version d/parse-integer)]
(when-not (contains? params :force)
(ex/raise :type :validation
:code :missing-force
:hint "missing force checkbox"))
(when (nil? file-id)
(ex/raise :type :validation
:code :invalid-file-id
:hint "provided invalid file id"))
(when (nil? version)
(ex/raise :type :validation
:code :invalid-version
:hint "provided invalid version"))
(db/tx-run! cfg srepl/process-file! file-id #(assoc % :version version))
{::yres/status 200
::yres/headers {"content-type" "text/plain"}
::yres/body "OK"}))
(defn- handle-team-features (defn- handle-team-features
[cfg {:keys [params] :as request}] [cfg {:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid) (let [team-id (some-> params :team-id d/parse-uuid)
@@ -576,8 +549,6 @@
{:handler (partial set-virtual-clock cfg)}] {:handler (partial set-virtual-clock cfg)}]
["/resend-email-verification" ["/resend-email-verification"
{:handler (partial resend-email-notification cfg)}] {:handler (partial resend-email-notification cfg)}]
["/reset-file-version"
{:handler (partial reset-file-version cfg)}]
["/handle-team-features" ["/handle-team-features"
{:handler (partial handle-team-features cfg)}] {:handler (partial handle-team-features cfg)}]
["/file-export" {:handler (partial export-handler cfg)}] ["/file-export" {:handler (partial export-handler cfg)}]

View File

@@ -447,7 +447,10 @@
:fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")} :fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")}
{:name "0141-add-idx-to-file-library-rel" {:name "0141-add-idx-to-file-library-rel"
:fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}]) :fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}
{:name "0141-add-file-data-table.sql"
:fn (mg/resource "app/migrations/sql/0141-add-file-data-table.sql")}])
(defn apply-migrations! (defn apply-migrations!
[pool name migrations] [pool name migrations]

View File

@@ -10,8 +10,8 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint] [app.common.pprint]
[app.srepl.fixes.media-refs :refer [process-file]]
[app.srepl.main :as srepl] [app.srepl.main :as srepl]
[app.srepl.procs.media-refs]
[clojure.edn :as edn])) [clojure.edn :as edn]))
(def ^:private required-services (def ^:private required-services
@@ -20,7 +20,10 @@
:app.storage/storage :app.storage/storage
:app.metrics/metrics :app.metrics/metrics
:app.db/pool :app.db/pool
:app.worker/executor]) :app.worker/netty-io-executor])
(def default-options
{:rollback? false})
(defn -main (defn -main
[& [options]] [& [options]]
@@ -28,22 +31,20 @@
(let [config-var (requiring-resolve 'app.main/system-config) (let [config-var (requiring-resolve 'app.main/system-config)
start-var (requiring-resolve 'app.main/start-custom) start-var (requiring-resolve 'app.main/start-custom)
stop-var (requiring-resolve 'app.main/stop) stop-var (requiring-resolve 'app.main/stop)
config (select-keys @config-var required-services)]
config (select-keys @config-var required-services)
options (if (string? options)
(ex/ignoring (edn/read-string options))
{})
options (-> (merge default-options options)
(assoc :proc-fn #'app.srepl.procs.media-refs/fix-media-refs))]
(start-var config) (start-var config)
(l/inf :hint "executing media-refs migration" :options options)
(let [options (if (string? options) (srepl/process! options)
(ex/ignoring (edn/read-string options))
{})]
(l/inf :hint "executing media-refs migration" :options options)
(srepl/process-files! process-file options))
(stop-var) (stop-var)
(System/exit 0)) (System/exit 0))
(catch Throwable cause (catch Throwable cause
(ex/print-throwable cause) (ex/print-throwable cause)
(flush) (flush)
(System/exit -1)))) (System/exit -1))))

View File

@@ -0,0 +1,38 @@
CREATE TABLE file_data (
file_id uuid NOT NULL REFERENCES file(id) DEFERRABLE,
id uuid NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
modified_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL,
type text NOT NULL,
backend text NULL,
metadata jsonb NULL,
data bytea NULL,
PRIMARY KEY (file_id, id)
) PARTITION BY HASH (file_id);
CREATE TABLE file_data_00 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE file_data_01 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE file_data_02 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE file_data_03 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE file_data_04 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE file_data_05 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE file_data_06 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE file_data_07 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE file_data_08 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE file_data_09 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE file_data_10 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE file_data_11 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE file_data_12 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE file_data_13 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE file_data_14 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE file_data_15 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 15);
CREATE INDEX file_data__deleted_at__idx
ON file_data (deleted_at, file_id, id)
WHERE deleted_at IS NOT NULL;

View File

@@ -239,7 +239,6 @@
'app.rpc.commands.files 'app.rpc.commands.files
'app.rpc.commands.files-create 'app.rpc.commands.files-create
'app.rpc.commands.files-share 'app.rpc.commands.files-share
'app.rpc.commands.files-temp
'app.rpc.commands.files-update 'app.rpc.commands.files-update
'app.rpc.commands.files-snapshot 'app.rpc.commands.files-snapshot
'app.rpc.commands.files-thumbnails 'app.rpc.commands.files-thumbnails

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.comments (ns app.rpc.commands.comments
(:require (:require
[app.binfile.common :as bfc]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@@ -163,34 +164,16 @@
(def xf-decode-row (def xf-decode-row
(map decode-row)) (map decode-row))
(def ^:private
sql:get-file
"SELECT f.id, f.modified_at, f.revn, f.features, f.name,
f.project_id, p.team_id, f.data,
f.data_ref_id, f.data_backend
FROM file as f
INNER JOIN project as p on (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR f.deleted_at > now())")
(defn- get-file (defn- get-file
"A specialized version of get-file for comments module." "A specialized version of get-file for comments module."
[cfg file-id page-id] [cfg file-id page-id]
(let [file (db/exec-one! cfg [sql:get-file file-id])] (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
(when-not file (let [file (bfc/get-file cfg file-id)
(ex/raise :type :not-found data (get file :data)]
:code :object-not-found (-> file
:hint "file not found")) (assoc :page-name (dm/get-in data [:pages-index page-id :name]))
(assoc :page-id page-id)
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] (dissoc :data)))))
(let [file (->> file
(files/decode-row)
(feat.fdata/resolve-file-data cfg))
data (get file :data)]
(-> file
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
(assoc :page-id page-id)
(dissoc :data))))))
;; FIXME: rename ;; FIXME: rename
(defn- get-comment-thread (defn- get-comment-thread

View File

@@ -24,7 +24,6 @@
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata] [app.features.fdata :as feat.fdata]
[app.features.file-migrations :as feat.fmigr]
[app.features.logical-deletion :as ldel] [app.features.logical-deletion :as ldel]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
@@ -54,12 +53,10 @@
(ct/duration {:days 7})) (ct/duration {:days 7}))
(defn decode-row (defn decode-row
[{:keys [data changes features] :as row}] [{:keys [features] :as row}]
(when row (when row
(cond-> row (cond-> row
features (assoc :features (db/decode-pgarray features #{})) (db/pgarray? features) (assoc :features (db/decode-pgarray features #{})))))
changes (assoc :changes (blob/decode changes))
data (assoc :data (blob/decode data)))))
(defn check-version! (defn check-version!
[file] [file]
@@ -209,85 +206,9 @@
[:id ::sm/uuid] [:id ::sm/uuid]
[:project-id {:optional true} ::sm/uuid]]) [:project-id {:optional true} ::sm/uuid]])
(defn- migrate-file
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} {:keys [read-only?]}]
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
pmap/*tracked* (pmap/create-tracked)]
(let [libs (delay (bfc/get-resolved-file-libraries cfg file))
;; For avoid unnecesary overhead of creating multiple pointers and
;; handly internally with objects map in their worst case (when
;; probably all shapes and all pointers will be readed in any
;; case), we just realize/resolve them before applying the
;; migration to the file
file (-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file libs))]
(if (or read-only? (db/read-only? conn))
file
(let [;; When file is migrated, we break the rule of no perform
;; mutations on get operations and update the file with all
;; migrations applied
file (if (contains? (:features file) "fdata/objects-map")
(feat.fdata/enable-objects-map file)
file)
file (if (contains? (:features file) "fdata/pointer-map")
(feat.fdata/enable-pointer-map file)
file)]
(db/update! conn :file
{:data (blob/encode (:data file))
:version (:version file)
:features (db/create-array conn "text" (:features file))}
{:id id}
{::db/return-keys false})
(when (contains? (:features file) "fdata/pointer-map")
(feat.fdata/persist-pointers! cfg id))
(feat.fmigr/upsert-migrations! conn file)
(feat.fmigr/resolve-applied-migrations cfg file))))))
(defn get-file
[{:keys [::db/conn] :as cfg} id
& {:keys [project-id
migrate?
include-deleted?
lock-for-update?
preload-pointers?]
:or {include-deleted? false
lock-for-update? false
migrate? true
preload-pointers? false}
:as options}]
(assert (db/connection? conn) "expected cfg with valid connection")
(let [params (merge {:id id}
(when (some? project-id)
{:project-id project-id}))
file (->> (db/get conn :file params
{::db/check-deleted (not include-deleted?)
::db/remove-deleted (not include-deleted?)
::sql/for-update lock-for-update?})
(feat.fmigr/resolve-applied-migrations cfg)
(feat.fdata/resolve-file-data cfg)
(decode-row))
file (if (and migrate? (fmg/need-migration? file))
(migrate-file cfg file options)
file)]
(if preload-pointers?
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
(update file :data feat.fdata/process-pointers deref))
file)))
(defn get-minimal-file (defn get-minimal-file
[cfg id & {:as opts}] [cfg id & {:as opts}]
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :vern :data-ref-id :data-backend])] (let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :vern])]
(db/get cfg :file {:id id} opts))) (db/get cfg :file {:id id} opts)))
(defn- get-minimal-file-with-perms (defn- get-minimal-file-with-perms
@@ -327,9 +248,9 @@
:project-id project-id :project-id project-id
:file-id id) :file-id id)
file (-> (get-file cfg id :project-id project-id) file (-> (bfc/get-file cfg id
:project-id project-id)
(assoc :permissions perms) (assoc :permissions perms)
(assoc :team-id (:id team))
(check-version!))] (check-version!))]
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
@@ -343,8 +264,7 @@
;; return a complete file ;; return a complete file
(if (and (contains? (:features file) "fdata/pointer-map") (if (and (contains? (:features file) "fdata/pointer-map")
(not (contains? (:features params) "fdata/pointer-map"))) (not (contains? (:features params) "fdata/pointer-map")))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (feat.fdata/realize-pointers cfg file)
(update file :data feat.fdata/process-pointers deref))
file) file)
;; This operation is needed for backward comapatibility with ;; This operation is needed for backward comapatibility with
@@ -352,7 +272,7 @@
;; just converts all objects map instaces to plain maps ;; just converts all objects map instaces to plain maps
(if (and (contains? (:features file) "fdata/objects-map") (if (and (contains? (:features file) "fdata/objects-map")
(not (contains? (:features params) "fdata/objects-map"))) (not (contains? (:features params) "fdata/objects-map")))
(update file :data feat.fdata/process-objects (partial into {})) (feat.fdata/realize-objects cfg file)
file))))) file)))))
;; --- COMMAND QUERY: get-file-fragment (by id) ;; --- COMMAND QUERY: get-file-fragment (by id)
@@ -372,10 +292,8 @@
(defn- get-file-fragment (defn- get-file-fragment
[cfg file-id fragment-id] [cfg file-id fragment-id]
(let [resolve-file-data (partial feat.fdata/resolve-file-data cfg)] (some-> (db/get cfg :file-data {:file-id file-id :id fragment-id :type "fragment"})
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id}) (update :data blob/decode)))
(resolve-file-data)
(update :data blob/decode))))
(sv/defmethod ::get-file-fragment (sv/defmethod ::get-file-fragment
"Retrieve a file fragment by its ID. Only authenticated users." "Retrieve a file fragment by its ID. Only authenticated users."
@@ -534,7 +452,7 @@
(let [perms (get-permissions conn profile-id file-id share-id) (let [perms (get-permissions conn profile-id file-id share-id)
file (get-file cfg file-id :read-only? true) file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)}) proj (db/get conn :project {:id (:project-id file)})
@@ -608,81 +526,68 @@
{:components components {:components components
:variant-ids variant-ids})) :variant-ids variant-ids}))
;;coalesce(string_agg(flr.library_file_id::text, ','), '') as library_file_ids
(def ^:private sql:team-shared-files (def ^:private sql:team-shared-files
"with file_library_agg as ( "WITH file_library_agg AS (
select flr.file_id, SELECT flr.file_id,
coalesce(array_agg(flr.library_file_id) filter (where flr.library_file_id is not null), '{}') as library_file_ids coalesce(array_agg(flr.library_file_id) filter (WHERE flr.library_file_id IS NOT NULL), '{}') AS library_file_ids
from file_library_rel flr FROM file_library_rel flr
group by flr.file_id GROUP BY flr.file_id
) )
select f.id, SELECT f.id,
f.revn, fla.library_file_ids,
f.vern, ft.media_id AS thumbnail_id
f.data, FROM file AS f
f.project_id, INNER JOIN project AS p ON (p.id = f.project_id)
f.created_at, LEFT JOIN file_thumbnail AS ft ON (ft.file_id = f.id AND ft.revn = f.revn AND ft.deleted_at IS NULL)
f.modified_at, LEFT JOIN file_library_agg AS fla ON (fla.file_id = f.id)
f.data_backend, WHERE f.is_shared = true
f.data_ref_id, AND f.deleted_at is null
f.name, AND p.deleted_at is null
f.version, AND p.team_id = ?
f.is_shared, ORDER BY f.modified_at DESC")
ft.media_id,
p.team_id,
fla.library_file_ids
from file as f
inner join project as p on (p.id = f.project_id)
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn and ft.deleted_at is null)
left join file_library_agg as fla on fla.file_id = f.id
where f.is_shared = true
and f.deleted_at is null
and p.deleted_at is null
and p.team_id = ?
order by f.modified_at desc")
(defn- get-library-summary (defn- get-library-summary
[cfg {:keys [id data] :as file}] [{:keys [data] :as file}]
(letfn [(assets-sample [assets limit] (let [assets-sample
(let [sorted-assets (->> (vals assets) (fn [assets limit]
(sort-by #(str/lower (:name %))))] (let [sorted-assets (->> (vals assets)
{:count (count sorted-assets) (sort-by #(str/lower (:name %))))]
:sample (into [] (take limit sorted-assets))}))] {:count (count sorted-assets)
:sample (into [] (take limit sorted-assets))}))
load-objects
(fn [component]
(ctf/load-component-objects data component))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] comps-and-variants
(let [load-objects (fn [component] (components-and-variants (ctkl/components-seq data))
(ctf/load-component-objects data component))
comps-and-variants (components-and-variants (ctkl/components-seq data)) components
components (into {} (map (juxt :id identity) (:components comps-and-variants))) (into {} (map (juxt :id identity) (:components comps-and-variants)))
components-sample (-> (assets-sample components 4)
(update :sample #(mapv load-objects %)) components-sample
(assoc :variants-count (-> comps-and-variants :variant-ids count)))] (-> (assets-sample components 4)
{:components components-sample (update :sample #(mapv load-objects %))
:media (assets-sample (:media data) 3) (assoc :variants-count (-> comps-and-variants :variant-ids count)))]
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)})))) {:components components-sample
:media (assets-sample (:media data) 3)
:colors (assets-sample (:colors data) 3)
:typographies (assets-sample (:typographies data) 3)}))
(defn- get-team-shared-files (defn- get-team-shared-files
[{:keys [::db/conn] :as cfg} {:keys [team-id profile-id]}] [{:keys [::db/conn] :as cfg} {:keys [team-id profile-id]}]
(teams/check-read-permissions! conn profile-id team-id) (teams/check-read-permissions! conn profile-id team-id)
(->> (db/exec! conn [sql:team-shared-files team-id]) (let [xform (map (fn [{:keys [id library-file-ids]}]
(into #{} (comp (let [file (bfc/get-file cfg id :migrate? false)
;; NOTE: this decode operation is a workaround for a summ (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
;; fast fix, this should be approached with a more (get-library-summary file))]
;; efficient implementation, for now it loads all (-> file
;; the files in memory. (dissoc :data)
(map (partial bfc/decode-file cfg)) (assoc :library-file-ids (db/decode-pgarray library-file-ids #{}))
(map (fn [row] (assoc :library-summary summ)))))]
(if-let [media-id (:media-id row)] (->> (db/plan conn [sql:team-shared-files team-id] {:fetch-size 1})
(-> row (transduce xform conj #{}))))
(dissoc :media-id)
(assoc :thumbnail-id media-id))
(dissoc row :media-id))))
(map (fn [row]
(update row :library-file-ids db/decode-pgarray #{})))
(map #(assoc % :library-summary (get-library-summary cfg %)))
(map #(dissoc % :data))))))
(def ^:private schema:get-team-shared-files (def ^:private schema:get-team-shared-files
[:map {:title "get-team-shared-files"} [:map {:title "get-team-shared-files"}
@@ -795,9 +700,9 @@
:project-id project-id :project-id project-id
:file-id id) :file-id id)
file (get-file cfg id file (bfc/get-file cfg id
:project-id project-id :project-id project-id
:read-only? true)] :read-only? true)]
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-client-features! (:features params)) (cfeat/check-client-features! (:features params))
@@ -887,7 +792,7 @@
;; --- MUTATION COMMAND: set-file-shared ;; --- MUTATION COMMAND: set-file-shared
(def sql:get-referenced-files (def ^:private sql:get-referenced-files
"SELECT f.id "SELECT f.id
FROM file_library_rel AS flr FROM file_library_rel AS flr
INNER JOIN file AS f ON (f.id = flr.file_id) INNER JOIN file AS f ON (f.id = flr.file_id)
@@ -898,56 +803,51 @@
(defn- absorb-library-by-file! (defn- absorb-library-by-file!
[cfg ldata file-id] [cfg ldata file-id]
(dm/assert! (assert (db/connection-map? cfg)
"expected cfg with valid connection" "expected cfg with valid connection")
(db/connection-map? cfg))
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id) (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)
pmap/*tracked* (pmap/create-tracked)] pmap/*tracked* (pmap/create-tracked)]
(let [file (-> (get-file cfg file-id (let [file (-> (bfc/get-file cfg file-id
:include-deleted? true :include-deleted? true
:lock-for-update? true) :lock-for-update? true)
(update :data ctf/absorb-assets ldata))] (update :data ctf/absorb-assets ldata))]
(l/trc :hint "library absorbed" (l/trc :hint "library absorbed"
:library-id (str (:id ldata)) :library-id (str (:id ldata))
:file-id (str file-id)) :file-id (str file-id))
(db/update! cfg :file (bfc/update-file! cfg {:id file-id
{:revn (inc (:revn file)) :migrations (:migrations file)
:data (blob/encode (:data file)) :revn (inc (:revn file))
:modified-at (ct/now) :data (:data file)
:has-media-trimmed false} :modified-at (ct/now)
{:id file-id}) :has-media-trimmed false}))))
(feat.fdata/persist-pointers! cfg file-id))))
(defn- absorb-library (defn- absorb-library
"Find all files using a shared library, and absorb all library assets "Find all files using a shared library, and absorb all library assets
into the file local libraries" into the file local libraries"
[cfg {:keys [id] :as library}] [cfg {:keys [id data] :as library}]
(dm/assert! (assert (db/connection-map? cfg)
"expected cfg with valid connection" "expected cfg with valid connection")
(db/connection-map? cfg))
(let [ldata (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (let [ids (->> (db/exec! cfg [sql:get-referenced-files id])
(-> library :data (feat.fdata/process-pointers deref))) (sequence bfc/xf-map-id))]
ids (->> (db/exec! cfg [sql:get-referenced-files id])
(map :id))]
(l/trc :hint "absorbing library" (l/trc :hint "absorbing library"
:library-id (str id) :library-id (str id)
:files (str/join "," (map str ids))) :files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file! cfg ldata) ids) (run! (partial absorb-library-by-file! cfg data) ids)
library)) library))
(defn absorb-library! (defn absorb-library!
[{:keys [::db/conn] :as cfg} id] [{:keys [::db/conn] :as cfg} id]
(let [file (-> (get-file cfg id (let [file (-> (bfc/get-file cfg id
:lock-for-update? true :realize? true
:include-deleted? true) :lock-for-update? true
:include-deleted? true)
(check-version!)) (check-version!))
proj (db/get* conn :project {:id (:project-id file)} proj (db/get* conn :project {:id (:project-id file)}

View File

@@ -8,6 +8,7 @@
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.files.migrations :as fmg]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.types.file :as ctf] [app.common.types.file :as ctf]
@@ -45,12 +46,14 @@
(binding [pmap/*tracked* (pmap/create-tracked) (binding [pmap/*tracked* (pmap/create-tracked)
cfeat/*current* features] cfeat/*current* features]
(let [file (ctf/make-file {:id id (let [file (ctf/make-file {:id id
:project-id project-id :project-id project-id
:name name :name name
:revn revn :revn revn
:is-shared is-shared :is-shared is-shared
:features features :features features
:migrations fmg/available-migrations
:ignore-sync-until ignore-sync-until :ignore-sync-until ignore-sync-until
:created-at modified-at :created-at modified-at
:deleted-at deleted-at} :deleted-at deleted-at}
@@ -66,7 +69,7 @@
{:modified-at (ct/now)} {:modified-at (ct/now)}
{:id project-id}) {:id project-id})
file))) (bfc/get-file cfg (:id file)))))
(def ^:private schema:create-file (def ^:private schema:create-file
[:map {:title "create-file"} [:map {:title "create-file"}

View File

@@ -8,52 +8,20 @@
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.files.migrations :as fmg]
[app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata] [app.features.file-snapshots :as fsnap]
[app.features.file-migrations :refer [reset-migrations!]] [app.features.logical-deletion :as ldel]
[app.main :as-alias main] [app.main :as-alias main]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.quotes :as quotes] [app.rpc.quotes :as quotes]
[app.storage :as sto] [app.util.services :as sv]))
[app.util.blob :as blob]
[app.util.services :as sv]
[cuerdas.core :as str]))
(defn decode-row
[{:keys [migrations] :as row}]
(when row
(cond-> row
(some? migrations)
(assoc :migrations (db/decode-pgarray migrations)))))
(def sql:get-file-snapshots
"WITH changes AS (
SELECT id, label, revn, created_at, created_by, profile_id, locked_by
FROM file_change
WHERE file_id = ?
AND data IS NOT NULL
AND (deleted_at IS NULL OR deleted_at > now())
), versions AS (
(SELECT * FROM changes WHERE created_by = 'system' LIMIT 1000)
UNION ALL
(SELECT * FROM changes WHERE created_by != 'system' LIMIT 1000)
)
SELECT * FROM versions
ORDER BY created_at DESC;")
(defn get-file-snapshots
[conn file-id]
(db/exec! conn [sql:get-file-snapshots file-id]))
(def ^:private schema:get-file-snapshots (def ^:private schema:get-file-snapshots
[:map {:title "get-file-snapshots"} [:map {:title "get-file-snapshots"}
@@ -65,73 +33,7 @@
[cfg {:keys [::rpc/profile-id file-id] :as params}] [cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/run! cfg (fn [{:keys [::db/conn]}] (db/run! cfg (fn [{:keys [::db/conn]}]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(get-file-snapshots conn file-id)))) (fsnap/get-visible-snapshots conn file-id))))
(defn- generate-snapshot-label
[]
(let [ts (-> (ct/now)
(ct/format-inst)
(str/replace #"[T:\.]" "-")
(str/rtrim "Z"))]
(str "snapshot-" ts)))
(defn create-file-snapshot!
[cfg file & {:keys [label created-by deleted-at profile-id]
:or {deleted-at :default
created-by :system}}]
(assert (#{:system :user :admin} created-by)
"expected valid keyword for created-by")
(let [created-by
(name created-by)
deleted-at
(cond
(= deleted-at :default)
(ct/plus (ct/now) (cf/get-deletion-delay))
(ct/inst? deleted-at)
deleted-at
:else
nil)
label
(or label (generate-snapshot-label))
snapshot-id
(uuid/next)
data
(blob/encode (:data file))
features
(into-array (:features file))
migrations
(into-array (:migrations file))]
(l/dbg :hint "creating file snapshot"
:file-id (str (:id file))
:id (str snapshot-id)
:label label)
(db/insert! cfg :file-change
{:id snapshot-id
:revn (:revn file)
:data data
:version (:version file)
:features features
:migrations migrations
:profile-id profile-id
:file-id (:id file)
:label label
:deleted-at deleted-at
:created-by created-by}
{::db/return-keys false})
{:id snapshot-id :label label}))
(def ^:private schema:create-file-snapshot (def ^:private schema:create-file-snapshot
[:map [:map
@@ -144,7 +46,7 @@
::db/transaction true} ::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}] [{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
(files/check-edition-permissions! conn profile-id file-id) (files/check-edition-permissions! conn profile-id file-id)
(let [file (bfc/get-file cfg file-id) (let [file (bfc/get-file cfg file-id :realize? true)
project (db/get-by-id cfg :project (:project-id file))] project (db/get-by-id cfg :project (:project-id file))]
(-> cfg (-> cfg
@@ -155,96 +57,10 @@
(quotes/check! {::quotes/id ::quotes/snapshots-per-file} (quotes/check! {::quotes/id ::quotes/snapshots-per-file}
{::quotes/id ::quotes/snapshots-per-team})) {::quotes/id ::quotes/snapshots-per-team}))
(create-file-snapshot! cfg file (fsnap/create! cfg file
{:label label {:label label
:profile-id profile-id :profile-id profile-id
:created-by :user}))) :created-by "user"})))
(defn restore-file-snapshot!
[{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id]
(let [storage (sto/resolve cfg {::db/reuse-conn true})
file (files/get-minimal-file conn file-id {::db/for-update true})
vern (rand-int Integer/MAX_VALUE)
snapshot (some->> (db/get* conn :file-change
{:file-id file-id
:id snapshot-id}
{::db/for-share true})
(feat.fdata/resolve-file-data cfg)
(decode-row))
;; If snapshot has tracked applied migrations, we reuse them,
;; if not we take a safest set of migrations as starting
;; point. This is because, at the time of implementing
;; snapshots, migrations were not taken into account so we
;; need to make this backward compatible in some way.
file (assoc file :migrations
(or (:migrations snapshot)
(fmg/generate-migrations-from-version 67)))]
(when-not snapshot
(ex/raise :type :not-found
:code :snapshot-not-found
:hint "unable to find snapshot with the provided label"
:snapshot-id snapshot-id
:file-id file-id))
(when-not (:data snapshot)
(ex/raise :type :validation
: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 restoring 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)))
;; In the same way, on reseting the file data, we need to restore
;; the applied migrations on the moment of taking the snapshot
(reset-migrations! conn file)
(db/update! conn :file
{:data (:data snapshot)
:revn (inc (:revn file))
:vern vern
:version (:version snapshot)
: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)))
;; Send to the clients a notification to reload the file
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restore
:file-id (:id file)
:vern vern})
{:id (:id snapshot)
:label (:label snapshot)}))
(def ^:private schema:restore-file-snapshot (def ^:private schema:restore-file-snapshot
[:map {:title "restore-file-snapshot"} [:map {:title "restore-file-snapshot"}
@@ -253,88 +69,76 @@
(sv/defmethod ::restore-file-snapshot (sv/defmethod ::restore-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:restore-file-snapshot} ::sm/params schema:restore-file-snapshot
[cfg {:keys [::rpc/profile-id file-id id] :as params}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
(fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id)
(files/check-edition-permissions! conn profile-id file-id) (let [file (bfc/get-file cfg file-id)
(let [file (bfc/get-file cfg file-id)] team (teams/get-team conn
(create-file-snapshot! cfg file :profile-id profile-id
{:profile-id profile-id :file-id file-id)
:created-by :system}) delay (ldel/get-deletion-delay team)]
(restore-file-snapshot! cfg file-id id)))))
(fsnap/create! cfg file
{:profile-id profile-id
:deleted-at (ct/in-future delay)
:created-by "system"})
(let [vern (fsnap/restore! cfg file-id id)]
;; Send to the clients a notification to reload the file
(mbus/pub! msgbus
:topic (:id file)
:message {:type :file-restore
:file-id (:id file)
:vern vern})
nil)))
(def ^:private schema:update-file-snapshot (def ^:private schema:update-file-snapshot
[:map {:title "update-file-snapshot"} [:map {:title "update-file-snapshot"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:label ::sm/text]]) [:label ::sm/text]])
(defn- update-file-snapshot!
[conn snapshot-id label]
(-> (db/update! conn :file-change
{:label label
:created-by "user"
:deleted-at nil}
{:id snapshot-id}
{::db/return-keys true})
(dissoc :data :features :migrations)))
(defn- get-snapshot
"Get a minimal snapshot from database and lock for update"
[conn id]
(db/get conn :file-change
{:id id}
{::sql/columns [:id :file-id :created-by :deleted-at :profile-id :locked-by]
::db/for-update true}))
(sv/defmethod ::update-file-snapshot (sv/defmethod ::update-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:update-file-snapshot} ::sm/params schema:update-file-snapshot
[cfg {:keys [::rpc/profile-id id label]}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn]} {:keys [::rpc/profile-id id label]}]
(fn [{:keys [::db/conn]}] (let [snapshot (fsnap/get-minimal-snapshot conn id)]
(let [snapshot (get-snapshot conn id)] (files/check-edition-permissions! conn profile-id (:file-id snapshot))
(files/check-edition-permissions! conn profile-id (:file-id snapshot)) (fsnap/update! conn (assoc snapshot :label label))))
(update-file-snapshot! conn id label)))))
(def ^:private schema:remove-file-snapshot (def ^:private schema:remove-file-snapshot
[:map {:title "remove-file-snapshot"} [:map {:title "remove-file-snapshot"}
[:id ::sm/uuid]]) [:id ::sm/uuid]])
(defn- delete-file-snapshot!
[conn snapshot-id]
(db/update! conn :file-change
{:deleted-at (ct/now)}
{:id snapshot-id}
{::db/return-keys false})
nil)
(sv/defmethod ::delete-file-snapshot (sv/defmethod ::delete-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:remove-file-snapshot} ::sm/params schema:remove-file-snapshot
[cfg {:keys [::rpc/profile-id id]}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
(fn [{:keys [::db/conn]}] (let [snapshot (fsnap/get-minimal-snapshot conn id)]
(let [snapshot (get-snapshot conn id)] (files/check-edition-permissions! conn profile-id (:file-id snapshot))
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
(when (not= (:created-by snapshot) "user") (when (not= (:created-by snapshot) "user")
(ex/raise :type :validation (ex/raise :type :validation
:code :system-snapshots-cant-be-deleted :code :system-snapshots-cant-be-deleted
:snapshot-id id :file-id (:file-id snapshot)
:profile-id profile-id)) :snapshot-id id
:profile-id profile-id))
;; Check if version is locked by someone else (when (and (some? (:locked-by snapshot))
(when (and (:locked-by snapshot) (not= (:locked-by snapshot) profile-id))
(not= (:locked-by snapshot) profile-id)) (ex/raise :type :validation
(ex/raise :type :validation :code :snapshot-is-locked
:code :snapshot-is-locked :file-id (:file-id snapshot)
:hint "Cannot delete a locked version" :snapshot-id id
:snapshot-id id :profile-id profile-id))
:profile-id profile-id
:locked-by (:locked-by snapshot)))
(delete-file-snapshot! conn id))))) (let [team (teams/get-team conn
:profile-id profile-id
:file-id (:file-id snapshot))
delay (ldel/get-deletion-delay team)]
(fsnap/delete! conn (assoc snapshot :deleted-at (ct/in-future delay))))))
;;; Lock/unlock version endpoints ;;; Lock/unlock version endpoints
@@ -342,93 +146,75 @@
[:map {:title "lock-file-snapshot"} [:map {:title "lock-file-snapshot"}
[:id ::sm/uuid]]) [:id ::sm/uuid]])
(defn- lock-file-snapshot!
[conn snapshot-id profile-id]
(db/update! conn :file-change
{:locked-by profile-id}
{:id snapshot-id}
{::db/return-keys false})
nil)
(sv/defmethod ::lock-file-snapshot (sv/defmethod ::lock-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:lock-file-snapshot} ::sm/params schema:lock-file-snapshot
[cfg {:keys [::rpc/profile-id id]}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
(fn [{:keys [::db/conn]}] (let [snapshot (fsnap/get-minimal-snapshot conn id)]
(let [snapshot (get-snapshot conn id)] (files/check-edition-permissions! conn profile-id (:file-id snapshot))
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
(when (not= (:created-by snapshot) "user") (when (not= (:created-by snapshot) "user")
(ex/raise :type :validation (ex/raise :type :validation
:code :system-snapshots-cant-be-locked :code :system-snapshots-cant-be-locked
:hint "Only user-created versions can be locked" :hint "Only user-created versions can be locked"
:snapshot-id id :snapshot-id id
:profile-id profile-id)) :profile-id profile-id))
;; Only the creator can lock their own version ;; Only the creator can lock their own version
(when (not= (:profile-id snapshot) profile-id) (when (not= (:profile-id snapshot) profile-id)
(ex/raise :type :validation (ex/raise :type :validation
:code :only-creator-can-lock :code :only-creator-can-lock
:hint "Only the version creator can lock it" :hint "Only the version creator can lock it"
:snapshot-id id :snapshot-id id
:profile-id profile-id :profile-id profile-id
:creator-id (:profile-id snapshot))) :creator-id (:profile-id snapshot)))
;; Check if already locked ;; Check if already locked
(when (:locked-by snapshot) (when (:locked-by snapshot)
(ex/raise :type :validation (ex/raise :type :validation
:code :snapshot-already-locked :code :snapshot-already-locked
:hint "Version is already locked" :hint "Version is already locked"
:snapshot-id id :snapshot-id id
:profile-id profile-id :profile-id profile-id
:locked-by (:locked-by snapshot))) :locked-by (:locked-by snapshot)))
(lock-file-snapshot! conn id profile-id))))) (fsnap/lock-by! conn id profile-id)))
(def ^:private schema:unlock-file-snapshot (def ^:private schema:unlock-file-snapshot
[:map {:title "unlock-file-snapshot"} [:map {:title "unlock-file-snapshot"}
[:id ::sm/uuid]]) [:id ::sm/uuid]])
(defn- unlock-file-snapshot!
[conn snapshot-id]
(db/update! conn :file-change
{:locked-by nil}
{:id snapshot-id}
{::db/return-keys false})
nil)
(sv/defmethod ::unlock-file-snapshot (sv/defmethod ::unlock-file-snapshot
{::doc/added "1.20" {::doc/added "1.20"
::sm/params schema:unlock-file-snapshot} ::sm/params schema:unlock-file-snapshot
[cfg {:keys [::rpc/profile-id id]}] ::db/transaction true}
(db/tx-run! cfg [{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
(fn [{:keys [::db/conn]}] (let [snapshot (fsnap/get-minimal-snapshot conn id)]
(let [snapshot (get-snapshot conn id)] (files/check-edition-permissions! conn profile-id (:file-id snapshot))
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
(when (not= (:created-by snapshot) "user") (when (not= (:created-by snapshot) "user")
(ex/raise :type :validation (ex/raise :type :validation
:code :system-snapshots-cant-be-unlocked :code :system-snapshots-cant-be-unlocked
:hint "Only user-created versions can be unlocked" :hint "Only user-created versions can be unlocked"
:snapshot-id id :snapshot-id id
:profile-id profile-id)) :profile-id profile-id))
;; Only the creator can unlock their own version ;; Only the creator can unlock their own version
(when (not= (:profile-id snapshot) profile-id) (when (not= (:profile-id snapshot) profile-id)
(ex/raise :type :validation (ex/raise :type :validation
:code :only-creator-can-unlock :code :only-creator-can-unlock
:hint "Only the version creator can unlock it" :hint "Only the version creator can unlock it"
:snapshot-id id :snapshot-id id
:profile-id profile-id :profile-id profile-id
:creator-id (:profile-id snapshot))) :creator-id (:profile-id snapshot)))
;; Check if not locked ;; Check if not locked
(when (not (:locked-by snapshot)) (when (not (:locked-by snapshot))
(ex/raise :type :validation (ex/raise :type :validation
:code :snapshot-not-locked :code :snapshot-not-locked
:hint "Version is not locked" :hint "Version is not locked"
:snapshot-id id :snapshot-id id
:profile-id profile-id)) :profile-id profile-id))
(unlock-file-snapshot! conn id))))) (fsnap/unlock! conn id)))

View File

@@ -1,160 +0,0 @@
;; 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-temp
(:require
[app.common.exceptions :as ex]
[app.common.features :as cfeat]
[app.common.files.changes :as cpc]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as sql]
[app.features.fdata :as fdata]
[app.loggers.audit :as audit]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.files-create :as files.create]
[app.rpc.commands.files-update :as-alias files.update]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.blob :as blob]
[app.util.pointer-map :as pmap]
[app.util.services :as sv]
[clojure.set :as set]))
;; --- MUTATION COMMAND: create-temp-file
(def ^:private schema:create-temp-file
[:map {:title "create-temp-file"}
[:name [:string {:max 250}]]
[:project-id ::sm/uuid]
[:id {:optional true} ::sm/uuid]
[:is-shared ::sm/boolean]
[:features ::cfeat/features]
[:create-page ::sm/boolean]])
(sv/defmethod ::create-temp-file
{::doc/added "1.17"
::doc/module :files
::sm/params schema:create-temp-file
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
(projects/check-edition-permissions! conn profile-id project-id)
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
;; When we create files, we only need to respect the team
;; features, because some features can be enabled
;; globally, but the team is still not migrated properly.
input-features
(:features params #{})
;; If the imported project doesn't contain v2 we need to remove it
team-features
(cond-> (cfeat/get-team-enabled-features cf/flags team)
(not (contains? input-features "components/v2"))
(disj "components/v2"))
;; We also include all no migration features declared by
;; client; that enables the ability to enable a runtime
;; feature on frontend and make it permanent on file
features
(-> input-features
(set/intersection cfeat/no-migration-features)
(set/union team-features))
params
(-> params
(assoc :profile-id profile-id)
(assoc :deleted-at (ct/in-future {:days 1}))
(assoc :features features))]
(files.create/create-file cfg params)))
;; --- MUTATION COMMAND: update-temp-file
(def ^:private schema:update-temp-file
[:map {:title "update-temp-file"}
[:changes [:vector cpc/schema:change]]
[:revn [::sm/int {:min 0}]]
[:session-id ::sm/uuid]
[:id ::sm/uuid]])
(sv/defmethod ::update-temp-file
{::doc/added "1.17"
::doc/module :files
::sm/params schema:update-temp-file}
[cfg {:keys [::rpc/profile-id session-id id revn changes] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
(db/insert! conn :file-change
{:id (uuid/next)
:session-id session-id
:profile-id profile-id
:created-at (ct/now)
:file-id id
:revn revn
:data nil
:changes (blob/encode changes)})
(rph/with-meta (rph/wrap nil)
{::audit/replace-props {:file-id id
:revn revn}}))))
;; --- MUTATION COMMAND: persist-temp-file
(defn persist-temp-file
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
(let [file (files/get-file cfg id
:migrate? false
:lock-for-update? true)]
(when (nil? (:deleted-at file))
(ex/raise :type :validation
:code :cant-persist-already-persisted-file))
(let [changes (->> (db/cursor conn
(sql/select :file-change {:file-id id}
{:order-by [[:revn :asc]]})
{:chunk-size 10})
(sequence (mapcat (comp blob/decode :changes))))
file (update file :data cpc/process-changes changes)
file (if (contains? (:features file) "fdata/objects-map")
(fdata/enable-objects-map file)
file)
file (if (contains? (:features file) "fdata/pointer-map")
(binding [pmap/*tracked* (pmap/create-tracked)]
(let [file (fdata/enable-pointer-map file)]
(fdata/persist-pointers! cfg id)
file))
file)]
;; Delete changes from the changes history
(db/delete! conn :file-change {:file-id id})
(db/update! conn :file
{:deleted-at nil
:revn 1
:data (blob/encode (:data file))}
{:id id})
nil)))
(def ^:private schema:persist-temp-file
[:map {:title "persist-temp-file"}
[:id ::sm/uuid]])
(sv/defmethod ::persist-temp-file
{::doc/added "1.17"
::doc/module :files
::sm/params schema:persist-temp-file}
[cfg {:keys [::rpc/profile-id id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id id)
(persist-temp-file cfg params))))

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.files-thumbnails (ns app.rpc.commands.files-thumbnails
(:require (:require
[app.binfile.common :as bfc]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.features :as cfeat] [app.common.features :as cfeat]
@@ -202,9 +203,9 @@
:profile-id profile-id :profile-id profile-id
:file-id file-id) :file-id file-id)
file (files/get-file cfg file-id file (bfc/get-file cfg file-id
:preload-pointers? true :realize? true
:read-only? true)] :read-only? true)]
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
@@ -339,6 +340,7 @@
data (-> (sto/content path) data (-> (sto/content path)
(sto/wrap-with-hash hash)) (sto/wrap-with-hash hash))
tnow (ct/now) tnow (ct/now)
media (sto/put-object! storage media (sto/put-object! storage
{::sto/content data {::sto/content data
::sto/deduplicate? true ::sto/deduplicate? true

View File

@@ -19,8 +19,8 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.features.fdata :as feat.fdata] [app.features.fdata :as fdata]
[app.features.file-migrations :as feat.fmigr] [app.features.file-snapshots :as fsnap]
[app.features.logical-deletion :as ldel] [app.features.logical-deletion :as ldel]
[app.http.errors :as errors] [app.http.errors :as errors]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
@@ -33,7 +33,6 @@
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.storage :as sto]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.util.services :as sv] [app.util.services :as sv]
@@ -127,76 +126,78 @@
::sm/params schema:update-file ::sm/params schema:update-file
::sm/result schema:update-file-result ::sm/result schema:update-file-result
::doc/module :files ::doc/module :files
::doc/added "1.17"} ::doc/added "1.17"
[{:keys [::mtx/metrics] :as cfg} ::db/transaction true}
[{:keys [::mtx/metrics ::db/conn] :as cfg}
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}] {:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id id)
(db/xact-lock! conn id)
(let [file (get-file conn id) (files/check-edition-permissions! conn profile-id id)
team (teams/get-team conn (db/xact-lock! conn id)
:profile-id profile-id
:team-id (:team-id file))
features (-> (cfeat/get-team-enabled-features cf/flags team) (let [file (get-file cfg id)
(cfeat/check-client-features! (:features params)) team (teams/get-team conn
(cfeat/check-file-features! (:features file))) :profile-id profile-id
:team-id (:team-id file))
changes (if changes-with-metadata features (-> (cfeat/get-team-enabled-features cf/flags team)
(->> changes-with-metadata (mapcat :changes) vec) (cfeat/check-client-features! (:features params))
(vec changes)) (cfeat/check-file-features! (:features file)))
params (-> params changes (if changes-with-metadata
(assoc :profile-id profile-id) (->> changes-with-metadata (mapcat :changes) vec)
(assoc :features (set/difference features cfeat/frontend-only-features)) (vec changes))
(assoc :team team)
(assoc :file file)
(assoc :changes changes))
cfg (assoc cfg ::timestamp (ct/now)) params (-> params
(assoc :profile-id profile-id)
(assoc :features (set/difference features cfeat/frontend-only-features))
(assoc :team team)
(assoc :file file)
(assoc :changes changes))
tpoint (ct/tpoint)] cfg (assoc cfg ::timestamp (ct/now))
(when (not= (:vern params) tpoint (ct/tpoint)]
(:vern file))
(ex/raise :type :validation
:code :vern-conflict
:hint "A different version has been restored for the file."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(when (> (:revn params) (when (not= (:vern params)
(:revn file)) (:vern file))
(ex/raise :type :validation (ex/raise :type :validation
:code :revn-conflict :code :vern-conflict
:hint "The incoming revision number is greater that stored version." :hint "A different version has been restored for the file."
:context {:incoming-revn (:revn params) :context {:incoming-revn (:revn params)
:stored-revn (:revn file)})) :stored-revn (:revn file)}))
;; When newly computed features does not match exactly with (when (> (:revn params)
;; the features defined on team row, we update it (:revn file))
(when-let [features (-> features (ex/raise :type :validation
(set/difference (:features team)) :code :revn-conflict
(set/difference cfeat/no-team-inheritable-features) :hint "The incoming revision number is greater that stored version."
(not-empty))] :context {:incoming-revn (:revn params)
(let [features (-> features :stored-revn (:revn file)}))
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team
{:features features}
{:id (:id team)}
{::db/return-keys false})))
(mtx/run! metrics {:id :update-file-changes :inc (count changes)}) ;; When newly computed features does not match exactly with the
;; features defined on team row, we update it
(when-let [features (-> features
(set/difference (:features team))
(set/difference cfeat/no-team-inheritable-features)
(not-empty))]
(let [features (-> features
(set/union (:features team))
(set/difference cfeat/no-team-inheritable-features)
(into-array))]
(db/update! conn :team
{:features features}
{:id (:id team)}
{::db/return-keys false})))
(binding [l/*context* (some-> (meta params)
(get :app.http/request) (mtx/run! metrics {:id :update-file-changes :inc (count changes)})
(errors/request->context))]
(-> (update-file* cfg params) (binding [l/*context* (some-> (meta params)
(rph/with-defer #(let [elapsed (tpoint)] (get :app.http/request)
(l/trace :hint "update-file" :time (ct/format-duration elapsed)))))))))) (errors/request->context))]
(-> (update-file* cfg params)
(rph/with-defer #(let [elapsed (tpoint)]
(l/trace :hint "update-file" :time (ct/format-duration elapsed))))))))
(defn- update-file* (defn- update-file*
"Internal function, part of the update-file process, that encapsulates "Internal function, part of the update-file process, that encapsulates
@@ -209,24 +210,38 @@
[{:keys [::db/conn ::timestamp] :as cfg} [{:keys [::db/conn ::timestamp] :as cfg}
{:keys [profile-id file team features changes session-id skip-validate] :as params}] {:keys [profile-id file team features changes session-id skip-validate] :as params}]
(let [;; Retrieve the file data (binding [pmap/*tracked* (pmap/create-tracked)
file (feat.fmigr/resolve-applied-migrations cfg file) pmap/*load-fn* (partial fdata/load-pointer cfg (:id file))]
file (feat.fdata/resolve-file-data cfg file)
file (assoc file :features
(-> features
(set/difference cfeat/frontend-only-features)
(set/union (:features file))))]
;; We create a new lexycal scope for clearly delimit the result of (let [file (assoc file :features
;; executing this update file operation and all its side effects (-> features
(let [file (binding [cfeat/*current* features (set/difference cfeat/frontend-only-features)
cfeat/*previous* (:features file)] (set/union (:features file))))
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))]
(feat.fmigr/upsert-migrations! conn file) ;; We need to preserve the original revn for the response
(persist-file! cfg file) revn
(get file :revn)
file
(binding [cfeat/*current* features
cfeat/*previous* (:features file)]
(update-file-data! cfg file
process-changes-and-validate
changes skip-validate))
deleted-at
(ct/plus timestamp (ct/duration {:hours 1}))]
(when-let [file (::snapshot file)]
(let [deleted-at (ct/plus timestamp (ldel/get-deletion-delay team))
label (str "internal/snapshot/" revn)]
(fsnap/create! cfg file
{:label label
:created-by "system"
:deleted-at deleted-at
:profile-id profile-id
:session-id session-id})))
;; Insert change (xlog) with deleted_at in a future data for ;; Insert change (xlog) with deleted_at in a future data for
;; make them automatically eleggible for GC once they expires ;; make them automatically eleggible for GC once they expires
@@ -236,87 +251,62 @@
:profile-id profile-id :profile-id profile-id
:created-at timestamp :created-at timestamp
:updated-at timestamp :updated-at timestamp
:deleted-at (if (::snapshot-data file) :deleted-at deleted-at
(ct/plus timestamp (ldel/get-deletion-delay team))
(ct/plus timestamp (ct/duration {:hours 1})))
:file-id (:id file) :file-id (:id file)
:revn (:revn file) :revn (:revn file)
:version (:version file) :version (:version file)
:features (:features file) :features (into-array (:features file))
:label (::snapshot-label file)
:data (::snapshot-data file)
:changes (blob/encode changes)} :changes (blob/encode changes)}
{::db/return-keys false}) {::db/return-keys false})
(persist-file! cfg file)
;; Send asynchronous notifications ;; Send asynchronous notifications
(send-notifications! cfg params file)) (send-notifications! cfg params file)
(when (feat.fdata/offloaded? file) (with-meta {:revn revn :lagged (get-lagged-changes conn params)}
(let [storage (sto/resolve cfg ::db/reuse-conn true)] {::audit/replace-props
(some->> (:data-ref-id file) (sto/touch-object! storage)))) {:id (:id file)
:name (:name file)
(let [response {:revn (:revn file) :features (:features file)
:lagged (get-lagged-changes conn params)}] :project-id (:project-id file)
(vary-meta response assoc ::audit/replace-props :team-id (:team-id file)}}))))
{:id (:id file)
:name (:name file)
:features (:features file)
:project-id (:project-id file)
:team-id (:team-id file)}))))
(defn update-file!
"A public api that allows apply a transformation to a file with all context setup."
[{:keys [::db/conn] :as cfg} file-id update-fn & args]
(let [file (get-file cfg file-id)
file (apply update-file-data! cfg file update-fn args)]
(feat.fmigr/upsert-migrations! conn file)
(persist-file! cfg file)))
(def ^:private sql:get-file
"SELECT f.*, p.team_id
FROM file AS f
JOIN project AS p ON (p.id = f.project_id)
WHERE f.id = ?
AND (f.deleted_at IS NULL OR
f.deleted_at > now())
FOR KEY SHARE")
(defn get-file (defn get-file
"Get not-decoded file, only decodes the features set." "Get not-decoded file, only decodes the features set."
[conn id] [cfg id]
(let [file (db/exec-one! conn [sql:get-file id])] (bfc/get-file cfg id :decode? false :lock-for-share? true))
(when-not file
(ex/raise :type :not-found
:code :object-not-found
:hint (format "file with id '%s' does not exists" id)))
(update file :features db/decode-pgarray #{})))
(defn persist-file! (defn persist-file!
"Function responsible of persisting already encoded file. Should be "Function responsible of persisting already encoded file. Should be
used together with `get-file` and `update-file-data!`. used together with `get-file` and `update-file-data!`.
It also updates the project modified-at attr." It also updates the project modified-at attr."
[{:keys [::db/conn ::timestamp]} file] [{:keys [::db/conn ::timestamp] :as cfg} file]
(let [;; The timestamp can be nil because this function is also (let [;; The timestamp can be nil because this function is also
;; intended to be used outside of this module ;; intended to be used outside of this module
modified-at (or timestamp (ct/now))] modified-at
(or timestamp (ct/now))
file
(-> file
(dissoc ::snapshot)
(assoc :modified-at modified-at)
(assoc :has-media-trimmed false))]
(db/update! conn :project (db/update! conn :project
{:modified-at modified-at} {:modified-at modified-at}
{:id (:project-id file)} {:id (:project-id file)}
{::db/return-keys false}) {::db/return-keys false})
(db/update! conn :file (bfc/update-file! cfg file)))
{:revn (:revn file)
:data (:data file) (defn- attach-snapshot
:version (:version file) "Attach snapshot data to the file. This should be called before the
:features (:features file) upcoming file operations are applied to the file."
:data-backend nil [cfg migrated? file]
:data-ref-id nil (let [snapshot (if migrated? file (fdata/realize cfg file))]
:modified-at modified-at (assoc file ::snapshot snapshot)))
:has-media-trimmed false}
{:id (:id file)}
{::db/return-keys false})))
(defn- update-file-data! (defn- update-file-data!
"Perform a file data transformation in with all update context setup. "Perform a file data transformation in with all update context setup.
@@ -328,52 +318,35 @@
fdata/pointer-map modified fragments." fdata/pointer-map modified fragments."
[cfg {:keys [id] :as file} update-fn & args] [cfg {:keys [id] :as file} update-fn & args]
(binding [pmap/*tracked* (pmap/create-tracked) (let [file (update file :data (fn [data]
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] (-> data
(let [file (update file :data (fn [data] (blob/decode)
(-> data (assoc :id id))))
(blob/decode) libs (delay (bfc/get-resolved-file-libraries cfg file))
(assoc :id (:id file)))))
libs (delay (bfc/get-resolved-file-libraries cfg file))
;; For avoid unnecesary overhead of creating multiple pointers need-migration?
;; and handly internally with objects map in their worst (fmg/need-migration? file)
;; case (when probably all shapes and all pointers will be
;; readed in any case), we just realize/resolve them before
;; applying the migration to the file
file (if (fmg/need-migration? file)
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(fmg/migrate-file libs))
file)
file (apply update-fn cfg file args) take-snapshot?
(take-snapshot? file)
;; TODO: reuse operations if file is migrated ;; For avoid unnecesary overhead of creating multiple
;; TODO: move encoding to a separated thread ;; pointers and handly internally with objects map in their
file (if (take-snapshot? file) ;; worst case (when probably all shapes and all pointers
(let [tpoint (ct/tpoint) ;; will be readed in any case), we just realize/resolve them
snapshot (-> (:data file) ;; before applying the migration to the file
(feat.fdata/process-pointers deref) file
(feat.fdata/process-objects (partial into {})) (cond-> file
(blob/encode)) ;; need-migration?
elapsed (tpoint) ;; (->> (fdata/realize cfg))
label (str "internal/snapshot/" (:revn file))]
(l/trc :hint "take snapshot" need-migration?
:file-id (str (:id file)) (fmg/migrate-file libs)
:revn (:revn file)
:label label
:elapsed (ct/format-duration elapsed))
(-> file take-snapshot?
(assoc ::snapshot-data snapshot) (->> (attach-snapshot cfg need-migration?)))]
(assoc ::snapshot-label label)))
file)]
(bfc/encode-file cfg file))))
(apply update-fn cfg file args)))
(defn- soft-validate-file-schema! (defn- soft-validate-file-schema!
[file] [file]
@@ -462,8 +435,9 @@
(defn- get-lagged-changes (defn- get-lagged-changes
[conn {:keys [id revn] :as params}] [conn {:keys [id revn] :as params}]
(->> (db/exec! conn [sql:lagged-changes id revn]) (->> (db/exec! conn [sql:lagged-changes id revn])
(map files/decode-row) (filter :changes)
(vec))) (mapv (fn [row]
(update row :changes blob/decode)))))
(defn- send-notifications! (defn- send-notifications!
[cfg {:keys [team changes session-id] :as params} file] [cfg {:keys [team changes session-id] :as params} file]

View File

@@ -35,7 +35,7 @@
(defn duplicate-file (defn duplicate-file
[{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id file-id name reset-shared-flag] :as params}] [{:keys [::db/conn ::bfc/timestamp] :as cfg} {:keys [profile-id file-id name reset-shared-flag] :as params}]
(let [;; We don't touch the original file on duplication (let [;; We don't touch the original file on duplication
file (bfc/get-file cfg file-id) file (bfc/get-file cfg file-id :realize? true)
project-id (:project-id file) project-id (:project-id file)
file (-> file file (-> file
(update :id bfc/lookup-index) (update :id bfc/lookup-index)

View File

@@ -6,6 +6,7 @@
(ns app.rpc.commands.teams-invitations (ns app.rpc.commands.teams-invitations
(:require (:require
[app.binfile.common :as bfc]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@@ -21,7 +22,6 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -550,7 +550,7 @@
"A specific method for obtain a file with name and page-id used for "A specific method for obtain a file with name and page-id used for
team request access procediment" team request access procediment"
[cfg file-id] [cfg file-id]
(let [file (files/get-file cfg file-id :migrate? false)] (let [file (bfc/get-file cfg file-id :migrate? false)]
(-> file (-> file
(dissoc :data) (dissoc :data)
(dissoc :deleted-at) (dissoc :deleted-at)

View File

@@ -51,7 +51,7 @@
(defn- get-view-only-bundle (defn- get-view-only-bundle
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
(let [file (files/get-file cfg file-id) (let [file (bfc/get-file cfg file-id)
project (db/get conn :project project (db/get conn :project
{:id (:project-id file)} {:id (:project-id file)}
@@ -81,7 +81,7 @@
libs (->> (bfc/get-file-libraries conn file-id) libs (->> (bfc/get-file-libraries conn file-id)
(mapv (fn [{:keys [id] :as lib}] (mapv (fn [{:keys [id] :as lib}]
(merge lib (files/get-file cfg id))))) (merge lib (bfc/get-file cfg id)))))
links (->> (db/query conn :share-link {:file-id file-id}) links (->> (db/query conn :share-link {:file-id file-id})
(mapv (fn [row] (mapv (fn [row]

View File

@@ -1,278 +0,0 @@
;; 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.srepl.fixes
"A misc of fix functions"
(:refer-clojure :exclude [parse-uuid])
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes :as cpc]
[app.common.files.helpers :as cfh]
[app.common.files.repair :as cfr]
[app.common.files.validate :as cfv]
[app.common.logging :as l]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[app.db :as db]
[app.features.fdata :as feat.fdata]
[app.srepl.helpers :as h]))
(defn disable-fdata-features
[{:keys [id features] :as file} _]
(when (or (contains? features "fdata/pointer-map")
(contains? features "fdata/objects-map"))
(l/warn :hint "disable fdata features" :file-id (str id))
(-> file
(update :data feat.fdata/process-pointers deref)
(update :data feat.fdata/process-objects (partial into {}))
(update :features disj "fdata/pointer-map" "fdata/objects-map"))))
(def sql:get-fdata-files
"SELECT id FROM file
WHERE deleted_at is NULL
AND (features @> '{fdata/pointer-map}' OR
features @> '{fdata/objects-map}')
ORDER BY created_at DESC")
(defn find-fdata-pointers
[{:keys [id features data] :as file} _]
(when (contains? features "fdata/pointer-map")
(let [pointers (feat.fdata/get-used-pointer-ids data)]
(l/warn :hint "found pointers" :file-id (str id) :pointers pointers)
nil)))
(defn repair-file-media
"A helper intended to be used with `srepl.main/process-files!` that
fixes all not propertly referenced file-media-object for a file"
[{:keys [id data] :as file} & _]
(let [conn (db/get-connection h/*system*)
used (cfh/collect-used-media data)
ids (db/create-array conn "uuid" used)
sql "SELECT * FROM file_media_object WHERE id = ANY(?)"
rows (db/exec! conn [sql ids])
index (reduce (fn [index media]
(if (not= (:file-id media) id)
(let [media-id (uuid/next)]
(l/wrn :hint "found not referenced media"
:file-id (str id)
:media-id (str (:id media)))
(db/insert! conn :file-media-object
(-> media
(assoc :file-id id)
(assoc :id media-id)))
(assoc index (:id media) media-id))
index))
{}
rows)]
(when (seq index)
(binding [bfc/*state* (atom {:index index})]
(update file :data (fn [fdata]
(-> fdata
(update :pages-index #'bfc/relink-shapes)
(update :components #'bfc/relink-shapes)
(update :media #'bfc/relink-media)
(d/without-nils))))))))
(defn repair-file
"Internal helper for validate and repair the file. The operation is
applied multiple times untile file is fixed or max iteration counter
is reached (default 10)"
[file libs & {:keys [max-iterations] :or {max-iterations 10}}]
(let [validate-and-repair
(fn [file libs iteration]
(when-let [errors (not-empty (cfv/validate-file file libs))]
(l/trc :hint "repairing file"
:file-id (str (:id file))
:iteration iteration
:errors (count errors))
(let [changes (cfr/repair-file file libs errors)]
(-> file
(update :revn inc)
(update :data cpc/process-changes changes)))))
process-file
(fn [file libs]
(loop [file file
iteration 0]
(if (< iteration max-iterations)
(if-let [file (validate-and-repair file libs iteration)]
(recur file (inc iteration))
file)
(do
(l/wrn :hint "max retry num reached on repairing file"
:file-id (str (:id file))
:iteration iteration)
file))))
file'
(process-file file libs)]
(when (not= (:revn file) (:revn file'))
(l/trc :hint "file repaired" :file-id (str (:id file))))
file'))
(defn fix-touched-shapes-group
[file _]
;; Remove :shapes-group from the touched elements
(letfn [(fix-fdata [data]
(-> data
(update :pages-index update-vals fix-container)))
(fix-container [container]
(d/update-when container :objects update-vals fix-shape))
(fix-shape [shape]
(d/update-when shape :touched
(fn [touched]
(disj touched :shapes-group))))]
file (-> file
(update :data fix-fdata))))
(defn add-swap-slots
[file libs _opts]
;; Detect swapped copies and try to generate a valid swap-slot.
(letfn [(process-fdata [data]
;; Walk through all containers in the file, both pages and deleted components.
(reduce process-container data (ctf/object-containers-seq data)))
(process-container [data container]
;; Walk through all shapes in depth-first tree order.
(l/dbg :hint "Processing container" :type (:type container) :name (:name container))
(let [root-shape (ctn/get-container-root container)]
(ctf/update-container data
container
#(reduce process-shape % (ctn/get-direct-children container root-shape)))))
(process-shape [container shape]
;; Look for head copies in the first level (either component roots or inside main components).
;; Even if they have been swapped, we don't add slot to them because there is no way to know
;; the original shape. Only children.
(if (and (ctk/instance-head? shape)
(ctk/in-component-copy? shape)
(nil? (ctk/get-swap-slot shape)))
(process-copy-head container shape)
(reduce process-shape container (ctn/get-direct-children container shape))))
(process-copy-head [container head-shape]
;; Process recursively all children, comparing each one with the corresponding child in the main
;; component, looking by position. If the shape-ref does not point to the found child, then it has
;; been swapped and need to set up a slot.
(l/trc :hint "Processing copy-head" :id (:id head-shape) :name (:name head-shape))
(let [component-shape (ctf/find-ref-shape file container libs head-shape :include-deleted? true :with-context? true)
component-container (:container (meta component-shape))]
(loop [container container
children (map #(ctn/get-shape container %) (:shapes head-shape))
component-children (map #(ctn/get-shape component-container %) (:shapes component-shape))]
(let [child (first children)
component-child (first component-children)]
(if (or (nil? child) (nil? component-child))
container
(let [container (if (and (not (ctk/is-main-of? component-child child))
(nil? (ctk/get-swap-slot child))
(ctk/instance-head? child))
(let [slot (guess-swap-slot component-child component-container)]
(l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot)
(ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot)))
container)]
(recur (process-copy-head container child)
(rest children)
(rest component-children))))))))
(guess-swap-slot [shape container]
;; To guess the slot, we must follow the chain until we find the definitive main. But
;; we cannot navigate by shape-ref, because main shapes may also have been swapped. So
;; chain by position, too.
(if-let [slot (ctk/get-swap-slot shape)]
slot
(if-not (ctk/in-component-copy? shape)
(:id shape)
(let [head-copy (ctn/get-component-shape (:objects container) shape)]
(if (= (:id head-copy) (:id shape))
(:id shape)
(let [head-main (ctf/find-ref-shape file
container
libs
head-copy
:include-deleted? true
:with-context? true)
container-main (:container (meta head-main))
shape-main (find-match-by-position shape
head-copy
container
head-main
container-main)]
(guess-swap-slot shape-main container-main)))))))
(find-match-by-position [shape-copy head-copy container-copy head-main container-main]
;; Find the shape in the main that has the same position under its parent than
;; the copy under its one. To get the parent we must process recursively until
;; the component head, because mains may also have been swapped.
(let [parent-copy (ctn/get-shape container-copy (:parent-id shape-copy))
parent-main (if (= (:id parent-copy) (:id head-copy))
head-main
(find-match-by-position parent-copy
head-copy
container-copy
head-main
container-main))
index (cfh/get-position-on-parent (:objects container-copy)
(:id shape-copy))
shape-main-id (dm/get-in parent-main [:shapes index])]
(ctn/get-shape container-main shape-main-id)))]
file (-> file
(update :data process-fdata))))
(defn fix-find-duplicated-slots
[file _]
;; Find the shapes whose children have duplicated slots
(let [check-duplicate-swap-slot
(fn [shape page]
(let [shapes (map #(get (:objects page) %) (:shapes shape))
slots (->> (map #(ctk/get-swap-slot %) shapes)
(remove nil?))
counts (frequencies slots)]
#_(when (some (fn [[_ count]] (> count 1)) counts)
(l/trc :info "This shape has children with the same swap slot" :id (:id shape) :file-id (str (:id file))))
(some (fn [[_ count]] (> count 1)) counts)))
count-slots-shape
(fn [page shape]
(if (ctk/instance-root? shape)
(check-duplicate-swap-slot shape page)
false))
count-slots-page
(fn [page]
(->> (:objects page)
(vals)
(mapv #(count-slots-shape page %))
(filter true?)
count))
count-slots-data
(fn [data]
(->> (:pages-index data)
(vals)
(mapv count-slots-page)
(reduce +)))
num-missing-slots (count-slots-data (:data file))]
(when (pos? num-missing-slots)
(l/trc :info (str "Shapes with children with the same swap slot: " num-missing-slots) :file-id (str (:id file))))
file))

View File

@@ -1,88 +0,0 @@
;; 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.srepl.fixes.lost-colors
"A collection of adhoc fixes scripts."
(:require
[app.binfile.common :as bfc]
[app.common.logging :as l]
[app.common.types.color :as types.color]
[app.db :as db]
[app.srepl.helpers :as h]))
(def sql:get-affected-files
"SELECT fm.file_id AS id FROM file_migration AS fm WHERE fm.name = '0008-fix-library-colors-v2'")
(def sql:get-matching-snapshot
"SELECT * 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 2")
(defn get-affected-migration
[conn file-id]
(db/get* conn :file-migration
{:name "0008-fix-library-colors-v2"
:file-id file-id}))
(defn get-last-valid-snapshot
[conn migration]
(let [[snapshot] (db/exec! conn [sql:get-matching-snapshot
(:file-id migration)
(:created-at migration)])]
(when snapshot
(let [snapshot (assoc snapshot :id (:file-id snapshot))]
(bfc/decode-file h/*system* snapshot)))))
(defn restore-color
[{:keys [data] :as snapshot} color]
(when-let [scolor (get-in data [:colors (:id color)])]
(-> (select-keys scolor types.color/library-color-attrs)
(types.color/check-library-color))))
(defn restore-missing-colors
[{:keys [id] :as file} & _opts]
(l/inf :hint "process file" :file-id (str id) :name (:name file) :has-colors (-> file :data :colors not-empty boolean))
(if-let [colors (-> file :data :colors not-empty)]
(let [migration (get-affected-migration h/*system* id)]
(if-let [snapshot (get-last-valid-snapshot h/*system* migration)]
(do
(l/inf :hint "using snapshot" :snapshot (:label snapshot))
(let [colors (reduce-kv (fn [colors color-id color]
(if-let [result (restore-color snapshot color)]
(do
(l/inf :hint "restored color" :file-id (str id) :color-id (str color-id))
(assoc colors color-id result))
(do
(l/wrn :hint "ignoring color" :file-id (str id) :color (pr-str color))
colors)))
colors
colors)
file (-> file
(update :data assoc :colors colors)
(update :migrations disj "0008-fix-library-colors-v2"))]
(db/delete! h/*system* :file-migration
{:name "0008-fix-library-colors-v2"
:file-id (:id file)})
file))
(do
(db/delete! h/*system* :file-migration
{:name "0008-fix-library-colors-v2"
:file-id (:id file)})
nil)))
(do
(db/delete! h/*system* :file-migration
{:name "0008-fix-library-colors-v2"
:file-id (:id file)})
nil)))

View File

@@ -14,9 +14,8 @@
[app.common.files.validate :as cfv] [app.common.files.validate :as cfv]
[app.common.time :as ct] [app.common.time :as ct]
[app.db :as db] [app.db :as db]
[app.main :as main] [app.features.file-snapshots :as fsnap]
[app.rpc.commands.files :as files] [app.main :as main]))
[app.rpc.commands.files-snapshot :as fsnap]))
(def ^:dynamic *system* nil) (def ^:dynamic *system* nil)
@@ -48,7 +47,7 @@
([system id] ([system id]
(db/run! system (db/run! system
(fn [system] (fn [system]
(files/get-file system id :migrate? false))))) (bfc/get-file system id :decode? false)))))
(defn update-team! (defn update-team!
[system {:keys [id] :as team}] [system {:keys [id] :as team}]
@@ -118,10 +117,10 @@
(let [conn (db/get-connection system)] (let [conn (db/get-connection system)]
(->> (get-and-lock-team-files conn team-id) (->> (get-and-lock-team-files conn team-id)
(reduce (fn [result file-id] (reduce (fn [result file-id]
(let [file (fsnap/get-file-snapshots system file-id)] (let [file (bfc/get-file system file-id :realize? true :lock-for-update? true)]
(fsnap/create-file-snapshot! system file (fsnap/create! system file
{:label label {:label label
:created-by :admin}) :created-by "admin"})
(inc result))) (inc result)))
0)))) 0))))
@@ -132,21 +131,34 @@
(into #{})) (into #{}))
snap (search-file-snapshots conn ids label) snap (search-file-snapshots conn ids label)
ids' (into #{} (map :file-id) snap)] ids' (into #{} (map :file-id) snap)]
(when (not= ids ids') (when (not= ids ids')
(throw (RuntimeException. "no uniform snapshot available"))) (throw (RuntimeException. "no uniform snapshot available")))
(reduce (fn [result {:keys [file-id id]}] (reduce (fn [result {:keys [file-id id]}]
(fsnap/restore-file-snapshot! system file-id id) (fsnap/restore! system file-id id)
(inc result)) (inc result))
0 0
snap))) snap)))
(defn mark-migrated!
"A helper that inserts an entry in the file migration table for make
file migrated for the specified migration label."
[system file-id label]
(db/insert! system :file-migration
{:file-id file-id
:name label}
{::db/return-keys false}))
(defn process-file! (defn process-file!
[system file-id update-fn & {:keys [label validate? with-libraries?] :or {validate? true} :as opts}] [system file-id update-fn
(let [file (bfc/get-file system file-id ::db/for-update true) & {:keys [::snapshot-label ::validate? ::with-libraries?]
:or {validate? true} :as opts}]
(let [file (bfc/get-file system file-id
:lock-for-update? true
:realize? true)
libs (when with-libraries? libs (when with-libraries?
(bfc/get-resolved-file-libraries system file)) (bfc/get-resolved-file-libraries system file))
@@ -162,12 +174,12 @@
(when validate? (when validate?
(cfv/validate-file-schema! file')) (cfv/validate-file-schema! file'))
(when (string? label) (when (string? snapshot-label)
(fsnap/create-file-snapshot! system file (fsnap/create! system file
{:label label {:label snapshot-label
:deleted-at (ct/in-future {:days 30}) :deleted-at (ct/in-future {:days 30})
:created-by :admin})) :created-by "admin"}))
(let [file' (update file' :revn inc)] (let [file' (update file' :revn inc)]
(bfc/update-file! system file') (bfc/update-file! system file' opts)
true)))) true))))

View File

@@ -5,7 +5,6 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.srepl.main (ns app.srepl.main
"A collection of adhoc fixes scripts."
#_:clj-kondo/ignore #_:clj-kondo/ignore
(:require (:require
[app.auth :refer [derive-password]] [app.auth :refer [derive-password]]
@@ -24,19 +23,19 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.fdata :as feat.fdata] [app.features.fdata :as fdata]
[app.features.file-snapshots :as fsnap]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as main] [app.main :as main]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.rpc.commands.auth :as auth] [app.rpc.commands.auth :as auth]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.files-snapshot :as fsnap]
[app.rpc.commands.management :as mgmt] [app.rpc.commands.management :as mgmt]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.commands.projects :as projects] [app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.srepl.fixes :as fixes]
[app.srepl.helpers :as h] [app.srepl.helpers :as h]
[app.srepl.procs.file-repair :as procs.file-repair]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[app.worker :as wrk] [app.worker :as wrk]
@@ -48,6 +47,7 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.fs :as fs] [datoteka.fs :as fs]
[promesa.exec :as px] [promesa.exec :as px]
[promesa.exec.csp :as sp]
[promesa.exec.semaphore :as ps] [promesa.exec.semaphore :as ps]
[promesa.util :as pu])) [promesa.util :as pu]))
@@ -147,25 +147,6 @@
;; FEATURES ;; FEATURES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare process-file!)
(defn enable-objects-map-feature-on-file!
[file-id & {:as opts}]
(process-file! file-id feat.fdata/enable-objects-map opts))
(defn enable-pointer-map-feature-on-file!
[file-id & {:as opts}]
(process-file! file-id feat.fdata/enable-pointer-map opts))
(defn enable-path-data-feature-on-file!
[file-id & {:as opts}]
(process-file! file-id feat.fdata/enable-path-data opts))
(defn enable-storage-features-on-file!
[file-id & {:as opts}]
(enable-objects-map-feature-on-file! file-id opts)
(enable-pointer-map-feature-on-file! file-id opts))
(defn enable-team-feature! (defn enable-team-feature!
[team-id feature & {:keys [skip-check] :or {skip-check false}}] [team-id feature & {:keys [skip-check] :or {skip-check false}}]
(when (and (not skip-check) (not (contains? cfeat/supported-features feature))) (when (and (not skip-check) (not (contains? cfeat/supported-features feature)))
@@ -339,7 +320,10 @@
collectable file-changes entry." collectable file-changes entry."
[& {:keys [file-id label]}] [& {:keys [file-id label]}]
(let [file-id (h/parse-uuid file-id)] (let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label}))) (db/tx-run! main/system
(fn [cfg]
(let [file (bfc/get-file cfg file-id :realize? true)]
(fsnap/create! cfg file {:label label :created-by "admin"}))))))
(defn restore-file-snapshot! (defn restore-file-snapshot!
[file-id & {:keys [label id]}] [file-id & {:keys [label id]}]
@@ -349,13 +333,13 @@
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(cond (cond
(uuid? snapshot-id) (uuid? snapshot-id)
(fsnap/restore-file-snapshot! system file-id snapshot-id) (fsnap/restore! system file-id snapshot-id)
(string? label) (string? label)
(->> (h/search-file-snapshots conn #{file-id} label) (->> (h/search-file-snapshots conn #{file-id} label)
(map :id) (map :id)
(first) (first)
(fsnap/restore-file-snapshot! system file-id)) (fsnap/restore! system file-id))
:else :else
(throw (ex-info "snapshot id or label should be provided" {}))))))) (throw (ex-info "snapshot id or label should be provided" {})))))))
@@ -364,9 +348,9 @@
[file-id & {:as _}] [file-id & {:as _}]
(let [file-id (h/parse-uuid file-id)] (let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system (db/tx-run! main/system
(fn [{:keys [::db/conn]}] (fn [cfg]
(->> (fsnap/get-file-snapshots conn file-id) (->> (fsnap/get-visible-snapshots cfg file-id)
(print-table [:label :id :revn :created-at])))))) (print-table [:label :id :revn :created-at :created-by]))))))
(defn take-team-snapshot! (defn take-team-snapshot!
[team-id & {:keys [label rollback?] :or {rollback? true}}] [team-id & {:keys [label rollback?] :or {rollback? true}}]
@@ -413,24 +397,19 @@
(println (sm/humanize-explain explain)) (println (sm/humanize-explain explain))
(ex/print-throwable cause)))))))) (ex/print-throwable cause))))))))
(defn repair-file!
"Repair the list of errors detected by validation."
[file-id & {:keys [rollback?] :or {rollback? true} :as opts}]
(let [system (assoc main/system ::db/rollback rollback?)
file-id (h/parse-uuid file-id)
opts (assoc opts :with-libraries? true)]
(db/tx-run! system h/process-file! file-id fixes/repair-file opts)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PROCESSING ;; PROCESSING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:get-files (defn repair-file!
"SELECT id FROM file "Repair the list of errors detected by validation."
WHERE deleted_at is NULL [file-id & {:keys [rollback?] :or {rollback? true} :as options}]
ORDER BY created_at DESC") (let [system (assoc main/system ::db/rollback rollback?)
file-id (h/parse-uuid file-id)
options (assoc options ::h/with-libraries? true)]
(db/tx-run! system h/process-file! file-id procs.file-repair/repair-file options)))
(defn process-file! (defn update-file!
"Apply a function to the file. Optionally save the changes or not. "Apply a function to the file. Optionally save the changes or not.
The function receives the decoded and migrated file data." The function receives the decoded and migrated file data."
[file-id update-fn & {:keys [rollback?] :or {rollback? true} :as opts}] [file-id update-fn & {:keys [rollback?] :or {rollback? true} :as opts}]
@@ -441,114 +420,117 @@
db/*conn* (db/get-connection system)] db/*conn* (db/get-connection system)]
(h/process-file! system file-id update-fn opts)))))) (h/process-file! system file-id update-fn opts))))))
(defn process-team-files! (defn process!
"Apply a function to each file of the specified team." [& {:keys [max-items
[team-id update-fn & {:keys [rollback? label] :or {rollback? true} :as opts}] max-jobs
(let [team-id (h/parse-uuid team-id) rollback?
opts (dissoc opts :label)] query
(db/tx-run! (assoc main/system ::db/rollback rollback?) proc-fn
(fn [{:keys [::db/conn] :as system}] buffer]
(when (string? label) :or {max-items Long/MAX_VALUE
(h/take-team-snapshot! system team-id label)) rollback? true
max-jobs 1
buffer 128}
:as opts}]
(binding [h/*system* system (l/inf :hint "process start"
db/*conn* (db/get-connection system)]
(->> (h/get-and-lock-team-files conn team-id)
(reduce (fn [result file-id]
(if (h/process-file! system file-id update-fn opts)
(inc result)
result))
0)))))))
(defn process-files!
"Apply a function to all files in the database"
[update-fn & {:keys [max-items
max-jobs
rollback?
query]
:or {max-jobs 1
max-items Long/MAX_VALUE
rollback? true
query sql:get-files}
:as opts}]
(l/dbg :hint "process:start"
:rollback rollback? :rollback rollback?
:max-jobs max-jobs :max-jobs max-jobs
:max-items max-items) :max-items max-items)
(let [tpoint (ct/tpoint) (let [tpoint (ct/tpoint)
factory (px/thread-factory :virtual false :prefix "penpot/file-process/") max-jobs (or max-jobs (px/get-available-processors))
executor (px/cached-executor :factory factory) query (or query
sjobs (ps/create :permits max-jobs) (:query (meta proc-fn))
(throw (ex-info "missing query" {})))
query (if (vector? query) query [query])
process-file proc-fn (if (var? proc-fn)
(fn [file-id idx tpoint] (deref proc-fn)
(let [thread-id (px/get-thread-id)] proc-fn)
(try
(l/trc :hint "process:file:start"
:tid thread-id
:file-id (str file-id)
:index idx)
(let [system (assoc main/system ::db/rollback rollback?)]
(db/tx-run! system (fn [system]
(binding [h/*system* system
db/*conn* (db/get-connection system)]
(h/process-file! system file-id update-fn opts)))))
(catch Throwable cause in-ch (sp/chan :buf buffer)
(l/wrn :hint "unexpected error on processing file (skiping)"
:tid thread-id
:file-id (str file-id)
:index idx
:cause cause))
(finally
(when-let [pause (:pause opts)]
(Thread/sleep (int pause)))
(ps/release! sjobs) worker-fn
(let [elapsed (ct/format-duration (tpoint))] (fn [worker-id]
(l/trc :hint "process:file:end" (l/dbg :hint "worker started"
:tid thread-id :id worker-id)
:file-id (str file-id)
:index idx
:elapsed elapsed))))))
process-file* (loop []
(fn [idx file-id] (when-let [[index item] (sp/<! in-ch)]
(ps/acquire! sjobs) (l/dbg :hint "process item" :worker-id worker-id :index index :item item)
(px/run! executor (partial process-file file-id idx (ct/tpoint))) (try
(inc idx)) (-> main/system
(assoc ::db/rollback rollback?)
(db/tx-run! (fn [system]
(binding [h/*system* system
db/*conn* (db/get-connection system)]
(proc-fn system item opts)))))
process-files (catch Throwable cause
(l/wrn :hint "unexpected error on processing item (skiping)"
:worker-id worker-id
:item item
:cause cause))
(finally
(when-let [pause (:pause opts)]
(Thread/sleep (int pause)))))
(recur)))
(l/dbg :hint "worker stoped"
:id worker-id))
enqueue-item
(fn [index row]
(sp/>! in-ch [index (into {} row)])
(inc index))
process-items
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(db/exec! conn ["SET statement_timeout = 0"]) (db/exec! conn ["SET statement_timeout = 0"])
(db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
(try (->> (db/plan conn query {:fetch-size (* max-jobs 3)})
(->> (db/plan conn [query]) (transduce (take max-items)
(transduce (comp (completing enqueue-item)
(take max-items) 0))
(map :id)) (sp/close! in-ch))
(completing process-file*)
0)) threads
(finally (->> (range max-jobs)
;; Close and await tasks (map (fn [idx]
(pu/close! executor))))] (px/fn->thread (partial worker-fn idx)
:name (str "pentpot/process/" idx))))
(doall))]
(try (try
(db/tx-run! main/system process-files) (db/tx-run! main/system process-items)
;; Await threads termination
(doseq [thread threads]
(px/await! thread))
(catch Throwable cause (catch Throwable cause
(l/dbg :hint "process:error" :cause cause)) (l/dbg :hint "process:error" :cause cause))
(finally (finally
(let [elapsed (ct/format-duration (tpoint))] (let [elapsed (ct/format-duration (tpoint))]
(l/dbg :hint "process:end" (l/inf :hint "process end"
:rollback rollback? :rollback rollback?
:elapsed elapsed)))))) :elapsed elapsed))))))
(defn process-file!
"A specialized, file specific process! alternative"
[& {:keys [id] :as opts}]
(let [id (h/parse-uuid id)]
(-> opts
(assoc :query ["select id from file where id = ?" id])
(assoc :max-items 1)
(assoc :max-jobs 1)
(process!))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT) ;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -579,25 +561,34 @@
(db/update! conn :file (db/update! conn :file
{:deleted-at nil {:deleted-at nil
:has-media-trimmed false} :has-media-trimmed false}
{:id file-id}) {:id file-id}
{::db/return-keys false})
;; Fragments are not handled here because they
;; use the database cascade operation and they
;; are not marked for deletion with objects-gc
;; task
(db/update! conn :file-media-object (db/update! conn :file-media-object
{:deleted-at nil} {:deleted-at nil}
{:file-id file-id}) {:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-change
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-data
{:deleted-at nil}
{:file-id file-id}
{::db/return-keys false})
;; Mark thumbnails to be deleted ;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail (db/update! conn :file-thumbnail
{:deleted-at nil} {:deleted-at nil}
{:file-id file-id}) {:file-id file-id}
{::db/return-keys false})
(db/update! conn :file-tagged-object-thumbnail (db/update! conn :file-tagged-object-thumbnail
{:deleted-at nil} {:deleted-at nil}
{:file-id file-id}) {:file-id file-id}
{::db/return-keys false})
:restored) :restored)
@@ -607,11 +598,10 @@
(let [file-id (h/parse-uuid file-id)] (let [file-id (h/parse-uuid file-id)]
(db/tx-run! main/system (db/tx-run! main/system
(fn [system] (fn [system]
(when-let [file (some-> (db/get* system :file (when-let [file (db/get* system :file
{:id file-id} {:id file-id}
{::db/remove-deleted false {::db/remove-deleted false
::sql/columns [:id :name]}) ::sql/columns [:id :name]})]
(files/decode-row))]
(audit/insert! system (audit/insert! system
{::audit/name "restore-file" {::audit/name "restore-file"
::audit/type "action" ::audit/type "action"

View File

@@ -0,0 +1,141 @@
;; 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.srepl.procs.fdata-storage
(:require
[app.common.logging :as l]
[app.db :as db]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SNAPSHOTS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:get-unmigrated-snapshots
"SELECT fc.id, fc.file_id
FROM file_change AS fc
WHERE fc.data IS NOT NULL
AND fc.label IS NOT NULL
ORDER BY fc.id ASC")
(def sql:get-migrated-snapshots
"SELECT f.id, f.file_id
FROM file_data AS f
WHERE f.data IS NOT NULL
AND f.type = 'snapshot'
AND f.id != f.file_id
ORDER BY f.id ASC")
(defn migrate-snapshot-to-storage
"Migrate the current existing files to store data in new storage
tables."
{:query sql:get-unmigrated-snapshots}
[{:keys [::db/conn]} {:keys [id file-id]} & {:as options}]
(let [{:keys [id file-id data created-at updated-at]}
(db/get* conn :file-change {:id id :file-id file-id}
::db/for-update true
::db/remove-deleted false)]
(when data
(l/inf :hint "migrating snapshot" :file-id (str file-id) :id (str id))
(db/update! conn :file-change
{:data nil}
{:id id :file-id file-id}
{::db/return-keys false})
(db/insert! conn :file-data
{:backend "db"
:metadata nil
:type "snapshot"
:data data
:created-at created-at
:modified-at updated-at
:file-id file-id
:id id}
{::db/return-keys false}))))
(defn rollback-snapshot-from-storage
"Migrate back to the file table storage."
{:query sql:get-unmigrated-snapshots}
[{:keys [::db/conn]} {:keys [id file-id]} & {:as opts}]
(when-let [{:keys [id file-id data]}
(db/get* conn :file-data {:id id :file-id file-id :type "snapshot"}
::db/for-update true
::db/remove-deleted false)]
(l/inf :hint "rollback snapshot" :file-id (str file-id) :id (str id))
(db/update! conn :file-change
{:data data}
{:id id :file-id file-id}
{::db/return-keys false})
(db/delete! conn :file-data
{:id id :file-id file-id :type "snapshot"}
{::db/return-keys false})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def sql:get-unmigrated-files
"SELECT f.id
FROM file AS f
WHERE f.data IS NOT NULL
ORDER BY f.modified_at ASC")
(def sql:get-migrated-files
"SELECT f.id, f.file_id
FROM file_data AS f
WHERE f.data IS NOT NULL
AND f.id = f.file_id
ORDER BY f.id ASC")
(defn migrate-file-to-storage
"Migrate the current existing files to store data in new storage
tables."
{:query sql:get-unmigrated-files}
[{:keys [::db/conn] :as cfg} {:keys [id]} & {:as opts}]
(let [{:keys [id data created-at modified-at]}
(db/get* conn :file {:id id}
::db/for-update true
::db/remove-deleted false)]
(when data
(l/inf :hint "migrating file" :file-id (str id))
(db/update! conn :file {:data nil} {:id id} ::db/return-keys false)
(db/insert! conn :file-data
{:backend "db"
:metadata nil
:type "main"
:data data
:created-at created-at
:modified-at modified-at
:file-id id
:id id}
{::db/return-keys false}))
(let [snapshots-sql
(str "WITH snapshots AS (" sql:get-unmigrated-snapshots ") "
"SELECT s.* FROM snapshots AS s WHERE s.file_id = ?")]
(run! (fn [params]
(migrate-snapshot-to-storage cfg params opts))
(db/plan cfg [snapshots-sql id])))))
(defn rollback-file-from-storage
"Migrate back to the file table storage."
{:query sql:get-migrated-files}
[{:keys [::db/conn] :as cfg} {:keys [id]} & {:as opts}]
(when-let [{:keys [id data]}
(db/get* conn :file-data {:id id :file-id id :type "main"}
::db/for-update true
::db/remove-deleted false)]
(l/inf :hint "rollback file" :file-id (str id))
(db/update! conn :file {:data data} {:id id} ::db/return-keys false)
(db/delete! conn :file-data {:file-id id :id id :type "main"} ::db/return-keys false)
(let [snapshots-sql
(str "WITH snapshots AS (" sql:get-migrated-snapshots ") "
"SELECT s.* FROM snapshots AS s WHERE s.file_id = ?")]
(run! (fn [params]
(rollback-snapshot-from-storage cfg params opts))
(db/plan cfg [snapshots-sql id])))))

View File

@@ -0,0 +1,60 @@
;; 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.srepl.procs.file-repair
(:require
[app.common.files.changes :as cfc]
[app.common.files.repair :as cfr]
[app.common.files.validate :as cfv]
[app.common.logging :as l]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GENERAL PURPOSE REPAIR
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn repair-file
"Internal helper for validate and repair the file. The operation is
applied multiple times untile file is fixed or max iteration counter
is reached (default 10).
This function should not be used directly, it is used throught the
app.srepl.main/repair-file! helper. In practical terms this function
is private and implementation detail."
[file libs & {:keys [max-iterations] :or {max-iterations 10}}]
(let [validate-and-repair
(fn [file libs iteration]
(when-let [errors (not-empty (cfv/validate-file file libs))]
(l/trc :hint "repairing file"
:file-id (str (:id file))
:iteration iteration
:errors (count errors))
(let [changes (cfr/repair-file file libs errors)]
(-> file
(update :revn inc)
(update :data cfc/process-changes changes)))))
process-file
(fn [file libs]
(loop [file file
iteration 0]
(if (< iteration max-iterations)
(if-let [file (validate-and-repair file libs iteration)]
(recur file (inc iteration))
file)
(do
(l/wrn :hint "max retry num reached on repairing file"
:file-id (str (:id file))
:iteration iteration)
file))))
file'
(process-file file libs)]
(when (not= (:revn file) (:revn file'))
(l/trc :hint "file repaired" :file-id (str (:id file))))
file'))

View File

@@ -4,10 +4,11 @@
;; ;;
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.srepl.fixes.media-refs (ns app.srepl.procs.media-refs
(:require (:require
[app.binfile.common :as bfc] [app.binfile.common :as bfc]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.logging :as l]
[app.srepl.helpers :as h])) [app.srepl.helpers :as h]))
(defn- collect-media-refs (defn- collect-media-refs
@@ -37,7 +38,22 @@
(let [media-refs (collect-media-refs (:data file))] (let [media-refs (collect-media-refs (:data file))]
(bfc/update-media-references! cfg file media-refs))) (bfc/update-media-references! cfg file media-refs)))
(defn process-file (def ^:private sql:get-files
[file _opts] "SELECT f.id
(let [system (h/get-current-system)] FROM file AS f
(update-all-media-references system file))) LEFT JOIN file_migration AS fm ON (fm.file_id = f.id AND fm.name = 'internal/procs/media-refs')
WHERE fm.name IS NULL
ORDER BY f.project_id")
(defn fix-media-refs
{:query sql:get-files}
[cfg {:keys [id]} & {:as options}]
(l/inf :hint "processing file" :id (str id))
(h/process-file! cfg id
(fn [file _opts]
(update-all-media-references cfg file))
(assoc options
::bfc/reset-migrations? true
::h/validate? false))
(h/mark-migrated! cfg id "internal/procs/media-refs"))

View File

@@ -0,0 +1,57 @@
;; 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.srepl.procs.path-data
(:require
[app.binfile.common :as bfc]
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.logging :as l]
[app.srepl.helpers :as h]))
(def ^:private sql:get-files-with-path-data
"SELECT id FROM file WHERE features @> '{fdata/path-data}'")
(defn disable
"A script responsible for remove the path data type from file data and
allow file to be open in older penpot versions.
Should be used only in cases when you want to downgrade to an older
penpot version for some reason."
{:query sql:get-files-with-path-data}
[cfg {:keys [id]} & {:as options}]
(l/inf :hint "disabling path-data" :file-id (str id))
(let [update-object
(fn [object]
(if (or (cfh/path-shape? object)
(cfh/bool-shape? object))
(update object :content vec)
object))
update-container
(fn [container]
(d/update-when container :objects d/update-vals update-object))
update-file
(fn [file & _opts]
(-> file
(update :data (fn [data]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(update :features disj "fdata/path-data")
(update :migrations disj
"0003-convert-path-content-v2"
"0003-convert-path-content")))
options
(-> options
(assoc ::bfc/reset-migrations? true)
(assoc ::h/validate? false))]
(h/process-file! cfg id update-file options)))

View File

@@ -115,13 +115,10 @@
(defn- create-database-object (defn- create-database-object
[{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}] [{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}]
(let [id (or (:id params) (uuid/random)) (let [id (or (::id params) (uuid/random))
mdata (cond-> (get-metadata params) mdata (cond-> (get-metadata params)
(satisfies? impl/IContentHash content) (satisfies? impl/IContentHash content)
(assoc :hash (impl/get-hash content)) (assoc :hash (impl/get-hash content)))
:always
(dissoc :id))
touched-at (if touch touched-at (if touch
(or touched-at (ct/now)) (or touched-at (ct/now))

View File

@@ -34,7 +34,7 @@
(SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE ttf_file_id = ?))) AS has_refs") (SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE ttf_file_id = ?))) AS has_refs")
(defn- has-team-font-variant-refs? (defn- has-team-font-variant-refs?
[conn id] [conn {:keys [id]}]
(-> (db/exec-one! conn [sql:has-team-font-variant-refs id id id id]) (-> (db/exec-one! conn [sql:has-team-font-variant-refs id id id id])
(get :has-refs))) (get :has-refs)))
@@ -44,7 +44,7 @@
(SELECT EXISTS (SELECT 1 FROM file_media_object WHERE thumbnail_id = ?))) AS has_refs") (SELECT EXISTS (SELECT 1 FROM file_media_object WHERE thumbnail_id = ?))) AS has_refs")
(defn- has-file-media-object-refs? (defn- has-file-media-object-refs?
[conn id] [conn {:keys [id]}]
(-> (db/exec-one! conn [sql:has-file-media-object-refs id id]) (-> (db/exec-one! conn [sql:has-file-media-object-refs id id])
(get :has-refs))) (get :has-refs)))
@@ -53,7 +53,7 @@
(SELECT EXISTS (SELECT 1 FROM team WHERE photo_id = ?))) AS has_refs") (SELECT EXISTS (SELECT 1 FROM team WHERE photo_id = ?))) AS has_refs")
(defn- has-profile-refs? (defn- has-profile-refs?
[conn id] [conn {:keys [id]}]
(-> (db/exec-one! conn [sql:has-profile-refs id id]) (-> (db/exec-one! conn [sql:has-profile-refs id id])
(get :has-refs))) (get :has-refs)))
@@ -62,7 +62,7 @@
"SELECT EXISTS (SELECT 1 FROM file_tagged_object_thumbnail WHERE media_id = ?) AS has_refs") "SELECT EXISTS (SELECT 1 FROM file_tagged_object_thumbnail WHERE media_id = ?) AS has_refs")
(defn- has-file-object-thumbnails-refs? (defn- has-file-object-thumbnails-refs?
[conn id] [conn {:keys [id]}]
(-> (db/exec-one! conn [sql:has-file-object-thumbnail-refs id]) (-> (db/exec-one! conn [sql:has-file-object-thumbnail-refs id])
(get :has-refs))) (get :has-refs)))
@@ -71,36 +71,23 @@
"SELECT EXISTS (SELECT 1 FROM file_thumbnail WHERE media_id = ?) AS has_refs") "SELECT EXISTS (SELECT 1 FROM file_thumbnail WHERE media_id = ?) AS has_refs")
(defn- has-file-thumbnails-refs? (defn- has-file-thumbnails-refs?
[conn id] [conn {:keys [id]}]
(-> (db/exec-one! conn [sql:has-file-thumbnail-refs id]) (-> (db/exec-one! conn [sql:has-file-thumbnail-refs id])
(get :has-refs))) (get :has-refs)))
(def ^:private (def sql:exists-file-data-refs
sql:has-file-data-refs "SELECT EXISTS (
"SELECT EXISTS (SELECT 1 FROM file WHERE data_ref_id = ?) AS has_refs") SELECT 1 FROM file_data
WHERE file_id = ?
AND id = ?
AND metadata->>'storage-ref-id' = ?::text
) AS has_refs")
(defn- has-file-data-refs? (defn- has-file-data-refs?
[conn id] [conn sobject]
(-> (db/exec-one! conn [sql:has-file-data-refs id]) (let [{:keys [file-id id]} (:metadata sobject)]
(get :has-refs))) (-> (db/exec-one! conn [sql:exists-file-data-refs file-id id (:id sobject)])
(get :has-refs))))
(def ^:private
sql:has-file-data-fragment-refs
"SELECT EXISTS (SELECT 1 FROM file_data_fragment WHERE data_ref_id = ?) AS has_refs")
(defn- has-file-data-fragment-refs?
[conn id]
(-> (db/exec-one! conn [sql:has-file-data-fragment-refs id])
(get :has-refs)))
(def ^:private
sql:has-file-change-refs
"SELECT EXISTS (SELECT 1 FROM file_change WHERE data_ref_id = ?) AS has_refs")
(defn- has-file-change-refs?
[conn id]
(-> (db/exec-one! conn [sql:has-file-change-refs id])
(get :has-refs)))
(def ^:private sql:mark-freeze-in-bulk (def ^:private sql:mark-freeze-in-bulk
"UPDATE storage_object "UPDATE storage_object
@@ -143,52 +130,48 @@
"file-media-object")) "file-media-object"))
(defn- process-objects! (defn- process-objects!
[conn has-refs? ids bucket] [conn has-refs? bucket objects]
(loop [to-freeze #{} (loop [to-freeze #{}
to-delete #{} to-delete #{}
ids (seq ids)] objects (seq objects)]
(if-let [id (first ids)] (if-let [{:keys [id] :as object} (first objects)]
(if (has-refs? conn id) (if (has-refs? conn object)
(do (do
(l/debug :hint "processing object" (l/debug :id (str id)
:id (str id)
:status "freeze" :status "freeze"
:bucket bucket) :bucket bucket)
(recur (conj to-freeze id) to-delete (rest ids))) (recur (conj to-freeze id) to-delete (rest objects)))
(do (do
(l/debug :hint "processing object" (l/debug :id (str id)
:id (str id)
:status "delete" :status "delete"
:bucket bucket) :bucket bucket)
(recur to-freeze (conj to-delete id) (rest ids)))) (recur to-freeze (conj to-delete id) (rest objects))))
(do (do
(some->> (seq to-freeze) (mark-freeze-in-bulk! conn)) (some->> (seq to-freeze) (mark-freeze-in-bulk! conn))
(some->> (seq to-delete) (mark-delete-in-bulk! conn)) (some->> (seq to-delete) (mark-delete-in-bulk! conn))
[(count to-freeze) (count to-delete)])))) [(count to-freeze) (count to-delete)]))))
(defn- process-bucket! (defn- process-bucket!
[conn bucket ids] [conn bucket objects]
(case bucket (case bucket
"file-media-object" (process-objects! conn has-file-media-object-refs? ids bucket) "file-media-object" (process-objects! conn has-file-media-object-refs? bucket objects)
"team-font-variant" (process-objects! conn has-team-font-variant-refs? ids bucket) "team-font-variant" (process-objects! conn has-team-font-variant-refs? bucket objects)
"file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? ids bucket) "file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? bucket objects)
"file-thumbnail" (process-objects! conn has-file-thumbnails-refs? ids bucket) "file-thumbnail" (process-objects! conn has-file-thumbnails-refs? bucket objects)
"profile" (process-objects! conn has-profile-refs? ids bucket) "profile" (process-objects! conn has-profile-refs? bucket objects)
"file-data" (process-objects! conn has-file-data-refs? ids bucket) "file-data" (process-objects! conn has-file-data-refs? bucket objects)
"file-data-fragment" (process-objects! conn has-file-data-fragment-refs? ids bucket)
"file-change" (process-objects! conn has-file-change-refs? ids bucket)
(ex/raise :type :internal (ex/raise :type :internal
:code :unexpected-unknown-reference :code :unexpected-unknown-reference
:hint (dm/fmt "unknown reference '%'" bucket)))) :hint (dm/fmt "unknown reference '%'" bucket))))
(defn process-chunk! (defn process-chunk!
[{:keys [::db/conn]} chunk] [{:keys [::db/conn]} chunk]
(reduce-kv (fn [[nfo ndo] bucket ids] (reduce-kv (fn [[nfo ndo] bucket objects]
(let [[nfo' ndo'] (process-bucket! conn bucket ids)] (let [[nfo' ndo'] (process-bucket! conn bucket objects)]
[(+ nfo nfo') [(+ nfo nfo')
(+ ndo ndo')])) (+ ndo ndo')]))
[0 0] [0 0]
(d/group-by lookup-bucket :id #{} chunk))) (d/group-by lookup-bucket identity #{} chunk)))
(def ^:private (def ^:private
sql:get-touched-storage-objects sql:get-touched-storage-objects
@@ -214,12 +197,7 @@
(let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)] (let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)]
(recur (long (+ freezed nfo)) (recur (long (+ freezed nfo))
(long (+ deleted ndo)))) (long (+ deleted ndo))))
(do {:freeze freezed :delete deleted})))
(l/inf :hint "task finished"
:to-freeze freezed
:to-delete deleted)
{:freeze freezed :delete deleted}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLER ;; HANDLER

View File

@@ -10,6 +10,7 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.common.time :as ct] [app.common.time :as ct]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -19,10 +20,28 @@
(defmulti delete-object (defmulti delete-object
(fn [_ props] (:object props))) (fn [_ props] (:object props)))
(defmethod delete-object :snapshot
[{:keys [::db/conn] :as cfg} {:keys [id file-id deleted-at]}]
(l/trc :obj "snapshot" :id (str id) :file-id (str file-id)
:deleted-at (ct/format-inst deleted-at))
(db/update! conn :file-change
{:deleted-at deleted-at}
{:id id :file-id file-id}
{::db/return-keys false})
(db/update! conn :file-data
{:deleted-at deleted-at}
{:id id :file-id file-id :type "snapshot"}
{::db/return-keys false}))
(defmethod delete-object :file (defmethod delete-object :file
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})] (when-let [file (db/get* conn :file {:id id}
(l/trc :hint "marking for deletion" :rel "file" :id (str id) {::db/remove-deleted false
::sql/columns [:id :is-shared]})]
(l/trc :obj "file" :id (str id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :file (db/update! conn :file
@@ -43,25 +62,35 @@
;; Mark file change to be deleted ;; Mark file change to be deleted
(db/update! conn :file-change (db/update! conn :file-change
{:deleted-at deleted-at} {:deleted-at deleted-at}
{:file-id id}) {:file-id id}
{::db/return-keys false})
;; Mark file data fragment to be deleted
(db/update! conn :file-data
{:deleted-at deleted-at}
{:file-id id}
{::db/return-keys false})
;; Mark file media objects to be deleted ;; Mark file media objects to be deleted
(db/update! conn :file-media-object (db/update! conn :file-media-object
{:deleted-at deleted-at} {:deleted-at deleted-at}
{:file-id id}) {:file-id id}
{::db/return-keys false})
;; Mark thumbnails to be deleted ;; Mark thumbnails to be deleted
(db/update! conn :file-thumbnail (db/update! conn :file-thumbnail
{:deleted-at deleted-at} {:deleted-at deleted-at}
{:file-id id}) {:file-id id}
{::db/return-keys false})
(db/update! conn :file-tagged-object-thumbnail (db/update! conn :file-tagged-object-thumbnail
{:deleted-at deleted-at} {:deleted-at deleted-at}
{:file-id id}))) {:file-id id}
{::db/return-keys false})))
(defmethod delete-object :project (defmethod delete-object :project
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "project" :id (str id) (l/trc :obj "project" :id (str id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :project (db/update! conn :project
@@ -78,7 +107,7 @@
(defmethod delete-object :team (defmethod delete-object :team
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "team" :id (str id) (l/trc :obj "team" :id (str id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :team (db/update! conn :team
{:deleted-at deleted-at} {:deleted-at deleted-at}
@@ -100,7 +129,7 @@
(defmethod delete-object :profile (defmethod delete-object :profile
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
(l/trc :hint "marking for deletion" :rel "profile" :id (str id) (l/trc :obj "profile" :id (str id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :profile (db/update! conn :profile
@@ -115,7 +144,7 @@
(defmethod delete-object :default (defmethod delete-object :default
[_cfg props] [_cfg props]
(l/wrn :hint "not implementation found" :rel (:object props))) (l/wrn :obj (:object props) :hint "not implementation found"))
(defmethod ig/assert-key ::handler (defmethod ig/assert-key ::handler
[_ params] [_ params]

View File

@@ -23,29 +23,16 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.features.fdata :as feat.fdata] [app.features.fdata :as feat.fdata]
[app.features.file-snapshots :as fsnap]
[app.storage :as sto] [app.storage :as sto]
[app.worker :as wrk] [app.worker :as wrk]
[integrant.core :as ig])) [integrant.core :as ig]))
(declare get-file) (declare get-file)
(def sql:get-snapshots
"SELECT fc.file_id AS id,
fc.id AS snapshot_id,
fc.data,
fc.revn,
fc.version,
fc.features,
fc.data_backend,
fc.data_ref_id
FROM file_change AS fc
WHERE fc.file_id = ?
AND fc.data IS NOT NULL
ORDER BY fc.created_at ASC")
(def ^:private sql:mark-file-media-object-deleted (def ^:private sql:mark-file-media-object-deleted
"UPDATE file_media_object "UPDATE file_media_object
SET deleted_at = now() SET deleted_at = ?
WHERE file_id = ? AND id != ALL(?::uuid[]) WHERE file_id = ? AND id != ALL(?::uuid[])
RETURNING id") RETURNING id")
@@ -56,37 +43,35 @@
(defn- clean-file-media! (defn- clean-file-media!
"Performs the garbage collection of file media objects." "Performs the garbage collection of file media objects."
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}] [{:keys [::db/conn ::timestamp] :as cfg} {:keys [id] :as file}]
(let [xform (comp (let [used-media
(map (partial bfc/decode-file cfg)) (fsnap/reduce-snapshots cfg id xf:collect-used-media conj #{})
xf:collect-used-media)
used (->> (db/plan conn [sql:get-snapshots id] {:fetch-size 1}) used-media
(transduce xform conj #{})) (into used-media xf:collect-used-media [file])
used (into used xf:collect-used-media [file])
ids (db/create-array conn "uuid" used) used-media
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids]) (db/create-array conn "uuid" used-media)
(into #{} (map :id)))]
(l/dbg :hint "clean" :rel "file-media-object" :file-id (str id) :total (count unused)) unused-media
(->> (db/exec! conn [sql:mark-file-media-object-deleted timestamp id used-media])
(into #{} (map :id)))]
(doseq [id unused] (doseq [id unused-media]
(l/trc :hint "mark deleted" (l/trc :obj "media-object"
:rel "file-media-object" :file-id (str id)
:id (str id) :id (str id)))
:file-id (str id)))
file)) file))
(def ^:private sql:mark-file-object-thumbnails-deleted (def ^:private sql:mark-file-object-thumbnails-deleted
"UPDATE file_tagged_object_thumbnail "UPDATE file_tagged_object_thumbnail
SET deleted_at = now() SET deleted_at = ?
WHERE file_id = ? AND object_id != ALL(?::text[]) WHERE file_id = ? AND object_id != ALL(?::text[])
RETURNING object_id") RETURNING object_id")
(defn- clean-file-object-thumbnails! (defn- clean-file-object-thumbnails!
[{:keys [::db/conn]} {:keys [data] :as file}] [{:keys [::db/conn ::timestamp]} {:keys [data] :as file}]
(let [file-id (:id file) (let [file-id (:id file)
using (->> (vals (:pages-index data)) using (->> (vals (:pages-index data))
(into #{} (comp (into #{} (comp
@@ -98,49 +83,37 @@
(thc/fmt-object-id file-id page-id id "frame") (thc/fmt-object-id file-id page-id id "frame")
(thc/fmt-object-id file-id page-id id "component"))))))) (thc/fmt-object-id file-id page-id id "component")))))))
ids (db/create-array conn "text" using) ids (into-array String using)
unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted file-id ids]) unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted timestamp file-id ids])
(into #{} (map :object-id)))] (into #{} (map :object-id)))]
(l/dbg :hint "clean" :rel "file-object-thumbnail" :file-id (str file-id) :total (count unused))
(doseq [object-id unused] (doseq [object-id unused]
(l/trc :hint "mark deleted" (l/trc :obj "object-thumbnail"
:rel "file-tagged-object-thumbnail" :file-id (str file-id)
:object-id object-id :id object-id))
:file-id (str file-id)))
file)) file))
(def ^:private sql:mark-file-thumbnails-deleted (def ^:private sql:mark-file-thumbnails-deleted
"UPDATE file_thumbnail "UPDATE file_thumbnail
SET deleted_at = now() SET deleted_at = ?
WHERE file_id = ? AND revn < ? WHERE file_id = ? AND revn < ?
RETURNING revn") RETURNING revn")
(defn- clean-file-thumbnails! (defn- clean-file-thumbnails!
[{:keys [::db/conn]} {:keys [id revn] :as file}] [{:keys [::db/conn ::timestamp]} {:keys [id revn] :as file}]
(let [unused (->> (db/exec! conn [sql:mark-file-thumbnails-deleted id revn]) (let [unused (->> (db/exec! conn [sql:mark-file-thumbnails-deleted timestamp id revn])
(into #{} (map :revn)))] (into #{} (map :revn)))]
(l/dbg :hint "clean" :rel "file-thumbnail" :file-id (str id) :total (count unused))
(doseq [revn unused] (doseq [revn unused]
(l/trc :hint "mark deleted" (l/trc :obj "thumbnail"
:rel "file-thumbnail" :file-id (str id)
:revn revn :revn revn))
:file-id (str id)))
file)) file))
(def ^:private sql:get-files-for-library (def ^:private sql:get-files-for-library
"SELECT f.id, "SELECT f.id
f.data,
f.modified_at,
f.features,
f.version,
f.data_backend,
f.data_ref_id
FROM file AS f FROM file AS f
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id) LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
WHERE fl.library_file_id = ? WHERE fl.library_file_id = ?
@@ -161,15 +134,21 @@
deleted-components deleted-components
(ctkl/deleted-components-seq data) (ctkl/deleted-components-seq data)
xform file-xform
(mapcat (partial get-used-components deleted-components file-id)) (mapcat (partial get-used-components deleted-components file-id))
library-xform
(comp
(map :id)
(map #(bfc/get-file cfg % :realize? true :read-only? true))
file-xform)
used-remote used-remote
(->> (db/plan conn [sql:get-files-for-library file-id] {:fetch-size 1}) (->> (db/plan conn [sql:get-files-for-library file-id] {:fetch-size 1})
(transduce (comp (map (partial bfc/decode-file cfg)) xform) conj #{})) (transduce library-xform conj #{}))
used-local used-local
(into #{} xform [file]) (into #{} file-xform [file])
unused unused
(transduce bfc/xf-map-id disj (transduce bfc/xf-map-id disj
@@ -180,21 +159,21 @@
(update file :data (update file :data
(fn [data] (fn [data]
(reduce (fn [data id] (reduce (fn [data id]
(l/trc :hint "delete component" (l/trc :obj "component"
:component-id (str id) :file-id (str file-id)
:file-id (str file-id)) :id (str id))
(ctkl/delete-component data id)) (ctkl/delete-component data id))
data data
unused)))] unused)))]
(l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused))
file)) file))
(def ^:private sql:mark-deleted-data-fragments (def ^:private sql:mark-deleted-data-fragments
"UPDATE file_data_fragment "UPDATE file_data
SET deleted_at = now() SET deleted_at = ?
WHERE file_id = ? WHERE file_id = ?
AND id != ALL(?::uuid[]) AND id != ALL(?::uuid[])
AND type = 'fragment'
AND deleted_at IS NULL AND deleted_at IS NULL
RETURNING id") RETURNING id")
@@ -203,19 +182,16 @@
(mapcat feat.fdata/get-used-pointer-ids))) (mapcat feat.fdata/get-used-pointer-ids)))
(defn- clean-fragments! (defn- clean-fragments!
[{:keys [::db/conn]} {:keys [id] :as file}] [{:keys [::db/conn ::timestamp]} {:keys [id] :as file}]
(let [used (into #{} xf:collect-pointers [file]) (let [used (into #{} xf:collect-pointers [file])
unused (->> (db/exec! conn [sql:mark-deleted-data-fragments timestamp id
unused (->> (db/exec! conn [sql:mark-deleted-data-fragments id
(db/create-array conn "uuid" used)]) (db/create-array conn "uuid" used)])
(into #{} bfc/xf-map-id))] (into #{} bfc/xf-map-id))]
(l/dbg :hint "clean" :rel "file-data-fragment" :file-id (str id) :total (count unused))
(doseq [id unused] (doseq [id unused]
(l/trc :hint "mark deleted" (l/trc :obj "fragment"
:rel "file-data-fragment" :file-id (str id)
:id (str id) :id (str id)))
:file-id (str id)))
file)) file))
@@ -229,36 +205,23 @@
(cfv/validate-file-schema! file) (cfv/validate-file-schema! file)
file)) file))
(def ^:private sql:get-file
"SELECT f.id,
f.data,
f.revn,
f.version,
f.features,
f.modified_at,
f.data_backend,
f.data_ref_id
FROM file AS f
WHERE f.has_media_trimmed IS false
AND f.modified_at < now() - ?::interval
AND f.deleted_at IS NULL
AND f.id = ?
FOR UPDATE
SKIP LOCKED")
(defn get-file (defn get-file
[{:keys [::db/conn ::min-age]} file-id] [cfg {:keys [file-id revn]}]
(let [min-age (if min-age (let [file (bfc/get-file cfg file-id
(db/interval min-age) :realize? true
(db/interval 0))] :skip-locked? true
(->> (db/exec! conn [sql:get-file min-age file-id]) :lock-for-update? true)]
(first))))
;; We should ensure that the scheduled file and the procesing file
;; has not changed since schedule, for this reason we check the
;; revn from props with the revn from retrieved file from database
(when (or (nil? revn) (= revn (:revn file)))
file)))
(defn- process-file! (defn- process-file!
[cfg file-id] [cfg {:keys [file-id] :as props}]
(if-let [file (get-file cfg file-id)] (if-let [file (get-file cfg props)]
(let [file (->> file (let [file (->> file
(bfc/decode-file cfg)
(bfl/clean-file) (bfl/clean-file)
(clean-media! cfg) (clean-media! cfg)
(clean-fragments! cfg)) (clean-fragments! cfg))
@@ -267,7 +230,7 @@
true) true)
(do (do
(l/dbg :hint "skip" :file-id (str file-id)) (l/dbg :hint "skip cleaning, criteria does not match" :file-id (str file-id))
false))) false)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -282,26 +245,23 @@
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ cfg] [_ cfg]
(fn [{:keys [props] :as task}] (fn [{:keys [props] :as task}]
(let [min-age (ct/duration (or (:min-age props) (try
(cf/get-deletion-delay))) (-> cfg
file-id (get props :file-id) (assoc ::db/rollback (:rollback? props))
cfg (-> cfg (db/tx-run! (fn [{:keys [::db/conn] :as cfg}]
(assoc ::db/rollback (:rollback? props)) (let [cfg (-> cfg
(assoc ::min-age min-age))] (update ::sto/storage sto/configure conn)
(assoc ::timestamp (ct/now)))
processed? (process-file! cfg props)]
(try (when (and processed? (contains? cf/flags :tiered-file-data-storage))
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (wrk/submit! (-> cfg
(let [cfg (update cfg ::sto/storage sto/configure conn) (assoc ::wrk/task :offload-file-data)
processed? (process-file! cfg file-id)] (assoc ::wrk/params props)
(when (and processed? (contains? cf/flags :tiered-file-data-storage)) (assoc ::wrk/priority 10)
(wrk/submit! (-> cfg (assoc ::wrk/delay 1000))))
(assoc ::wrk/task :offload-file-data) processed?))))
(assoc ::wrk/params props) (catch Throwable cause
(assoc ::wrk/priority 10) (l/err :hint "error on cleaning file"
(assoc ::wrk/delay 1000)))) :file-id (str (:file-id props))
processed?))) :cause cause)))))
(catch Throwable cause
(l/err :hint "error on cleaning file"
:file-id (str (:file-id props))
:cause cause))))))

View File

@@ -17,29 +17,29 @@
(def ^:private (def ^:private
sql:get-candidates sql:get-candidates
"SELECT f.id, "SELECT f.id,
f.revn,
f.modified_at f.modified_at
FROM file AS f FROM file AS f
WHERE f.has_media_trimmed IS false WHERE f.has_media_trimmed IS false
AND f.modified_at < now() - ?::interval AND f.modified_at < now() - ?::interval
AND f.deleted_at IS NULL AND f.deleted_at IS NULL
ORDER BY f.modified_at DESC ORDER BY f.modified_at DESC
FOR UPDATE FOR UPDATE OF f
SKIP LOCKED") SKIP LOCKED")
(defn- get-candidates (defn- get-candidates
[{:keys [::db/conn ::min-age] :as cfg}] [{:keys [::db/conn ::min-age] :as cfg}]
(let [min-age (db/interval min-age)] (let [min-age (db/interval min-age)]
(db/cursor conn [sql:get-candidates min-age] {:chunk-size 10}))) (db/plan conn [sql:get-candidates min-age] {:fetch-size 10})))
(defn- schedule! (defn- schedule!
[{:keys [::min-age] :as cfg}] [cfg]
(let [total (reduce (fn [total {:keys [id]}] (let [total (reduce (fn [total {:keys [id modified-at revn]}]
(let [params {:file-id id :min-age min-age}] (let [params {:file-id id :modified-at modified-at :revn revn}]
(wrk/submit! (assoc cfg ::wrk/params params)) (wrk/submit! (assoc cfg ::wrk/params params))
(inc total))) (inc total)))
0 0
(get-candidates cfg))] (get-candidates cfg))]
{:processed total})) {:processed total}))
(defmethod ig/assert-key ::handler (defmethod ig/assert-key ::handler
@@ -48,7 +48,7 @@
(defmethod ig/expand-key ::handler (defmethod ig/expand-key ::handler
[k v] [k v]
{k (assoc v ::min-age (cf/get-deletion-delay))}) {k (assoc v ::min-age (cf/get-file-clean-delay))})
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ cfg] [_ cfg]

View File

@@ -11,6 +11,7 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.common.time :as ct] [app.common.time :as ct]
[app.db :as db] [app.db :as db]
[app.features.fdata :as fdata]
[app.storage :as sto] [app.storage :as sto]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -27,14 +28,14 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-profiles deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-profiles deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id]}] (reduce (fn [total {:keys [id photo-id]}]
(l/trc :hint "permanently delete" :rel "profile" :id (str id)) (l/trc :obj "profile" :id (str id))
;; Mark as deleted the storage object ;; Mark as deleted the storage object
(some->> photo-id (sto/touch-object! storage)) (some->> photo-id (sto/touch-object! storage))
(db/delete! conn :profile {:id id}) (let [affected (-> (db/delete! conn :profile {:id id})
(db/get-update-count))]
(inc total)) (+ total affected)))
0))) 0)))
(def ^:private sql:get-teams (def ^:private sql:get-teams
@@ -50,8 +51,7 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-teams deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-teams deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id photo-id deleted-at]}] (reduce (fn [total {:keys [id photo-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :obj "team"
:rel "team"
:id (str id) :id (str id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
@@ -59,9 +59,9 @@
(some->> photo-id (sto/touch-object! storage)) (some->> photo-id (sto/touch-object! storage))
;; And finally, permanently delete the team. ;; And finally, permanently delete the team.
(db/delete! conn :team {:id id}) (let [affected (-> (db/delete! conn :team {:id id})
(db/get-update-count))]
(inc total)) (+ total affected)))
0))) 0)))
(def ^:private sql:get-fonts (def ^:private sql:get-fonts
@@ -78,8 +78,7 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-fonts deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-fonts deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at] :as font}] (reduce (fn [total {:keys [id team-id deleted-at] :as font}]
(l/trc :hint "permanently delete" (l/trc :obj "font-variant"
:rel "team-font-variant"
:id (str id) :id (str id)
:team-id (str team-id) :team-id (str team-id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
@@ -90,10 +89,9 @@
(some->> (:otf-file-id font) (sto/touch-object! storage)) (some->> (:otf-file-id font) (sto/touch-object! storage))
(some->> (:ttf-file-id font) (sto/touch-object! storage)) (some->> (:ttf-file-id font) (sto/touch-object! storage))
;; And finally, permanently delete the team font variant (let [affected (-> (db/delete! conn :team-font-variant {:id id})
(db/delete! conn :team-font-variant {:id id}) (db/get-update-count))]
(+ total affected)))
(inc total))
0))) 0)))
(def ^:private sql:get-projects (def ^:private sql:get-projects
@@ -110,45 +108,40 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-projects deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-projects deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id team-id deleted-at]}] (reduce (fn [total {:keys [id team-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :obj "project"
:rel "project"
:id (str id) :id (str id)
:team-id (str team-id) :team-id (str team-id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
;; And finally, permanently delete the project. (let [affected (-> (db/delete! conn :project {:id id})
(db/delete! conn :project {:id id}) (db/get-update-count))]
(+ total affected)))
(inc total))
0))) 0)))
(def ^:private sql:get-files (def ^:private sql:get-files
"SELECT id, deleted_at, project_id, data_backend, data_ref_id "SELECT f.id,
FROM file f.deleted_at,
WHERE deleted_at IS NOT NULL f.project_id
AND deleted_at < now() + ?::interval FROM file AS f
ORDER BY deleted_at ASC WHERE f.deleted_at IS NOT NULL
AND f.deleted_at < now() + ?::interval
ORDER BY f.deleted_at ASC
LIMIT ? LIMIT ?
FOR UPDATE FOR UPDATE
SKIP LOCKED") SKIP LOCKED")
(defn- delete-files! (defn- delete-files!
[{:keys [::db/conn ::sto/storage ::deletion-threshold ::chunk-size] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-files deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-files deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id deleted-at project-id] :as file}] (reduce (fn [total {:keys [id deleted-at project-id] :as file}]
(l/trc :hint "permanently delete" (l/trc :obj "file"
:rel "file"
:id (str id) :id (str id)
:project-id (str project-id) :project-id (str project-id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(when (= "objects-storage" (:data-backend file)) (let [affected (-> (db/delete! conn :file {:id id})
(sto/touch-object! storage (:data-ref-id file))) (db/get-update-count))]
(+ total affected)))
;; And finally, permanently delete the file.
(db/delete! conn :file {:id id})
(inc total))
0))) 0)))
(def ^:private sql:get-file-thumbnails (def ^:private sql:get-file-thumbnails
@@ -165,8 +158,7 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-thumbnails deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-file-thumbnails deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}] (reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :obj "file-thumbnail"
:rel "file-thumbnail"
:file-id (str file-id) :file-id (str file-id)
:revn revn :revn revn
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
@@ -174,10 +166,9 @@
;; Mark as deleted the storage object ;; Mark as deleted the storage object
(some->> media-id (sto/touch-object! storage)) (some->> media-id (sto/touch-object! storage))
;; And finally, permanently delete the object (let [affected (-> (db/delete! conn :file-thumbnail {:file-id file-id :revn revn})
(db/delete! conn :file-thumbnail {:file-id file-id :revn revn}) (db/get-update-count))]
(+ total affected)))
(inc total))
0))) 0)))
(def ^:private sql:get-file-object-thumbnails (def ^:private sql:get-file-object-thumbnails
@@ -194,8 +185,7 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-object-thumbnails deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-file-object-thumbnails deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}] (reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
(l/trc :hint "permanently delete" (l/trc :obj "file-object-thumbnail"
:rel "file-tagged-object-thumbnail"
:file-id (str file-id) :file-id (str file-id)
:object-id object-id :object-id object-id
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
@@ -203,36 +193,10 @@
;; Mark as deleted the storage object ;; Mark as deleted the storage object
(some->> media-id (sto/touch-object! storage)) (some->> media-id (sto/touch-object! storage))
;; And finally, permanently delete the object (let [affected (-> (db/delete! conn :file-tagged-object-thumbnail
(db/delete! conn :file-tagged-object-thumbnail {:file-id file-id :object-id object-id}) {:file-id file-id :object-id object-id})
(db/get-update-count))]
(inc total)) (+ total affected)))
0)))
(def ^:private sql:get-file-data-fragments
"SELECT file_id, id, deleted_at, data_ref_id
FROM file_data_fragment
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-data-fragments!
[{:keys [::db/conn ::sto/storage ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-data-fragments deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
(l/trc :hint "permanently delete"
:rel "file-data-fragment"
:id (str id)
:file-id (str file-id)
:deleted-at (ct/format-inst deleted-at))
(some->> data-ref-id (sto/touch-object! storage))
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
(inc total))
0))) 0)))
(def ^:private sql:get-file-media-objects (def ^:private sql:get-file-media-objects
@@ -249,8 +213,7 @@
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
(->> (db/plan conn [sql:get-file-media-objects deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-file-media-objects deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}] (reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
(l/trc :hint "permanently delete" (l/trc :obj "file-media-object"
:rel "file-media-object"
:id (str id) :id (str id)
:file-id (str file-id) :file-id (str file-id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
@@ -259,13 +222,48 @@
(some->> (:media-id fmo) (sto/touch-object! storage)) (some->> (:media-id fmo) (sto/touch-object! storage))
(some->> (:thumbnail-id fmo) (sto/touch-object! storage)) (some->> (:thumbnail-id fmo) (sto/touch-object! storage))
(db/delete! conn :file-media-object {:id id}) (let [affected (-> (db/delete! conn :file-media-object {:id id})
(db/get-update-count))]
(+ total affected)))
0)))
(inc total)) (def ^:private sql:get-file-data
"SELECT file_id, id, type, deleted_at, metadata, backend
FROM file_data
WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval
ORDER BY deleted_at ASC
LIMIT ?
FOR UPDATE
SKIP LOCKED")
(defn- delete-file-data!
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-data deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [file-id id type deleted-at metadata backend]}]
(some->> metadata
(fdata/decode-metadata)
(fdata/process-metadata cfg))
(l/trc :obj "file-data"
:id (str id)
:file-id (str file-id)
:type type
:backend backend
:deleted-at (ct/format-inst deleted-at))
(let [affected (-> (db/delete! conn :file-data
{:file-id file-id
:id id
:type type})
(db/get-update-count))]
(+ total affected)))
0))) 0)))
(def ^:private sql:get-file-change (def ^:private sql:get-file-change
"SELECT id, file_id, deleted_at, data_backend, data_ref_id "SELECT id, file_id, deleted_at
FROM file_change FROM file_change
WHERE deleted_at IS NOT NULL WHERE deleted_at IS NOT NULL
AND deleted_at < now() + ?::interval AND deleted_at < now() + ?::interval
@@ -275,29 +273,25 @@
SKIP LOCKED") SKIP LOCKED")
(defn- delete-file-changes! (defn- delete-file-changes!
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}] [{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
(->> (db/plan conn [sql:get-file-change deletion-threshold chunk-size] {:fetch-size 5}) (->> (db/plan conn [sql:get-file-change deletion-threshold chunk-size] {:fetch-size 5})
(reduce (fn [total {:keys [id file-id deleted-at] :as xlog}] (reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
(l/trc :hint "permanently delete" (l/trc :obj "file-change"
:rel "file-change"
:id (str id) :id (str id)
:file-id (str file-id) :file-id (str file-id)
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(when (= "objects-storage" (:data-backend xlog)) (let [affected (-> (db/delete! conn :file-change {:id id})
(sto/touch-object! storage (:data-ref-id xlog))) (db/get-update-count))]
(+ total affected)))
(db/delete! conn :file-change {:id id})
(inc total))
0))) 0)))
(def ^:private deletion-proc-vars (def ^:private deletion-proc-vars
[#'delete-profiles! [#'delete-profiles!
#'delete-file-media-objects! #'delete-file-media-objects!
#'delete-file-data-fragments!
#'delete-file-object-thumbnails! #'delete-file-object-thumbnails!
#'delete-file-thumbnails! #'delete-file-thumbnails!
#'delete-file-data!
#'delete-file-changes! #'delete-file-changes!
#'delete-files! #'delete-files!
#'delete-projects! #'delete-projects!
@@ -309,9 +303,10 @@
until 0 results is returned" until 0 results is returned"
[cfg proc-fn] [cfg proc-fn]
(loop [total 0] (loop [total 0]
(let [result (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (let [result (db/tx-run! cfg
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) (fn [{:keys [::db/conn] :as cfg}]
(proc-fn cfg)))] (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
(proc-fn cfg)))]
(if (pos? result) (if (pos? result)
(recur (long (+ total result))) (recur (long (+ total result)))
total)))) total))))
@@ -336,6 +331,4 @@
(let [result (execute-proc! cfg proc-fn)] (let [result (execute-proc! cfg proc-fn)]
(recur (rest procs) (recur (rest procs)
(long (+ total result)))) (long (+ total result))))
(do {:processed total})))))
(l/inf :hint "task finished" :deleted total)
{:processed total}))))))

View File

@@ -8,101 +8,25 @@
"A maintenance task responsible of moving file data from hot "A maintenance task responsible of moving file data from hot
storage (the database row) to a cold storage (fs or s3)." storage (the database row) to a cold storage (fs or s3)."
(:require (:require
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.features.fdata :as fdata]
[app.storage :as sto] [app.storage :as sto]
[integrant.core :as ig])) [integrant.core :as ig]))
(defn- offload-file-data! (def ^:private sql:get-file-data
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}] "SELECT fd.*
(let [file (db/get conn :file {:id file-id} FROM file_data AS fd
{::sql/for-update true})] WHERE fd.file_id = ?
(when (nil? (:data file)) AND fd.backend = 'db'
(ex/raise :hint "file already offloaded" AND fd.deleted_at IS NULL")
:type :internal
:code :file-already-offloaded
:file-id file-id))
(let [data (sto/content (:data file)) (defn- offload-file-data
sobj (sto/put-object! storage [cfg {:keys [id file-id type] :as fdata}]
{::sto/content data (fdata/upsert! cfg (assoc fdata :backend "storage"))
::sto/touch true (l/trc :file-id (str file-id)
:bucket "file-data" :id (str id)
:content-type "application/octet-stream" :type type))
:file-id file-id})]
(l/trc :hint "offload file data"
:file-id (str file-id)
:storage-id (str (:id sobj)))
(db/update! conn :file
{:data-backend "objects-storage"
:data-ref-id (:id sobj)
:data nil}
{:id file-id}
{::db/return-keys false}))))
(defn- offload-file-data-fragments!
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
(doseq [fragment (db/query conn :file-data-fragment
{:file-id file-id
:deleted-at nil
:data-backend nil}
{::db/for-update true})]
(let [data (sto/content (:data fragment))
sobj (sto/put-object! storage
{::sto/content data
::sto/touch true
:bucket "file-data-fragment"
:content-type "application/octet-stream"
:file-id file-id
:file-fragment-id (:id fragment)})]
(l/trc :hint "offload file data fragment"
:file-id (str file-id)
:file-fragment-id (str (:id fragment))
:storage-id (str (:id sobj)))
(db/update! conn :file-data-fragment
{:data-backend "objects-storage"
:data-ref-id (:id sobj)
:data nil}
{:id (:id fragment)}
{::db/return-keys false}))))
(def sql:get-snapshots
"SELECT fc.*
FROM file_change AS fc
WHERE fc.file_id = ?
AND fc.label IS NOT NULL
AND fc.data IS NOT NULL
AND fc.data_backend IS NULL")
(defn- offload-file-snapshots!
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
(doseq [snapshot (db/exec! conn [sql:get-snapshots file-id])]
(let [data (sto/content (:data snapshot))
sobj (sto/put-object! storage
{::sto/content data
::sto/touch true
:bucket "file-change"
:content-type "application/octet-stream"
:file-id file-id
:file-change-id (:id snapshot)})]
(l/trc :hint "offload file change"
:file-id (str file-id)
:file-change-id (str (:id snapshot))
:storage-id (str (:id sobj)))
(db/update! conn :file-change
{:data-backend "objects-storage"
:data-ref-id (:id sobj)
:data nil}
{:id (:id snapshot)}
{::db/return-keys false}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLER ;; HANDLER
@@ -116,10 +40,9 @@
(defmethod ig/init-key ::handler (defmethod ig/init-key ::handler
[_ cfg] [_ cfg]
(fn [{:keys [props] :as task}] (fn [{:keys [props] :as task}]
(-> cfg (let [file-id (:file-id props)]
(assoc ::db/rollback (:rollback? props)) (-> cfg
(assoc ::file-id (:file-id props)) (assoc ::db/rollback (:rollback? props))
(db/tx-run! (fn [cfg] (db/tx-run! (fn [{:keys [::db/conn] :as cfg}]
(offload-file-data! cfg) (run! (partial offload-file-data cfg)
(offload-file-data-fragments! cfg) (db/plan conn [sql:get-file-data file-id]))))))))
(offload-file-snapshots! cfg))))))

View File

@@ -62,7 +62,8 @@
(def default (def default
{:database-uri "postgresql://postgres/penpot_test" {:database-uri "postgresql://postgres/penpot_test"
:redis-uri "redis://redis/1" :redis-uri "redis://redis/1"
:auto-file-snapshot-every 1}) :auto-file-snapshot-every 1
:file-data-backend "db"})
(def config (def config
(cf/read-config :prefix "penpot-test" (cf/read-config :prefix "penpot-test"
@@ -74,9 +75,6 @@
:enable-smtp :enable-smtp
:enable-quotes :enable-quotes
:enable-rpc-climit :enable-rpc-climit
:enable-feature-fdata-pointer-map
:enable-feature-fdata-objets-map
:enable-feature-components-v2
:enable-auto-file-snapshot :enable-auto-file-snapshot
:disable-file-validation]) :disable-file-validation])

View File

@@ -144,7 +144,6 @@
(t/is (not= (:modified-at comment) (:modified-at comment'))) (t/is (not= (:modified-at comment) (:modified-at comment')))
(t/is (= (:content data) (:content comment')))))) (t/is (= (:content data) (:content comment'))))))
(t/testing "retrieve threads" (t/testing "retrieve threads"
(let [data {::th/type :get-comment-threads (let [data {::th/type :get-comment-threads
::rpc/profile-id (:id profile-1) ::rpc/profile-id (:id profile-1)

View File

@@ -29,7 +29,7 @@
true true
(catch Throwable _cause (catch Throwable _cause
false))) false)))
{:num 30})) {:num 15}))

View File

@@ -8,10 +8,10 @@
(:require (:require
[app.common.features :as cfeat] [app.common.features :as cfeat]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.pprint :as pp]
[app.common.thumbnails :as thc] [app.common.thumbnails :as thc]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.http :as http] [app.http :as http]
@@ -87,10 +87,7 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))))
(let [result (:result out)]
(t/is (= "label1" (:label result)))
(t/is (uuid? (:id result)))))
(let [[row1 row2 :as rows] (let [[row1 row2 :as rows]
(th/db-query :file-change (th/db-query :file-change
@@ -116,7 +113,7 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))))) (t/is (true? (:result out)))))
(t/testing "delete system created snapshot" (t/testing "delete system created snapshot"
(let [params {::th/type :delete-file-snapshot (let [params {::th/type :delete-file-snapshot
@@ -130,7 +127,15 @@
data (ex-data error)] data (ex-data error)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= (:type data) :validation)) (t/is (= (:type data) :validation))
(t/is (= (:code data) :system-snapshots-cant-be-deleted))))))))) (t/is (= (:code data) :system-snapshots-cant-be-deleted)))))
;; this will run pending task triggered by deleting user snapshot
(th/run-pending-tasks!)
;; this will
(let [res (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
;; delete 2 snapshots and 2 file data entries
(t/is (= 4 (:processed res))))))))
(t/deftest snapshots-locking (t/deftest snapshots-locking
(let [profile-1 (th/create-profile* 1 {:is-active true}) (let [profile-1 (th/create-profile* 1 {:is-active true})
@@ -172,7 +177,7 @@
out (th/command! params)] out (th/command! params)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))) (t/is (true? (:result out)))
(let [snapshot (th/db-get :file-change {:id (:id snapshot)})] (let [snapshot (th/db-get :file-change {:id (:id snapshot)})]
(t/is (= (:id profile-1) (:locked-by snapshot)))))) (t/is (= (:id profile-1) (:locked-by snapshot))))))
@@ -199,7 +204,7 @@
out (th/command! params)] out (th/command! params)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))) (t/is (true? (:result out)))
(let [snapshot (th/db-get :file-change {:id (:id snapshot)})] (let [snapshot (th/db-get :file-change {:id (:id snapshot)})]
(t/is (= nil (:locked-by snapshot)))))) (t/is (= nil (:locked-by snapshot))))))
@@ -213,4 +218,4 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out))))))) (t/is (true? (:result out)))))))

View File

@@ -15,6 +15,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.db.sql :as sql] [app.db.sql :as sql]
[app.features.fdata :as fdata]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
@@ -185,10 +186,10 @@
shape-id (uuid/random)] shape-id (uuid/random)]
;; Preventive file-gc ;; Preventive file-gc
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file) :revn (:revn file)})))
;; Check the number of fragments before adding the page ;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows)))) (t/is (= 2 (count rows))))
;; Add page ;; Add page
@@ -203,22 +204,23 @@
:id page-id}]) :id page-id}])
;; Check the number of fragments before adding the page ;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows)))) (t/is (= 3 (count rows))))
;; The file-gc should mark for remove unused fragments ;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check the number of fragments ;; Check the number of fragments
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 5 (count rows)))) (t/is (= 5 (count rows)))
(t/is (= 3 (count (filterv :deleted-at rows)))))
;; The objects-gc should remove unused fragments ;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res)))) (t/is (= 3 (:processed res))))
;; Check the number of fragments ;; Check the number of fragments
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows)))) (t/is (= 2 (count rows))))
;; Add shape to page that should add a new fragment ;; Add shape to page that should add a new fragment
@@ -242,44 +244,47 @@
:type :rect})}]) :type :rect})}])
;; Check the number of fragments ;; Check the number of fragments
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 3 (count rows)))) (t/is (= 3 (count rows))))
;; The file-gc should mark for remove unused fragments ;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; The objects-gc should remove unused fragments ;; The objects-gc should remove unused fragments
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed res)))) (t/is (= 3 (:processed res))))
;; Check the number of fragments; ;; Check the number of fragments;
(let [rows (th/db-query :file-data-fragment {:file-id (:id file) (let [rows (th/db-query :file-data {:file-id (:id file)
:deleted-at nil})] :type "fragment"
:deleted-at nil})]
(t/is (= 2 (count rows)))) (t/is (= 2 (count rows))))
;; Lets proceed to delete all changes ;; Lets proceed to delete all changes
(th/db-delete! :file-change {:file-id (:id file)}) (th/db-delete! :file-change {:file-id (:id file)})
(th/db-delete! :file-data {:file-id (:id file) :type "snapshot"})
(th/db-update! :file (th/db-update! :file
{:has-media-trimmed false} {:has-media-trimmed false}
{:id (:id file)}) {:id (:id file)})
;; The file-gc should remove fragments related to changes ;; The file-gc should remove fragments related to changes
;; snapshots previously deleted. ;; snapshots previously deleted.
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Check the number of fragments; ;; Check the number of fragments;
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
;; (pp/pprint rows) ;; (pp/pprint rows)
(t/is (= 4 (count rows))) (t/is (= 4 (count rows)))
(t/is (= 2 (count (remove (comp some? :deleted-at) rows))))) (t/is (= 2 (count (remove :deleted-at rows)))))
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 2 (count rows))))))) (t/is (= 2 (count rows)))))))
(t/deftest file-gc-task-with-thumbnails (t/deftest file-gc-with-thumbnails
(letfn [(add-file-media-object [& {:keys [profile-id file-id]}] (letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
(let [mfile {:filename "sample.jpg" (let [mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg") :path (th/tempfile "backend_tests/test_files/sample.jpg")
@@ -347,7 +352,7 @@
:fills [{:fill-opacity 1 :fills [{:fill-opacity 1
:fill-image {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}}]})}]) :fill-image {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}}]})}])
;; Check that reference storage objects on filemediaobjects ;; Check that reference storage objects on file_media_objects
;; are the same because of deduplication feature. ;; are the same because of deduplication feature.
(t/is (= (:media-id fmo1) (:media-id fmo2))) (t/is (= (:media-id fmo1) (:media-id fmo2)))
(t/is (= (:thumbnail-id fmo1) (:thumbnail-id fmo2))) (t/is (= (:thumbnail-id fmo1) (:thumbnail-id fmo2)))
@@ -360,32 +365,33 @@
(t/is (= 2 (:freeze res))) (t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
;; run the file-gc task immediately without forced min-age
(t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))
;; run the task again ;; run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; retrieve file and check trimmed attribute ;; retrieve file and check trimmed attribute
(let [row (th/db-get :file {:id (:id file)})] (let [row (th/db-get :file {:id (:id file)})]
(t/is (true? (:has-media-trimmed row)))) (t/is (true? (:has-media-trimmed row))))
;; check file media objects ;; check file media objects
(let [rows (th/db-query :file-media-object {:file-id (:id file)})] (let [[row1 row2 :as rows]
(t/is (= 2 (count rows))) (th/db-query :file-media-object
(t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) {:file-id (:id file)}
{:order-by [:created-at]})]
(t/is (= (:id fmo1) (:id row1)))
(t/is (= (:id fmo2) (:id row2)))
(t/is (ct/inst? (:deleted-at row2))))
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
;; delete 2 fragments and 1 media object
(t/is (= 3 (:processed res)))) (t/is (= 3 (:processed res))))
;; check file media objects ;; check file media objects
(let [rows (th/db-query :file-media-object {:file-id (:id file)})] (let [rows (th/db-query :file-media-object {:file-id (:id file)})]
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) (t/is (= 1 (count (remove :deleted-at rows)))))
;; The underlying storage objects are still available. ;; The underlying storage objects are still available.
(t/is (some? (sto/get-object storage (:media-id fmo2))))
(t/is (some? (sto/get-object storage (:thumbnail-id fmo2))))
(t/is (some? (sto/get-object storage (:media-id fmo1)))) (t/is (some? (sto/get-object storage (:media-id fmo1))))
(t/is (some? (sto/get-object storage (:thumbnail-id fmo1)))) (t/is (some? (sto/get-object storage (:thumbnail-id fmo1))))
@@ -402,34 +408,40 @@
;; Now, we have deleted the usage of pointers to the ;; Now, we have deleted the usage of pointers to the
;; file-media-objects, if we paste file-gc, they should be marked ;; file-media-objects, if we paste file-gc, they should be marked
;; as deleted. ;; as deleted.
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; This only clears fragments, the file media objects still referenced because ;; This only clears fragments, the file media objects still referenced because
;; snapshots are preserved ;; snapshots are preserved
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
;; Mark all snapshots to be a non-snapshot file change ;; Delete all snapshots
(th/db-exec! ["update file_change set data = null where file_id = ?" (:id file)]) (th/db-exec! ["update file_data set deleted_at = now() where file_id = ? and type = 'snapshot'" (:id file)])
(th/db-exec! ["update file_change set deleted_at = now() where file_id = ? and label is not null" (:id file)])
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)]) (th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)])
(let [res (th/run-task! :objects-gc {:deletion-threshold 0})]
;; this will remove the file change and file data entries for two snapshots
(t/is (= 4 (:processed res))))
;; Rerun the file-gc and objects-gc ;; Rerun the file-gc and objects-gc
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [res (th/run-task! :objects-gc {})]
(let [res (th/run-task! :objects-gc {:deletion-threshold 0})]
;; this will remove the file media objects marked as deleted
;; on prev file-gc
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
;; Now that file-gc have deleted the file-media-object usage, ;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of ;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted. ;; them are marked to be deleted
(let [res (th/run-task! :storage-gc-touched {:min-age 0})] (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res)))) (t/is (= 2 (:delete res))))
;; Finally, check that some of the objects that are marked as ;; Finally, check that some of the objects that are marked as
;; deleted we are unable to retrieve them using standard storage ;; deleted we are unable to retrieve them using standard storage
;; public api. ;; public api
(t/is (nil? (sto/get-object storage (:media-id fmo2))))
(t/is (nil? (sto/get-object storage (:thumbnail-id fmo2))))
(t/is (nil? (sto/get-object storage (:media-id fmo1)))) (t/is (nil? (sto/get-object storage (:media-id fmo1))))
(t/is (nil? (sto/get-object storage (:thumbnail-id fmo1))))))) (t/is (nil? (sto/get-object storage (:thumbnail-id fmo1)))))))
@@ -470,8 +482,9 @@
page-id (first (get-in file [:data :pages]))] page-id (first (get-in file [:data :pages]))]
(let [rows (th/db-query :file-data-fragment {:file-id (:id file) (let [rows (th/db-query :file-data {:file-id (:id file)
:deleted-at nil})] :type "fragment"
:deleted-at nil})]
(t/is (= (count rows) 1))) (t/is (= (count rows) 1)))
;; Update file inserting a new image object ;; Update file inserting a new image object
@@ -536,17 +549,15 @@
:strokes [{:stroke-opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}]) :strokes [{:stroke-opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}])
;; run the file-gc task immediately without forced min-age
(t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))
;; run the task again ;; run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
(let [rows (th/db-query :file-data-fragment {:file-id (:id file) (let [rows (th/db-query :file-data {:file-id (:id file)
:deleted-at nil})] :type "fragment"
:deleted-at nil})]
(t/is (= (count rows) 1))) (t/is (= (count rows) 1)))
;; retrieve file and check trimmed attribute ;; retrieve file and check trimmed attribute
@@ -583,7 +594,7 @@
;; Now, we have deleted the usage of pointers to the ;; Now, we have deleted the usage of pointers to the
;; file-media-objects, if we paste file-gc, they should be marked ;; file-media-objects, if we paste file-gc, they should be marked
;; as deleted. ;; as deleted.
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; This only removes unused fragments, file media are still ;; This only removes unused fragments, file media are still
;; referenced on snapshots. ;; referenced on snapshots.
@@ -592,16 +603,18 @@
;; Mark all snapshots to be a non-snapshot file change ;; Mark all snapshots to be a non-snapshot file change
(th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)]) (th/db-exec! ["update file set has_media_trimmed = false where id = ?" (:id file)])
(th/db-exec! ["update file_change set data = null where file_id = ?" (:id file)]) (th/db-delete! :file-data {:file-id (:id file)
:type "snapshot"})
;; Rerun file-gc and objects-gc task for the same file once all snapshots are ;; Rerun file-gc and objects-gc task for the same file once all snapshots are
;; "expired/deleted" ;; "expired/deleted"
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 6 (:processed res)))) (t/is (= 6 (:processed res))))
(let [rows (th/db-query :file-data-fragment {:file-id (:id file) (let [rows (th/db-query :file-data {:file-id (:id file)
:deleted-at nil})] :type "fragment"
:deleted-at nil})]
(t/is (= (count rows) 1))) (t/is (= (count rows) 1)))
;; Now that file-gc have deleted the file-media-object usage, ;; Now that file-gc have deleted the file-media-object usage,
@@ -620,7 +633,7 @@
(t/is (nil? (sto/get-object storage (:media-id fmo2)))) (t/is (nil? (sto/get-object storage (:media-id fmo2))))
(t/is (nil? (sto/get-object storage (:media-id fmo1))))))) (t/is (nil? (sto/get-object storage (:media-id fmo1)))))))
(t/deftest file-gc-task-with-object-thumbnails (t/deftest file-gc-with-object-thumbnails
(letfn [(insert-file-object-thumbnail! [& {:keys [profile-id file-id page-id frame-id]}] (letfn [(insert-file-object-thumbnail! [& {:keys [profile-id file-id page-id frame-id]}]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "frame") (let [object-id (thc/fmt-object-id file-id page-id frame-id "frame")
mfile {:filename "sample.jpg" mfile {:filename "sample.jpg"
@@ -704,11 +717,7 @@
(t/is (= 1 (:freeze res))) (t/is (= 1 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
;; run the file-gc task immediately without forced min-age (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))
;; run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; retrieve file and check trimmed attribute ;; retrieve file and check trimmed attribute
(let [row (th/db-get :file {:id (:id file)})] (let [row (th/db-get :file {:id (:id file)})]
@@ -738,7 +747,7 @@
:page-id page-id :page-id page-id
:id frame-id-2}]) :id frame-id-2}])
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
@@ -772,7 +781,7 @@
:page-id page-id :page-id page-id
:id frame-id-1}]) :id frame-id-1}])
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})]
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
@@ -933,6 +942,8 @@
out (th/command! params)] out (th/command! params)]
(t/is (nil? (:error out)))) (t/is (nil? (:error out))))
(th/run-pending-tasks!)
;; query the list of files after soft deletion ;; query the list of files after soft deletion
(let [data {::th/type :get-project-files (let [data {::th/type :get-project-files
::rpc/profile-id (:id profile1) ::rpc/profile-id (:id profile1)
@@ -943,11 +954,6 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= 0 (count result))))) (t/is (= 0 (count result)))))
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {})]
(t/is (= 0 (:processed result))))
;; query the list of file libraries of a after hard deletion
(let [data {::th/type :get-file-libraries (let [data {::th/type :get-file-libraries
::rpc/profile-id (:id profile1) ::rpc/profile-id (:id profile1)
:file-id (:id file)} :file-id (:id file)}
@@ -957,9 +963,13 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= 0 (count result))))) (t/is (= 0 (count result)))))
;; run permanent deletion (should be noop)
(let [result (th/run-task! :objects-gc {})]
(t/is (= 0 (:processed result))))
;; run permanent deletion ;; run permanent deletion
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})] (let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 1 (:processed result)))) (t/is (= 3 (:processed result))))
;; query the list of file libraries of a after hard deletion ;; query the list of file libraries of a after hard deletion
(let [data {::th/type :get-file-libraries (let [data {::th/type :get-file-libraries
@@ -972,7 +982,6 @@
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found)))))) (t/is (= (:type error-data) :not-found))))))
(t/deftest object-thumbnails-ops (t/deftest object-thumbnails-ops
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
file (th/create-file* 1 {:profile-id (:id prof) file (th/create-file* 1 {:profile-id (:id prof)
@@ -1282,17 +1291,19 @@
:is-shared false}) :is-shared false})
page-id (uuid/random) page-id (uuid/random)
shape-id (uuid/random)] shape-id (uuid/random)
sobject (volatile! nil)]
;; Preventive file-gc ;; Preventive file-gc
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Preventive objects-gc ;; Preventive objects-gc
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
;; deletes the fragment created by file-gc
(t/is (= 1 (:processed result)))) (t/is (= 1 (:processed result))))
;; Check the number of fragments before adding the page ;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (every? #(some? (:data %)) rows))) (t/is (every? #(some? (:data %)) rows)))
@@ -1301,35 +1312,42 @@
{:has-media-trimmed false} {:has-media-trimmed false}
{:id (:id file)}) {:id (:id file)})
;; Run FileGC again, with tiered storage activated ;; Run FileGC again, with tiered storage activated
(with-redefs [app.config/flags (conj app.config/flags :tiered-file-data-storage)] (with-redefs [app.config/flags (conj app.config/flags :tiered-file-data-storage)]
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)}))))
;; The FileGC task will schedule an inner taskq ;; The FileGC task will schedule an inner taskq
(th/run-pending-tasks!)) (th/run-pending-tasks!)
;; Clean objects after file-gc (let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 2 (:freeze res)))
(t/is (= 0 (:delete res))))
;; Clean objects after file-gc
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed result)))) (t/is (= 1 (:processed result))))
;; Check the number of fragments before adding the page ;; Check the number of fragments before adding the page
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
;; (pp/pprint rows)
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (every? #(nil? (:data %)) rows)) (t/is (every? #(nil? (:data %)) rows))
(t/is (every? #(uuid? (:data-ref-id %)) rows)) (t/is (every? #(= "storage" (:backend %)) rows)))
(t/is (every? #(= "objects-storage" (:data-backend %)) rows)))
(let [file (th/db-get :file {:id (:id file)}) (let [file (-> (th/db-get :file-data {:id (:id file) :type "main"})
(update :metadata fdata/decode-metadata))
storage (sto/resolve th/*system*)] storage (sto/resolve th/*system*)]
(t/is (= "objects-storage" (:data-backend file))) ;; (pp/pprint file)
(t/is (= "storage" (:backend file)))
(t/is (nil? (:data file))) (t/is (nil? (:data file)))
(t/is (uuid? (:data-ref-id file)))
(let [sobj (sto/get-object storage (:data-ref-id file))] (let [sobj (sto/get-object storage (-> file :metadata :storage-ref-id))]
(vreset! sobject sobj)
;; (pp/pprint (meta sobj))
(t/is (= "file-data" (:bucket (meta sobj)))) (t/is (= "file-data" (:bucket (meta sobj))))
(t/is (= (:id file) (:file-id (meta sobj)))))) (t/is (= (:id file) (:file-id (meta sobj))))))
;; Add shape to page that should load from cold storage again into the hot storage (db) ;; Add shape to page that should load from cold storage again into the hot storage (db)
(update-file! (update-file!
:file-id (:id file) :file-id (:id file)
:profile-id (:id profile) :profile-id (:id profile)
@@ -1340,36 +1358,68 @@
:name "test" :name "test"
:id page-id}]) :id page-id}])
;; Check the number of fragments ;; Check the number of fragments
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 2 (count rows))))
;; Check the number of fragments
(let [[row1 row2 :as rows] (let [[row1 row2 :as rows]
(th/db-query :file-data-fragment (th/db-query :file-data
{:file-id (:id file) {:file-id (:id file)
:deleted-at nil} :type "fragment"}
{:order-by [:created-at]})] {:order-by [:created-at]})]
;; (pp/pprint rows) ;; (pp/pprint rows)
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
(t/is (nil? (:data row1))) (t/is (nil? (:data row1)))
(t/is (= "objects-storage" (:data-backend row1))) (t/is (= "storage" (:backend row1)))
(t/is (bytes? (:data row2))) (t/is (bytes? (:data row2)))
(t/is (nil? (:data-backend row2)))) (t/is (= "db" (:backend row2))))
;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)})))
;; The objects-gc should remove unused fragments ;; The file-gc should mark for remove unused fragments
(t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; The file-gc task, recreates all fragments, so after it we have
;; now the double of fragments, and the old ones are marked as
;; deleted, and the new ones are on DB
(let [[row1 row2 row3 row4 :as rows]
(th/db-query :file-data
{:file-id (:id file)
:type "fragment"}
{:order-by [:created-at]})]
;; (pp/pprint rows)
(t/is (= 4 (count rows)))
(t/is (nil? (:data row1)))
(t/is (ct/inst? (:deleted-at row1)))
(t/is (= "storage" (:backend row1)))
(t/is (bytes? (:data row2)))
(t/is (= "db" (:backend row2)))
(t/is (ct/inst? (:deleted-at row2)))
(t/is (bytes? (:data row3)))
(t/is (= "db" (:backend row3)))
(t/is (nil? (:deleted-at row3)))
(t/is (bytes? (:data row4)))
(t/is (= "db" (:backend row4)))
(t/is (nil? (:deleted-at row4))))
;; The objects-gc should remove the marked to delete fragments
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
;; Check the number of fragments before adding the page (let [rows (th/db-query :file-data {:file-id (:id file) :type "fragment"})]
(let [rows (th/db-query :file-data-fragment {:file-id (:id file)})]
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
(t/is (every? #(bytes? (:data %)) rows)) (t/is (every? #(bytes? (:data %)) rows))
(t/is (every? #(nil? (:data-ref-id %)) rows)) (t/is (every? #(= "db" (:backend %)) rows)))
(t/is (every? #(nil? (:data-backend %)) rows)))))
;; we ensure that once object-gc is passed and marked two storage
;; objects to delete
(let [res (th/run-task! :storage-gc-touched {:min-age 0})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
(let [storage (sto/resolve th/*system*)]
(t/is (uuid? (:id @sobject)))
(t/is (nil? (sto/get-object storage (:id @sobject)))))))
(t/deftest file-gc-with-components-1 (t/deftest file-gc-with-components-1
(let [storage (:app.storage/storage th/*system*) (let [storage (:app.storage/storage th/*system*)
@@ -1384,8 +1434,9 @@
page-id (first (get-in file [:data :pages]))] page-id (first (get-in file [:data :pages]))]
(let [rows (th/db-query :file-data-fragment {:file-id (:id file) (let [rows (th/db-query :file-data {:file-id (:id file)
:deleted-at nil})] :type "fragment"
:deleted-at nil})]
(t/is (= (count rows) 1))) (t/is (= (count rows) 1)))
;; Update file inserting new component ;; Update file inserting new component
@@ -1437,11 +1488,8 @@
:id c-id :id c-id
:anotation nil}]) :anotation nil}])
;; Run the file-gc task immediately without forced min-age
(t/is (false? (th/run-task! :file-gc {:file-id (:id file)})))
;; Run the task again ;; Run the task again
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file)})))
;; Retrieve file and check trimmed attribute ;; Retrieve file and check trimmed attribute
(let [row (th/db-get :file {:id (:id file)})] (let [row (th/db-get :file {:id (:id file)})]
@@ -1651,8 +1699,7 @@
(t/is (some? (not-empty (:objects component)))))) (t/is (some? (not-empty (:objects component))))))
;; Re-run the file-gc task ;; Re-run the file-gc task
(t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-1)}))) (t/is (true? (th/run-task! :file-gc {:file-id (:id file-1)})))
(t/is (false? (th/run-task! :file-gc {:min-age 0 :file-id (:id file-2)})))
;; Check that component is still there after file-gc task ;; Check that component is still there after file-gc task
(let [data {::th/type :get-file (let [data {::th/type :get-file

View File

@@ -39,8 +39,6 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(:result out))) (:result out)))
;; TODO: migrate to commands
(t/deftest duplicate-file (t/deftest duplicate-file
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
(configure-storage-backend)) (configure-storage-backend))
@@ -233,15 +231,7 @@
;; check that the both files are equivalent ;; check that the both files are equivalent
(doseq [[fa fb] (map vector p1-files p2-files)] (doseq [[fa fb] (map vector p1-files p2-files)]
(t/is (not= (:id fa) (:id fb))) (t/is (not= (:id fa) (:id fb)))
(t/is (= (:name fa) (:name fb))) (t/is (= (:name fa) (:name fb)))))))))
(when (= (:id fa) (:id file1))
(t/is (false? (b/equals? (:data fa)
(:data fb)))))
(when (= (:id fa) (:id file2))
(t/is (false? (b/equals? (:data fa)
(:data fb)))))))))))
(t/deftest duplicate-project-with-deleted-files (t/deftest duplicate-project-with-deleted-files
(let [storage (-> (:app.storage/storage th/*system*) (let [storage (-> (:app.storage/storage th/*system*)
@@ -297,15 +287,7 @@
;; check that the both files are equivalent ;; check that the both files are equivalent
(doseq [[fa fb] (map vector (rest p1-files) p2-files)] (doseq [[fa fb] (map vector (rest p1-files) p2-files)]
(t/is (not= (:id fa) (:id fb))) (t/is (not= (:id fa) (:id fb)))
(t/is (= (:name fa) (:name fb))) (t/is (= (:name fa) (:name fb)))))))))
(when (= (:id fa) (:id file1))
(t/is (false? (b/equals? (:data fa)
(:data fb)))))
(when (= (:id fa) (:id file2))
(t/is (false? (b/equals? (:data fa)
(:data fb)))))))))))
(t/deftest move-file-on-same-team (t/deftest move-file-on-same-team
(let [profile (th/create-profile* 1 {:is-active true}) (let [profile (th/create-profile* 1 {:is-active true})

View File

@@ -162,7 +162,7 @@
;; execute permanent deletion task ;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})] (let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result)))) (t/is (= 6 (:processed result))))
(let [row (th/db-get :team (let [row (th/db-get :team
{:id (:default-team-id prof)} {:id (:default-team-id prof)}
@@ -324,7 +324,7 @@
;; execute permanent deletion task ;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})] (let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 4 (:processed result)))) (t/is (= 6 (:processed result))))
(let [row (th/db-get :team (let [row (th/db-get :team
{:id (:default-team-id prof1)} {:id (:default-team-id prof1)}
@@ -363,7 +363,7 @@
;; execute permanent deletion task ;; execute permanent deletion task
(let [result (th/run-task! :objects-gc {:min-age 0})] (let [result (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 8 (:processed result)))))) (t/is (= 10 (:processed result))))))
(t/deftest email-blacklist-1 (t/deftest email-blacklist-1

View File

@@ -582,7 +582,7 @@
(t/is (ct/inst? (:deleted-at (first rows))))) (t/is (ct/inst? (:deleted-at (first rows)))))
(let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})] (let [result (th/run-task! :objects-gc {:deletion-threshold (cf/get-deletion-delay)})]
(t/is (= 5 (:processed result)))))) (t/is (= 7 (:processed result))))))
(t/deftest create-team-access-request (t/deftest create-team-access-request
(with-mocks [mock {:target 'app.email/send! :return nil}] (with-mocks [mock {:target 'app.email/send! :return nil}]

View File

@@ -105,13 +105,14 @@
(into frontend-only-features) (into frontend-only-features)
(into backend-only-features))) (into backend-only-features)))
(sm/register! (def schema:features
^{::sm/type ::features} (sm/register!
[:schema ^{::sm/type ::features}
{:title "FileFeatures" [:schema
::smdj/inline true {:title "FileFeatures"
:gen/gen (smg/subseq supported-features)} ::smdj/inline true
[::sm/set :string]]) :gen/gen (smg/subseq supported-features)}
[::sm/set :string]]))
(defn- flag->feature (defn- flag->feature
"Translate a flag to a feature name" "Translate a flag to a feature name"

View File

@@ -914,6 +914,8 @@
:gen/gen (sg/uri) :gen/gen (sg/uri)
:decode/string decode-uri :decode/string decode-uri
:decode/json decode-uri :decode/json decode-uri
:encode/json str
:encode/string str
::oapi/type "string" ::oapi/type "string"
::oapi/format "uri"}}) ::oapi/format "uri"}})

View File

@@ -155,7 +155,7 @@
(defn make-file (defn make-file
[{:keys [id project-id name revn is-shared features migrations [{:keys [id project-id name revn is-shared features migrations
ignore-sync-until created-at modified-at deleted-at] metadata backend ignore-sync-until created-at modified-at deleted-at]
:as params} :as params}
& {:keys [create-page with-data page-id] & {:keys [create-page with-data page-id]
@@ -186,8 +186,9 @@
:data data :data data
:features features :features features
:migrations migrations :migrations migrations
:metadata metadata
:backend backend
:ignore-sync-until ignore-sync-until :ignore-sync-until ignore-sync-until
:has-media-trimmed false
:created-at created-at :created-at created-at
:modified-at modified-at :modified-at modified-at
:deleted-at deleted-at})] :deleted-at deleted-at})]

View File

@@ -407,7 +407,8 @@
(watch [_ _ _] (watch [_ _ _]
(let [{:keys [on-error on-success] (let [{:keys [on-error on-success]
:or {on-error rx/throw :or {on-error rx/throw
on-success identity}} (meta data)] on-success identity}}
(meta data)]
(->> (rp/cmd! :request-profile-recovery data) (->> (rp/cmd! :request-profile-recovery data)
(rx/tap on-success) (rx/tap on-success)