From 582187dd036652e01fab7c5a0cabd0224a6e5a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 27 Aug 2026 11:50:14 +0200 Subject: [PATCH] :tada: Allow to validate and repair files from /dbg (#11335) --- backend/resources/app/templates/debug.tmpl | 43 ++++++++++ backend/src/app/http/debug.clj | 93 +++++++++++++++++++++- backend/src/app/srepl/helpers.clj | 5 +- backend/src/app/srepl/main.clj | 8 +- 4 files changed, 141 insertions(+), 8 deletions(-) diff --git a/backend/resources/app/templates/debug.tmpl b/backend/resources/app/templates/debug.tmpl index 42894b570c..635380282b 100644 --- a/backend/resources/app/templates/debug.tmpl +++ b/backend/resources/app/templates/debug.tmpl @@ -190,6 +190,20 @@ Debug Main Page + +
+ Validate file: + Given an FILE-ID, check the referential integrity. +
+
+ +
+
+ +
+
+
+
@@ -222,6 +236,7 @@ Debug Main Page
+
Import binfile: Import penpot file in binary format. @@ -236,6 +251,34 @@ Debug Main Page
+ +
+ Repair file: + Given an FILE-ID, repair the referential integrity errors. +
+
+ WARNING: the reparation is not guaranteed and may cause loss of data! +
+
+ You may need to give several repair rounds until all errors are cleared. +
+
+
+ +
+
+ + +
+ + A snapshot is made just before the validation, unless skipped. + +
+
+ +
+
+
{% endblock %} diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 26cac774f6..e86cc1ffda 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -13,6 +13,9 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.features :as cfeat] + [app.common.files.changes :as cfc] + [app.common.files.repair :as cfr] + [app.common.files.validate :as cfv] [app.common.logging :as l] [app.common.pprint :as pp] [app.common.time :as ct] @@ -28,6 +31,7 @@ [app.rpc.commands.teams :as teams] [app.setup :as-alias setup] [app.setup.clock :as clock] + [app.srepl.helpers :as h] [app.srepl.main :as srepl] [app.storage :as-alias sto] [app.storage.tmp :as tmp] @@ -130,7 +134,7 @@ :hint "invalid button")) (ex/raise :type :not-found - :code :enpty-data + :code :empty-data :hint "empty response")))) (defn- is-file-exists? @@ -484,6 +488,89 @@ {::yres/status 302 ::yres/headers {"location" "/dbg"}})))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; VALIDATE / REPAIR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- validate-file + [cfg {:keys [params] :as request}] + (let [file-id (some-> params :file-id parse-uuid)] + + (when-not file-id + (ex/raise :type :validation + :code :missing-arguments)) + + (db/tx-run! (assoc cfg ::db/rollback true) + (fn [cfg] + (let [file (bfc/get-file cfg file-id) + libs (bfc/get-resolved-file-libraries cfg file-id)] + (if file + (let [errors (cfv/validate-file file libs)] + {::yres/status 200 + ::yres/headers {"content-type" "text/plain"} + ::yres/body (if (empty? errors) + "NO VALIDATION ERRORS FOUND" + (pp/pprint-str errors))}) + (ex/raise :type :not-found + :code :empty-data + :hint "empty response"))))))) + +(defn- repair-file + [cfg {:keys [params] :as request}] + (let [file-id (some-> params :file-id parse-uuid) + skip-snapshot? (contains? params :skip-snapshot) + profile-id (:app.http.session/profile-id request)] + + (when-not file-id + (ex/raise :type :validation + :code :missing-arguments)) + + (let [output (StringBuilder.) + + repair-file + (fn [file libs _] + (let [errors (cfv/validate-file file libs)] + (.append output (if (empty? errors) + "NO VALIDATION ERRORS FOUND\n" + (str "VALIDATION ERRORS FOUND:\n" + (pp/pprint-str errors) "\n"))) + (if (empty? errors) + file + (let [changes (cfr/repair-file file libs errors)] + (-> file + (update :revn inc) + (update :data cfc/process-changes changes))))))] + + (add-watch l/log-record ::repair-watcher + (fn [_ _ _ record] + (when (= "app.common.files.repair" (::l/logger record)) + (let [props (::l/props record) + hint (get props :hint "") + args (dissoc props :hint) + message (str hint " " + (when-not (empty? args) + args) + "\n")] + (.append output message))))) + (try + (db/tx-run! cfg + h/process-file! + file-id + repair-file + {::h/with-libraries? true + ::h/validate? false + ::h/profile-id profile-id + ::h/snapshot-label (when-not skip-snapshot? "repair")}) + + (.append output "\nREPAIR FINISHED") + + {::yres/status 200 + ::yres/headers {"content-type" "text/plain"} + ::yres/body (.toString output)} + + (finally + (remove-watch l/log-record ::repair-watcher)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OTHER SMALL VIEWS/HANDLERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -578,5 +665,7 @@ {:handler (partial handle-team-features cfg)}] ["/file-export" {:handler (partial export-handler cfg)}] ["/file-import" {:handler (partial import-handler cfg)}] - ["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]]) + ["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}] + ["/file-validate" {:handler (partial validate-file cfg)}] + ["/file-repair" {:handler (partial repair-file cfg)}]]]]) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 658181635b..1cfaf09e05 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -153,7 +153,7 @@ (defn process-file! [system file-id update-fn - & {:keys [::snapshot-label ::validate? ::with-libraries?] + & {:keys [::profile-id ::snapshot-label ::validate? ::with-libraries?] :or {validate? true} :as opts}] (let [file (bfc/get-file system file-id :lock-for-update? true @@ -177,8 +177,9 @@ (when (string? snapshot-label) (fsnap/create! system file {:label snapshot-label + :profile-id profile-id :deleted-at (ct/in-future {:days 30}) - :created-by "admin"})) + :created-by "system"})) (let [file' (update file' :revn inc)] (bfc/update-file! system file' opts) diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 6745858f0e..fdc0caea1a 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -398,10 +398,6 @@ (println (sm/humanize-explain explain)) (ex/print-throwable cause)))))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; PROCESSING -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (defn repair-file! "Repair the list of errors detected by validation." [file-id & {:keys [rollback?] :or {rollback? true} :as options}] @@ -410,6 +406,10 @@ options (assoc options ::h/with-libraries? true)] (db/tx-run! system h/process-file! file-id procs.file-repair/repair-file options))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; PROCESSING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defn update-file! "Apply a function to the file. Optionally save the changes or not. The function receives the decoded and migrated file data."