mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 06:50:45 +01:00
S3: let a key that is a prefix of other keys be an object (#10912)
* filer: keep the sentinel when CreateEntry reports an update failure CreateEntry flattened the error UpdateEntry wraps, so errors.Is stopped matching and ErrExistingIsDirectory and ErrExistingIsFile never reached the S3 mapper, which answered a retryable 500 instead. * s3: let a key that is a prefix of other keys be an object S3 keys are flat, so "a/b" and "a/b/c" are independent objects that coexist in either write order. The filer stores a key as a path, so one of them has to live on the directory the other is nested under. Writing the nested key first refused the prefix key outright. Writing it second promoted the file to a directory, which kept its data but lost the key: an empty object left nothing to recognise it by and disappeared, and one with data listed under a trailing slash it never had. Mark the directory that carries such a key, and write the object onto it when the path is already a directory. The mark makes an empty prefix object visible to listings and readable by GET and HEAD, keeps the empty folder cleaner off it, and lists it under the key it was written with. Deleting the key strips the mark back off along with the data. * filer: keep a TTL off a directory that stands for an object An expired entry is deleted a row at a time, so expiring a directory removes it and leaves everything under it unreachable. Promoting a file to a directory carried its TTL across, and a promoted file is exactly the one that has keys nested under it. Drop the TTL on promotion, and leave one an older build wrote alone. The lifecycle worker still expires the object, through the delete that leaves the directory behind. * s3: delete the null version of a key other keys are nested under The routed delete cannot remove an entry that other keys live under, and answered a retryable 500 rather than falling back to the lock path the unversioned delete already falls back to. That path then looked the entry up under the bucket with the whole key as its name, so the demote wrote it back one directory too high and failed as not found. Fall back on any non-precondition error, and split the key before deleting it. Trailing-slash directory markers with children reach the same delete. * filer: keep the sentinel when MkFile and Mkdir report a create failure Same flattening one layer out: every mkFile caller lost the sentinel, so a CopyObject onto a key that other keys are nested under answered a retryable 500 where a PutObject of the same key answers 409. * s3: copy and rename a key that other keys are nested under Such a key is stored on the directory those keys live in, and copy and rename both refused it: the source lookup maps every directory entry to NoSuchKey, so a key a plain GET serves could not be copied or moved, and the destination side refused it as a directory conflict. The source is read through a view of the entry as the object it names. The destination is written the way a PutObject of that key writes it. A rename at either end copies the object's own data across and strips it off the source key rather than going through AtomicRenameEntry, which moves a directory by moving everything under it - the nested keys are not part of what is being renamed.
This commit is contained in:
1 parent
46ce2c45a2
commit
863fec6c3f
20 files changed
+981
-39
No files matched your search
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
s3_constants "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
@@ -856,6 +857,13 @@ func (vc *versionCollector) collectVersions(currentPath, relativePath string) er
|
||||
vc.commonPrefixes[commonPrefix] = true
|
||||
}
|
||||
|
||||
// The prefix rolled up here belongs to the keys nested under this
|
||||
// entry. A prefix object is also a key of its own, and that key
|
||||
// carries no trailing slash, so it does not roll up with them.
|
||||
if entry.IsPrefixObject() {
|
||||
vc.processPrefixObject(currentPath, entryPath, entry)
|
||||
}
|
||||
|
||||
// Skip further processing (recursion or addition) for this entry
|
||||
// because it has been rolled up into the CommonPrefix
|
||||
continue
|
||||
@@ -879,13 +887,31 @@ func (vc *versionCollector) collectVersions(currentPath, relativePath string) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPrefixObject emits the null version of a key that other keys are nested
|
||||
// under. The key carries no trailing slash, so it is a null object like any other and
|
||||
// needs the same reconciliation against a .versions sibling - but the directory it is
|
||||
// stored on is walked as an ancestor of the requested prefix, which its own key does
|
||||
// not have to match.
|
||||
func (vc *versionCollector) processPrefixObject(currentPath, entryPath string, entry *filer_pb.Entry) {
|
||||
if !strings.HasPrefix(entryPath, vc.prefix) {
|
||||
return
|
||||
}
|
||||
if vc.delimiter != "" && strings.Contains(entryPath[len(vc.prefix):], vc.delimiter) {
|
||||
// The key folds into the same CommonPrefix its nested keys do.
|
||||
return
|
||||
}
|
||||
vc.processRegularFile(currentPath, entryPath, entry)
|
||||
}
|
||||
|
||||
// processDirectory handles directory entries
|
||||
func (vc *versionCollector) processDirectory(currentPath, entryPath string, entry *filer_pb.Entry) error {
|
||||
// Handle explicit S3 directory object. Match ListObjectsV2's
|
||||
// IsDirectoryKeyObject (any non-empty mime), not just FolderMimeType:
|
||||
// an SDK PutObject of "dir/" carries a default Content-Type, so the two
|
||||
// listings must agree on what counts as a directory key.
|
||||
if entry.IsDirectoryKeyObject() {
|
||||
if entry.IsPrefixObject() {
|
||||
vc.processPrefixObject(currentPath, entryPath, entry)
|
||||
} else if entry.IsDirectoryKeyObject() {
|
||||
vc.processExplicitDirectory(entryPath, entry)
|
||||
}
|
||||
|
||||
@@ -1113,8 +1139,11 @@ func (s3a *S3ApiServer) deleteSpecificObjectVersion(ctx context.Context, bucket,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete the regular file
|
||||
deleteErr := s3a.rmObject(bucketDir, normalizedObject, !metadataOnly, false)
|
||||
// Delete the regular file. rmObject takes a parent and a name, and the demote
|
||||
// it falls back to for an entry other keys are nested under writes the entry
|
||||
// back under that parent - so a key with a slash in it has to be split first.
|
||||
dir, name := util.NewFullPath(bucketDir, normalizedObject).DirAndName()
|
||||
deleteErr := s3a.rmObject(dir, name, !metadataOnly, false)
|
||||
if deleteErr != nil {
|
||||
// Check if file was already deleted by another process
|
||||
if _, checkErr := s3a.getEntry(bucketDir, normalizedObject); checkErr != nil {
|
||||
|
||||
Reference in new issue
Block a user