🐛 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
This commit is contained in:
alonso.torres 2026-10-16 10:48:03 +02:00
parent 68bafacc9d
commit 0bffa600d8
3 changed files with 39 additions and 14 deletions

View File

@ -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

View File

@ -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

View File

@ -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