Skip to content

Harden dataset storage cleanup - #12690

Open
ErykKul wants to merge 7 commits into
developfrom
harden-dataset-clean-storage
Open

Harden dataset storage cleanup#12690
ErykKul wants to merge 7 commits into
developfrom
harden-dataset-clean-storage

Conversation

@ErykKul

@ErykKul ErykKul commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What this PR does / why we need it:

cleanStorage deletes storage objects that are not referenced by any file in the dataset. Three problems with how it does that.

It was a GET, so anything that follows a link could trigger a deletion: browser prefetch, link previews in chat and mail clients, crawlers, or opening the URL again from history. It is now a PUT.

dryrun defaulted to false, so omitting the parameter deleted. It now defaults to true, and deleting requires dryrun=false.

It could delete real files. An upload is written to the dataset's storage location before Dataverse registers a DataFile for it, so an upload that had finished but was still waiting to be saved looked exactly like an abandoned one and was removed. cleanUp now takes a minimum age and skips anything modified more recently. The default is 7 days, configurable with dataverse.files.clean-storage-min-age-days.

The permission failure also returned 500 instead of 403.

Which issue(s) this PR closes:

Special notes for your reviewer:

The age check costs no extra calls. The S3 listing already retrieved lastModified for each object and discarded it, and the filesystem listing gets it from the directory walk.

An unknown timestamp counts as too recent, so a backend that cannot report one never causes a deletion. That is why the Swift lookup is wrapped.

getToDeleteFilesFilter stays a name-only predicate, so its existing test is unchanged. The age check lives in the storage layer, where the timestamps already are.

InputStreamIO and AbstractRemoteOverlayAccessIO only needed the new signature.

This changes the API. Callers have to switch to PUT, and any that relied on the old default to delete have to pass dryrun=false.

Suggestions on how to test this:

  1. Run StorageIOCleanUpAgeTest and DatasetsTest.
  2. Upload a file to a dataset with direct upload and leave it unsaved. Call PUT /api/datasets/{id}/cleanStorage?dryrun=true and confirm the unsaved upload is not listed.
  3. Call it without dryrun and confirm the response reports files without deleting them.
  4. Confirm a user without EditDataset on the dataset gets 403.
  5. With dataverse.files.clean-storage-min-age-days=0, confirm orphaned files are reported and then deleted with dryrun=false.

Does this PR introduce a user interface change? If mockups are available, please link/include them here:

No.

Is there a release notes update needed for this change?:

Yes, included: doc/release-notes/harden-clean-storage.md.

Additional documentation:

Native API guide updated for the new method, the dryrun default, and the grace period.

cleanStorage was a GET that deleted by default, so anything following links
could trigger it: browser prefetch, link previews, crawlers, or revisiting the
URL from history. It is now a PUT, and dryrun defaults to true so omitting the
parameter reports instead of deleting.

It could also delete real files. Direct upload writes its object to the final
storage key before Dataverse registers a DataFile for it, so an upload that had
finished but was still waiting to be saved looked exactly like an abandoned one.
cleanUp now takes a minimum age and skips anything modified more recently,
configurable through dataverse.files.clean-storage-min-age-days, default 7.

The listing already retrieved modification times from S3 and discarded them, so
the age check costs no extra calls. An unknown timestamp counts as too recent,
so a backend that cannot report one never causes a deletion.

getToDeleteFilesFilter stays a name-only predicate, leaving its existing test
unchanged.
@github-actions github-actions Bot added the Type: Bug a defect label Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Test Results

405 tests  ±0   390 ✅ ±0   33m 21s ⏱️ +6s
 55 suites ±0    15 💤 ±0 
 55 files   ±0     0 ❌ ±0 

Results for commit 6fc04c9. ± Comparison against base commit dca906f.

♻️ This comment has been updated with latest results.

getAuditFiles calls cleanUp purely to list what is in storage, with a
match-everything filter and dryRun set. Duration.ZERO keeps that listing
complete, which is the behaviour it had before.
@ErykKul ErykKul moved this to Ready for Triage in IQSS Dataverse Project Sep 10, 2026
@ErykKul ErykKul added the Size: 3 A percentage of a sprint. 2.1 hours. label Sep 10, 2026
@coveralls

coveralls commented Sep 10, 2026

Copy link
Copy Markdown

Coverage Status

Coverage is 25.469%harden-dataset-clean-storage into develop. No base build found for develop.

@github-actions

This comment has been minimized.

Sonar failed the gate on duplication and coverage, not on code smells.

The three backends had the same filter-and-age pipeline inline, so it moves
to StorageIO.selectForCleanUp. That leaves one definition of the selection
rule, removes the duplicated block, and makes the part worth testing
reachable without a storage backend. Five tests cover it.

Also returns an unmodifiable list, drops the two imports that became unused,
and removes the unread local in the audit endpoint.
@github-actions

This comment has been minimized.

FileAccessIOTest already stages files under the default root, so cleanUp can
be exercised end to end there: an old orphan is removed, a recently modified
one is not, a dry run deletes nothing, and the filter still protects
referenced files.

That covers the age guard on the backend most installations use. The Swift
and S3 equivalents would need test seams in production code, and the resource
method needs the container, so both stay with the integration tests.
S3AccessIO already takes an injected client, so its cleanup needs no
production change: a mocked listing with per-object timestamps drives it.

Swift has no such seam, so swiftContainer drops private to package-private
and the test supplies a mocked container. Setting isWriteAccess directly
keeps open() out of the way in both.

That leaves the resource method and the overlay delegation uncovered, both
of which need the container to reach.
@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

swiftContainer goes back to private and its test comes out. The seam existed
only to move the coverage number, and a production visibility change is not
worth carrying for that.
Adding selectForCleanUp put it between isOlderThan's javadoc and the method,
so the doc described the wrong one.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
53.7% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

Copy link
Copy Markdown

📦 Pushed preview images as

ghcr.io/gdcc/dataverse:harden-dataset-clean-storage
ghcr.io/gdcc/configbaker:harden-dataset-clean-storage

🚢 See on GHCR. Use by referencing with full name as printed above, mind the registry name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Size: 3 A percentage of a sprint. 2.1 hours. Type: Bug a defect

Projects

Status: Ready for Triage

Development

Successfully merging this pull request may close these issues.

cleanStorage can delete an upload that is still in progress

2 participants