From 8cc3e299fcf0a42d112e1e29ac742a1039c1391f Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 23 Sep 2026 17:07:02 -0700 Subject: [PATCH] filer: compare IF_ENTRY_EQUAL against the normalized expected entry FindEntry grows FileSize to the chunk extent, so a raw event entry with FileSize still zero failed the condition on an unchanged file and the stamp was skipped, letting a replay upload the object again. --- weed/server/filer_grpc_server_condition.go | 9 ++++-- .../filer_grpc_server_condition_test.go | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/weed/server/filer_grpc_server_condition.go b/weed/server/filer_grpc_server_condition.go index 7addf586c..f51b0dcef 100644 --- a/weed/server/filer_grpc_server_condition.go +++ b/weed/server/filer_grpc_server_condition.go @@ -84,10 +84,13 @@ func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bo case filer_pb.WriteCondition_IF_CHUNKS_EQUAL: return chunkFidsEqual(current, c.Fids) case filer_pb.WriteCondition_IF_ENTRY_EQUAL: - if !exists { - return c.ExpectedEntry == nil + if !exists || c.ExpectedEntry == nil { + return !exists && c.ExpectedEntry == nil } - return proto.Equal(current.ToProtoEntry(), c.ExpectedEntry) + // Normalize the expected entry the way FindEntry normalizes the stored + // one (e.g. FileSize grows to the chunk extent), or an unchanged entry + // can compare unequal. + return proto.Equal(current.ToProtoEntry(), filer.FromPbEntry("", c.ExpectedEntry).ToProtoEntry()) default: // An unrecognized clause kind (e.g. from a newer client) must not be // treated as satisfied, which would silently bypass the guard. Fail diff --git a/weed/server/filer_grpc_server_condition_test.go b/weed/server/filer_grpc_server_condition_test.go index 66906fafb..2946e9614 100644 --- a/weed/server/filer_grpc_server_condition_test.go +++ b/weed/server/filer_grpc_server_condition_test.go @@ -193,6 +193,34 @@ func TestWriteConditionUnknownKindFailsClosed(t *testing.T) { } } +// IF_ENTRY_EQUAL compares the expected entry after the same normalization +// FindEntry applied to the stored one: a raw event entry whose FileSize is +// still zero must match a stored entry grown to its chunk extent. +func TestIfEntryEqualNormalizesExpected(t *testing.T) { + raw := &filer_pb.Entry{ + Name: "f", + Attributes: &filer_pb.FuseAttributes{Mtime: 42}, + Chunks: []*filer_pb.FileChunk{ + {Fid: &filer_pb.FileId{VolumeId: 3, FileKey: 1, Cookie: 2}, Size: 100}, + }, + } + stored := filer.FromPbEntry("/d", raw) + if stored.FileSize != 100 { + t.Fatalf("stored FileSize = %d, want chunk extent 100", stored.FileSize) + } + cond := one(&filer_pb.WriteCondition_Clause{ + Kind: filer_pb.WriteCondition_IF_ENTRY_EQUAL, + ExpectedEntry: raw, + }) + if !writeConditionSatisfied(cond, stored) { + t.Error("raw expected entry must equal its normalized stored form") + } + raw.Attributes.Mtime = 43 + if writeConditionSatisfied(cond, stored) { + t.Error("changed expected entry must not equal the stored entry") + } +} + // storedEntryETag prefers the stored Seaweed ETag attribute and falls back to // the Md5-derived ETag, matching the S3 gateway. func TestStoredEntryETag(t *testing.T) {