Make remote nuke delete leftover .partial uploads (closes #281)
check / check (push) Waiting to run

Every listing skips an object whose name ends in `.partial`, the
temporary name a `file://` or rclone upload writes before moving the
object into place. `remote nuke` deletes only what the listings return,
so it left the `.partial` objects killed uploads leave behind and still
reported the destination store empty.

Storer gains DeletePartialUploads. The file and rclone backends remove
every `.partial` object under the prefix; S3 has none to remove, since
it shows an object only once its upload completes. `remote nuke` calls
it for `metadata/` and `blobs/` as its last step.

Empty directories under a `file://` destination are still left behind.

Model: opus-5-5
This commit is contained in:
2026-10-08 07:12:38 +00:00
parent 0d0368df81
commit d6752fc252
13 changed files with 244 additions and 2 deletions
+36
View File
@@ -242,6 +242,42 @@ func (f *FileStorer) ListStream(ctx context.Context, prefix string) <-chan Objec
return ch
}
// DeletePartialUploads removes every file under prefix whose name ends in
// tempSuffix. A missing prefix has none to remove.
func (f *FileStorer) DeletePartialUploads(ctx context.Context, prefix string) error {
basePath := f.fullPath(prefix)
exists, err := afero.Exists(f.fs, basePath)
if err != nil {
return fmt.Errorf("checking path: %w", err)
}
if !exists {
return nil
}
err = afero.Walk(f.fs, basePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
if info.IsDir() || !strings.HasSuffix(info.Name(), tempSuffix) {
return nil
}
return f.fs.Remove(path)
})
if err != nil {
return fmt.Errorf("deleting partial uploads: %w", err)
}
return nil
}
// Info returns human-readable storage location information.
func (f *FileStorer) Info() Info {
return Info{