From 6feccaebb36beb4ff3218c264bca0aabc0ca21a8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 27 Aug 2026 10:28:59 +0000 Subject: [PATCH] :bug: Cascade profile deletion in objects-gc task The objects-gc task was performing a hard delete on profiles without cascading the soft-delete to owned teams, projects, and files. This left orphaned objects that were never cleaned up. Now the task invokes delete-object before the hard delete, ensuring all owned resources are properly marked for deletion and cleaned up in subsequent GC iterations. AI-assisted-by: qwen3.7-plus --- backend/src/app/tasks/objects_gc.clj | 6 ++++ .../test/backend_tests/rpc_profile_test.clj | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index ac457d47b0..02a9ac62f4 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -13,6 +13,7 @@ [app.db :as db] [app.features.fdata :as fdata] [app.storage :as sto] + [app.tasks.delete-object :as dobj] [integrant.core :as ig])) (def ^:private sql:get-profiles @@ -33,6 +34,11 @@ ;; Mark as deleted the storage object (some->> photo-id (sto/touch-object! storage)) + ;; Cascade soft-delete to owned teams, projects, files, etc. + (dobj/delete-object cfg {:object :profile + :id id + :deleted-at timestamp}) + (let [affected (-> (db/delete! conn :profile {:id id}) (db/get-update-count))] (+ total affected))) diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index de9ae0aacd..ab10c91f4c 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -424,6 +424,38 @@ (let [count-after (:count (th/db-exec-one! ["SELECT count(*) FROM http_session_v2 WHERE profile_id = ?" (:id prof)]))] (t/is (= 0 count-after))))) +(t/deftest profile-deletion-via-gc-cascades + (let [prof (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id prof) + :project-id (:default-project-id prof) + :is-shared false}) + team-id (:default-team-id prof) + project-id (:default-project-id prof) + file-id (:id file) + + deleted-at (ct/minus (ct/now) (ct/duration {:days 1}))] + + (th/db-update! :profile + {:deleted-at deleted-at} + {:id (:id prof)}) + + (let [team-before (th/db-get :team {:id team-id} {::db/remove-deleted false})] + (t/is (nil? (:deleted-at team-before)))) + + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (pos? (:processed result)))) + + (let [profile-after (th/db-get :profile {:id (:id prof)} {::db/remove-deleted false})] + (t/is (nil? profile-after))) + + (let [team-after (th/db-get :team {:id team-id} {::db/remove-deleted false})] + (t/is (nil? team-after))) + + (let [project-after (th/db-get :project {:id project-id} {::db/remove-deleted false})] + (t/is (nil? project-after))) + + (let [file-after (th/db-get :file {:id file-id} {::db/remove-deleted false})] + (t/is (nil? file-after))))) (t/deftest email-blacklist-1 (t/is (false? (email.blacklist/enabled? th/*system*)))