mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-24 23:10:41 +02:00
feat(s3/lifecycle): classify versioned events by storage path (Phase 5b/1) (#9373)
* feat(s3/lifecycle/router): classify versioned events by storage path Phase 5b first slice. Pass the bucket's Versioned flag from the engine snapshot into buildObjectInfo and: - Recognize <key>.versions/<vid> events as noncurrent versions. IsLatest=false, info.Key strips the .versions/<vid> suffix so a rule's Filter.Prefix matches the user's logical key, and the AWS-visible version_id rides on Match.VersionID for the dispatcher to target a single version on the server. - Read IsDeleteMarker from Extended unconditionally — the engine rejects ExpiredObjectDeleteMarker when NumVersions != 1, so without sibling listing the marker case stays correctly suppressed (a separate PR will add the listing). - Non-versioned buckets keep the existing behavior even when an object literally named "*.versions/v1" exists; Versioned=false short-circuits the path classification. Time-based NoncurrentDays now fires on noncurrent events. NewerNoncurrent and ExpiredObjectDeleteMarker still need sibling listing — left for a follow-up. * fix(s3/lifecycle/router): require ExtVersionIdKey to confirm noncurrent Path classification alone misclassifies a literal-key collision: a versioned bucket holding an object with key "logs/backup.versions/2023" would be flagged noncurrent and have its key stripped to "logs/backup", losing the user's actual rule-prefix-matching path. SeaweedFS doesn't reserve the .versions/ segment, so the path shape is necessary but not sufficient. Add an authoritative confirmation: the entry must declare the same version_id via ExtVersionIdKey (the field SeaweedFS sets when storing a tracked version). Also reject idx==0 paths so ".versions/<vid>" can't yield an empty logical key. Tests: - collision: versioned bucket + .versions/ in literal key + no metadata (and the mismatched-vid variant) → still classified as a current-version object; - root-versions: .versions/v1 (idx==0) → treated as a regular key; - existing noncurrent test now sets ExtVersionIdKey to mirror the storage shape. * fix(s3/lifecycle/router): skip versioned-bucket version-folder events The previous attempt tried to classify <key>.versions/<vid> events as noncurrent versions by storage path. That's broken on three counts: - SeaweedFS stores version files as v_<id> (getVersionFileName), so comparing the path suffix to the raw ExtVersionIdKey never matches. - The "current latest" version on a versioned bucket lives at the same .versions/v_<id> path shape as noncurrent versions; the latest pointer is on the parent .versions/ directory's Extended[ExtLatestVersionIdKey], which the router doesn't see. - Even with a correct vid match, IsLatest=false plus the storage path as ObjectKey would have the dispatcher recompose <storagepath>.versions/v_<id> and no-op (or worse, target the wrong file). Until we route from .versions/ directory pointer-transition events (or supply IsLatest/SuccessorModTime/index from sibling listing), skip every event under a *.versions/ folder. Bare-key events (null versions) still route normally; bootstrap walking covers the versioned-storage cases. Tests assert the skip across tracked, literal-collision, and bucket-root .versions paths. * feat(s3api): refuse noncurrent-kind delete on the current latest version Defense-in-depth for the noncurrent kinds: even when bootstrap (or a future event-driven path) thinks a version is noncurrent, the server must verify against the .versions/ directory's Extended[ExtLatestVersionIdKey] before deleting. If the target version matches the latest pointer the action is silently dropped as NOOP_RESOLVED:VERSION_IS_LATEST instead of deleting the live data. * refactor(s3/lifecycle): tidy versioning gates per review - router: skip directory entries (other than MPU init) in buildObjectInfo so .versions/ folder events never become ObjectInfo. Subtest "versions dir itself" added. - s3api: switch isCurrentLatestVersion's path split from filepath.Split (OS-dependent) to path.Split so filer paths always use '/'.
This commit is contained in:
1 parent
935fb42e1d
commit
159cfc97ce
3 files changed
+273
-12
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -102,6 +103,23 @@ func (s3a *S3ApiServer) lifecycleDispatch(ctx context.Context, req *s3_lifecycle
|
||||
if req.VersionId == "" {
|
||||
return blocked("FATAL_EVENT_ERROR: version_id required for noncurrent / delete-marker delete"), nil
|
||||
}
|
||||
// Latest-pointer guard for noncurrent kinds: refuse to delete
|
||||
// the version that the .versions/ directory currently points
|
||||
// to. The router can't always tell current from noncurrent
|
||||
// without sibling state, so the server checks here.
|
||||
if req.ActionKind == s3_lifecycle_pb.ActionKind_NONCURRENT_DAYS ||
|
||||
req.ActionKind == s3_lifecycle_pb.ActionKind_NEWER_NONCURRENT {
|
||||
isLatest, lookupErr := s3a.isCurrentLatestVersion(req.Bucket, req.ObjectPath, req.VersionId)
|
||||
if lookupErr != nil {
|
||||
if errors.Is(lookupErr, filer_pb.ErrNotFound) || errors.Is(lookupErr, ErrObjectNotFound) {
|
||||
return noopResolved("NOT_FOUND"), nil
|
||||
}
|
||||
return retryLater("TRANSPORT_ERROR: latest-pointer lookup: " + lookupErr.Error()), nil
|
||||
}
|
||||
if isLatest {
|
||||
return noopResolved("VERSION_IS_LATEST"), nil
|
||||
}
|
||||
}
|
||||
if err := s3a.deleteSpecificObjectVersion(req.Bucket, req.ObjectPath, req.VersionId); err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) || errors.Is(err, ErrVersionNotFound) || errors.Is(err, ErrObjectNotFound) {
|
||||
return noopResolved("NOT_FOUND_AT_DELETE"), nil
|
||||
@@ -157,6 +175,34 @@ func (s3a *S3ApiServer) lifecycleAbortMPU(ctx context.Context, req *s3_lifecycle
|
||||
return done(), nil
|
||||
}
|
||||
|
||||
// isCurrentLatestVersion reports whether versionId is the version the
|
||||
// .versions/ directory currently points to. SeaweedFS records the latest
|
||||
// version on the parent directory's Extended map; without consulting it,
|
||||
// a noncurrent-kind dispatch can't safely distinguish current from
|
||||
// noncurrent and would risk deleting the live version. Returns
|
||||
// (false, nil) when the directory has no latest pointer (e.g., the
|
||||
// bucket isn't versioned in this object's history).
|
||||
func (s3a *S3ApiServer) isCurrentLatestVersion(bucket, object, versionId string) (bool, error) {
|
||||
versionsDir := s3a.bucketDir(bucket) + "/" + object + s3_constants.VersionsFolder
|
||||
parent, name := path.Split(versionsDir)
|
||||
parent = strings.TrimRight(parent, "/")
|
||||
if parent == "" {
|
||||
parent = "/"
|
||||
}
|
||||
entry, err := s3a.getEntry(parent, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if entry == nil || len(entry.Extended) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
latest, ok := entry.Extended[s3_constants.ExtLatestVersionIdKey]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
return string(latest) == versionId, nil
|
||||
}
|
||||
|
||||
// computeEntryIdentity captures (mtime, size, head fid, sorted-Extended hash):
|
||||
// an overwrite changes mtime/size/fid; a metadata edit changes Extended; a
|
||||
// snapshot-restore that preserves mtime+size still differs in head_fid.
|
||||
|
||||
Reference in new issue
Block a user