Files
rclone/cmd/serve/s3/list.go
T
Nick Craig-Wood 84298fc090 serve s3: fix failed uploads deleting or corrupting the object at the key - fixes #9718
A PUT which failed part way through removed the object at the
destination key. As well as differing from real S3 (where a failed PUT
never affects the stored object), this raced with the client's
automatic retry of the same PUT: the retry stored the object and
returned 200 OK, then the failed first attempt's cleanup deleted it,
silently losing an acknowledged upload. The interrupted upload could
also be committed as a truncated object, since closing the write handle
gave the streaming upload a clean end of stream.

Now a failed or interrupted PUT never disturbs the object at the key:

- The object at the key is never removed on error.
- On backends where a partial upload is visible at its final name
  (PartialUploads), and when the VFS cache mode is writes or above, the
  upload is written to a temporary object which is renamed into place
  on success and removed on failure, as streamed multipart uploads
  already do. Backends which upload atomically are still streamed
  straight to the destination.
- An interrupted or short body fails the upload via
  WriteFileHandle.CloseWithError instead of committing truncated data,
  and a body which ends cleanly short of its declared size is rejected
  with IncompleteBody.
2026-08-11 20:58:48 +01:00

61 lines
1.4 KiB
Go

package s3
import (
"path"
"strings"
"github.com/rclone/gofakes3"
"github.com/rclone/rclone/vfs"
)
func (b *s3Backend) entryListR(_vfs *vfs.VFS, bucketName, fdPath, name string, addPrefix bool, response *gofakes3.ObjectList) error {
fp, err := bucketDirPath(bucketName, fdPath)
if err != nil {
// A listing prefix that can't be represented as a path matches nothing.
return gofakes3.ErrNoSuchKey
}
dirEntries, err := getDirEntries(fp, _vfs)
if err != nil {
return err
}
for _, entry := range dirEntries {
object := entry.Name()
// Hide the temporary objects of in-progress uploads
if strings.HasPrefix(object, multipartUploadPrefix) || strings.HasPrefix(object, putObjectPrefix) {
continue
}
// workaround for control-chars detect
objectPath := path.Join(fdPath, object)
if !strings.HasPrefix(object, name) {
continue
}
if entry.IsDir() {
if addPrefix {
prefixWithTrailingSlash := objectPath + "/"
response.AddPrefix(prefixWithTrailingSlash)
continue
}
err := b.entryListR(_vfs, bucketName, path.Join(fdPath, object), "", false, response)
if err != nil {
return err
}
} else {
item := &gofakes3.Content{
Key: objectPath,
LastModified: gofakes3.NewContentTime(entry.ModTime()),
ETag: getFileHash(entry, b.s.etagHashType),
Size: entry.Size(),
StorageClass: gofakes3.StorageStandard,
}
response.Add(item)
}
}
return nil
}