From 0bffa600d80d693a8eabe654f779d893eeca641a Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 16 Oct 2026 10:48:03 +0200 Subject: [PATCH] :bug: Absorb a deleted library into the files that used it Deleting a shared library removes its links to the files using it at once, so they stop listing it, and queues the delete-object job, which absorbs the library into the files it finds through those links. By then there were none, so nothing was absorbed: the files lost the library and their copies kept pointing at the deleted file (PENPOT-1092). Unpublishing absorbs before it unlinks, so it was not affected. The delete reads the files using the library before unlinking them and gives them to the job (:absorb-file-ids), which absorbs into them; without them, as from the project and team deletions, the job still finds them through the links. rpc-file-test/delete-shared-library-absorbs-it-into-the-files-using-it deletes the library through the RPC and runs the job it queued. AI-assisted-by: claude-opus-5-5 --- backend/src/app/rpc/commands/files.clj | 35 ++++++++++++++------ backend/src/app/tasks/delete_object.clj | 8 +++-- backend/test/backend_tests/rpc_file_test.clj | 10 ++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index e8f5fd49b2..25ec6f08d0 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -1004,16 +1004,21 @@ :modified-at (ct/now) :has-media-trimmed false})))) +(defn- get-referenced-file-ids + "The ids of the files that use the library `id`." + [cfg id] + (->> (db/exec! cfg [sql:get-referenced-files id]) + (into [] bfc/xf-map-id))) + (defn- absorb-library* - "Find all files using a shared library, and absorb all library assets - into the file local libraries" - [cfg {:keys [id data] :as library}] + "Absorb all library assets into the local libraries of the files + `file-ids`, or, without them, of all the files using it." + [cfg {:keys [id data] :as library} file-ids] (assert (db/connection-map? cfg) "expected cfg with valid connection") - (let [ids (->> (db/exec! cfg [sql:get-referenced-files id]) - (sequence bfc/xf-map-id))] + (let [ids (or file-ids (get-referenced-file-ids cfg id))] (l/trc :hint "absorbing library" :library-id (str id) @@ -1023,7 +1028,10 @@ library)) (defn absorb-library - [{:keys [::db/conn] :as cfg} id] + "Absorb the assets of the library `id` into the files using it, or into + the files `file-ids` when given (the users of a library that is already + unlinked from them)." + [{:keys [::db/conn] :as cfg} id & [file-ids]] (let [file (-> (bfc/get-file cfg id :realize? true :lock-for-update? true @@ -1039,7 +1047,7 @@ (-> (cfeat/get-team-enabled-features cf/flags team) (cfeat/check-file-features! (:features file))) - (absorb-library* cfg file))) + (absorb-library* cfg file file-ids))) (defn- set-file-shared [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] @@ -1112,7 +1120,10 @@ {:deleted-at (ct/in-future delay)} {:id file-id} {::db/return-keys [:id :name :is-shared :deleted-at - :project-id :created-at :modified-at]})] + :project-id :created-at :modified-at]}) + ;; read before the relations go, for the job that absorbs the library + users (when (:is-shared file) + (get-referenced-file-ids cfg file-id))] ;; Remove all possible relations for that file (db/delete! conn :file-library-rel @@ -1120,9 +1131,11 @@ (jobs/submit cfg {::jobs/name :delete-object - ::jobs/params {:object :file - :deleted-at (:deleted-at file) - :id file-id}}) + ::jobs/params (cond-> {:object :file + :deleted-at (:deleted-at file) + :id file-id} + (seq users) + (assoc :absorb-file-ids users))}) file)) (def ^:private diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index 730a2364c9..338fec7d87 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -39,7 +39,7 @@ {::db/return-keys false})) (defmethod delete-object :file - [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] + [{:keys [::db/conn] :as cfg} {:keys [id deleted-at absorb-file-ids]}] (when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false ::sql/columns [:id :is-shared]})] @@ -57,7 +57,7 @@ (not *team-deletion*)) ;; NOTE: we don't prevent file deletion on absorb operation failure (try - (db/tx-run! cfg files/absorb-library id) + (db/tx-run! cfg files/absorb-library id absorb-file-ids) (catch Throwable cause (l/warn :hint "error on absorbing library" :file-id id @@ -156,7 +156,9 @@ [:object [:enum :snapshot :team :project :profile :file]] [:deleted-at ::ct/inst] [:id ::sm/uuid] - [:file-id {:optional true} ::sm/uuid]]) + [:file-id {:optional true} ::sm/uuid] + ;; the users of a deleted library, already unlinked from it + [:absorb-file-ids {:optional true} [:vector ::sm/uuid]]]) (defn execute-delete-object "Plain job handler: run the delete-object multimethod on the provided diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index a53b194b41..fef66cd25d 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -2214,6 +2214,16 @@ :is-shared false}))) (check-absorbed! setup))) +(t/deftest delete-shared-library-absorbs-it-into-the-files-using-it + ;; Deleting the file unlinks it from its users at once; the job that + ;; follows must still find them and give them the components they use. + (let [{:keys [profile library] :as setup} (create-library-copy!)] + (t/is (th/success? (th/command! {::th/type :delete-file + ::rpc/profile-id (:id profile) + :id (:id library)}))) + (th/run-pending-jobs) + (check-absorbed! setup))) + (t/deftest delete-shared-library-with-storage-file-data (binding [cf/config (assoc cf/config :file-data-backend "storage")] ;; Keep this test focused on the storage-backed file-data fix: create