diff --git a/weed/s3api/s3lifecycle/dailyrun/filer_list_func.go b/weed/s3api/s3lifecycle/dailyrun/filer_list_func.go index 39c5192c2..aa251f809 100644 --- a/weed/s3api/s3lifecycle/dailyrun/filer_list_func.go +++ b/weed/s3api/s3lifecycle/dailyrun/filer_list_func.go @@ -104,6 +104,7 @@ func walkBucketTree(ctx context.Context, client filer_pb.SeaweedFilerClient, dir ModTime: time.Unix(e.Attributes.Mtime, int64(e.Attributes.MtimeNs)), Size: int64(e.Attributes.FileSize), IsLatest: true, // Non-versioned default. + Tags: extractTags(e.Extended), } return cb(entry) }) @@ -183,7 +184,7 @@ func expandVersionsDir(ctx context.Context, client filer_pb.SeaweedFilerClient, // Resolve latest: // 1. Pointer names a real id -> that wins. // 2. Pointer absent (or stale: set but no sibling carries it) - // + items[0] is an EXPLICIT null -> null is latest. + // + any EXPLICIT null bare exists -> null is latest. // 3. Otherwise -> newest sibling (latestPos = 0 by default). // // A stale pointer falls through to the no-pointer fallback rather @@ -203,8 +204,13 @@ func expandVersionsDir(ctx context.Context, client filer_pb.SeaweedFilerClient, } } } - if !pointerResolved && len(items) > 0 && items[0].versionID == "null" && items[0].isExplicitNull { - latestPos = 0 + if !pointerResolved { + for i, it := range items { + if it.versionID == "null" && it.isExplicitNull { + latestPos = i + break + } + } } if start != "" && logical <= start { @@ -228,6 +234,7 @@ func expandVersionsDir(ctx context.Context, client filer_pb.SeaweedFilerClient, IsDeleteMarker: string(it.entry.Extended[s3_constants.ExtDeleteMarkerKey]) == "true", NumVersions: len(items), SuccessorModTime: successor, + Tags: extractTags(it.entry.Extended), } if !isLatest { rank := i @@ -318,3 +325,21 @@ func isMPUInitDir(key string, entry *filer_pb.Entry) bool { v, ok := entry.Extended[s3_constants.ExtMultipartObjectKey] return ok && len(v) > 0 } + +func extractTags(ext map[string][]byte) map[string]string { + if len(ext) == 0 { + return nil + } + prefix := s3_constants.AmzObjectTagging + "-" + var tags map[string]string + for k, v := range ext { + if !strings.HasPrefix(k, prefix) { + continue + } + if tags == nil { + tags = make(map[string]string) + } + tags[k[len(prefix):]] = string(v) + } + return tags +} diff --git a/weed/s3api/s3lifecycle/dailyrun/filer_list_func_test.go b/weed/s3api/s3lifecycle/dailyrun/filer_list_func_test.go index 47ec0514a..7796a5da6 100644 --- a/weed/s3api/s3lifecycle/dailyrun/filer_list_func_test.go +++ b/weed/s3api/s3lifecycle/dailyrun/filer_list_func_test.go @@ -126,6 +126,25 @@ func TestFilerListFunc_EmitsFlatFiles(t *testing.T) { assert.Equal(t, []string{"a.txt", "b.txt"}, got) } +func TestFilerListFunc_PropagatesTagsOnFlatFiles(t *testing.T) { + mtime := time.Now().Add(-7 * 24 * time.Hour) + tagged := fileWithExt("tagged.txt", mtime, 10, map[string][]byte{ + s3_constants.AmzObjectTagging + "-env": []byte("temp"), + s3_constants.AmzObjectTagging + "-tier": []byte("cold"), + }) + client := &fakeFiler{tree: map[string][]*filer_pb.Entry{ + "/buckets/bkt": {tagged}, + }} + listFn := FilerListFunc(client, "/buckets") + var got *bootstrap.Entry + require.NoError(t, listFn(context.Background(), "bkt", "", func(e *bootstrap.Entry) error { + got = e + return nil + })) + require.NotNil(t, got) + assert.Equal(t, map[string]string{"env": "temp", "tier": "cold"}, got.Tags) +} + func TestFilerListFunc_RecursesIntoSubdirs(t *testing.T) { mtime := time.Now() client := &fakeFiler{tree: map[string][]*filer_pb.Entry{ @@ -360,6 +379,40 @@ func TestFilerListFunc_VersionedExpansionExplicitNullIsLatestWhenPointerMissing( assert.Equal(t, 2, count) } +func TestFilerListFunc_VersionedExpansionExplicitNullBeatsNewerSiblingWhenPointerMissing(t *testing.T) { + // Suspended-versioning current-ness is carried by the explicit bare + // null, not by mtime ordering. Backdating the null to make it due + // must not demote it behind a newer noncurrent sibling when the + // .versions pointer is absent. + tNull := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // older + tV1 := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC) // newer + bareNull := fileWithExt("foo", tNull, 1, map[string][]byte{s3_constants.ExtVersionIdKey: []byte("null")}) + client := &fakeFiler{tree: map[string][]*filer_pb.Entry{ + "/buckets/bkt": { + bareNull, + versionsDir("foo"+s3_constants.VersionsFolder, ""), + }, + "/buckets/bkt/foo" + s3_constants.VersionsFolder: { + fileWithExt("v1", tV1, 1, map[string][]byte{s3_constants.ExtVersionIdKey: []byte("v1")}), + }, + }} + listFn := FilerListFunc(client, "/buckets") + var got []*bootstrap.Entry + require.NoError(t, listFn(context.Background(), "bkt", "", func(e *bootstrap.Entry) error { + got = append(got, e) + return nil + })) + require.Len(t, got, 2) + byID := map[string]*bootstrap.Entry{} + for _, e := range got { + byID[e.VersionID] = e + } + require.NotNil(t, byID["null"]) + require.NotNil(t, byID["v1"]) + assert.True(t, byID["null"].IsLatest, "explicit null must stay latest even when older than noncurrent siblings") + assert.False(t, byID["v1"].IsLatest) +} + func TestFilerListFunc_VersionsDirWithoutMarkersRecursesAsRegular(t *testing.T) { // A `.versions`-named folder whose children have no // ExtVersionIdKey is a coincidence (user folder). Recurse into