Deleting a project takes its documentation with it (#164) #166
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
qwc-open/asiakirjat!166
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/delete-project-removes-storage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #164. Found while investigating #163; independent of PR #165 and mergeable in either order.
What was wrong
handleAdminDeleteProjectremoved the search index entries and the database row, and stopped there. Nothing in that path touched storage — andStoragehad no way to remove a project at all, only a version.So every project ever deleted has left its entire directory under
storage.base_pathbehind, versions and all. Nothing reads those directories again and nothing reports them, so they only accumulate. Retention cannot reclaim them either, since it walks projects that still exist.The sharper problem is reuse: a slug deleted and later recreated inherits the old directory, so files from the previous project can be served under the new one wherever a version tag happens to match.
The fix
FilesystemStorage.DeleteProject, called from the delete handler after the row is gone.That order is deliberate. A project whose row survived while its files vanished would be broken; a directory that resists removal is exactly the state we already had. So a storage failure is logged, not fatal — the delete the user asked for has happened either way.
It runs under the same per-project lock a rename takes. An upload extracting into that directory while it is being removed would leave half a project on disk, which is the very thing this is meant to prevent.
The guard on RemoveAll
ProjectPathjoins the base path with the slug andfilepath.Joincleans the result, so a slug that is empty or climbs upwards resolves to the base path or above it — andRemoveAllwould take every other project with it. Only a direct child of the base path may be removed.TestDeleteProjectRefusesToEscapeStoragewalks"",".","..","../..","other/nested"and"/", asserting for each that the call is refused and that both a neighbouring project and the storage base path survive.Tests
TestDeleteProject— a two-version tree goes, a neighbouring project stays.TestDeleteProjectMissingDirIsSuccess— unlikeMoveProject, nothing is lost deleting a project that was never deployed to, and an error there would fail a delete that already happened in the database.TestAdminDeleteProjectRemovesStorage— drives the real route end to end.Both new assertions were confirmed to fail against the old code first:
Notes for review
ReconcileStorageat least report directories with no project; I have not done that here since it is a separate decision about whether to touch data nobody asked us to touch.manage-projectspage and I will write one.storage.base_pathfor directories with no matching project: on the instance in #163 there are at least three, from the deleted projects that caused that whole failure.🤖 Written with Claude Code