mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 17:00:48 +01:00
* filer_pb,filer: per-mutation preconditions on ObjectTransaction A transaction's request-level condition evaluates one entry before any mutation runs, so a commit guarding two paths — the object it publishes and the upload directory it assembles — could not be expressed. Give each ObjectMutation an optional condition (evaluated against condition_key, else the mutation's target, under the same lock just before the mutation applies) and report which mutation failed through failed_mutation on the response, so callers can tell a mutation guard's rejection apart from the request-level precondition's. * s3: evaluate multipart completion conditions under the object lock CompleteMultipartUpload checked conditional headers gateway-side before committing, and the routed commit transaction guarded only the upload directory — nothing made If-None-Match: * atomic with the publish, so concurrent conditional completions of the same absent key could all report success. The request's reducible write condition (If-None-Match/If-Match, star or single strong ETag) now rides the commit as the transaction's request condition on the owner filer: on the object path for unversioned buckets, falling back to the .versions directory for suspended and enabled versioning. The upload-exists guard moves onto the PUT mutation, keeping its check-before-publish ordering. Conditions the reducer cannot express stay on the distributed write lock, where check and commit already serialize. * filer,s3: resolve the latest version first for versioned conditional writes * filer: keep directory entries holding content out of the absent-slot rule * s3: keep a gateway-side upload-exists check for filers without mutation conditions * filer: honor null-version-is-latest resolving prefer-alt conditions * drop scratch multipart-race repro programs * s3: note the write lock does not cover routed writes on non-reducible conditions * filer: mirror the gateway's remote-etag precedence in condition evaluation storedEntryETag only knew the extended attribute and the chunk fallback, so a synced remote entry compared chunk-derived ETags while the gateway compares the remote ETag. Follow the same precedence: extended attribute, remote ETag while the entry still holds the remote bytes, Md5, then the chunk hash — and no ETag for an entry holding none. * s3: reduce all conditional write headers into owner-lock clauses Time-based headers, combined conditions, ETag lists, and weak ETags stayed on the distributed write lock because buildWriteCondition could not express them, leaving their gateway check racing owner-routed writes. Reduce every parseable header form to WriteCondition clauses — multiple clauses compose AND, ETag candidates carry the raw list for the filer's canonicalization, and RFC 7232 precedence keeps If-Unmodified-Since hidden by If-Match and If-Modified-Since hidden by If-None-Match — so the owner filer evaluates them under the same object-path lock as the commit. Only unparseable headers still fall back, where the lock path reports the same parse error.
189 lines
6.5 KiB
Go
189 lines
6.5 KiB
Go
package weed_server
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// conditionIsSet reports whether a condition asks for any check at all.
|
|
func conditionIsSet(cond *filer_pb.WriteCondition) bool {
|
|
return cond != nil && len(cond.Clauses) > 0
|
|
}
|
|
|
|
// writeConditionSatisfied reports whether the precondition holds against the
|
|
// current entry (nil if absent), evaluated under the path lock. Every clause
|
|
// must hold (logical AND).
|
|
func writeConditionSatisfied(cond *filer_pb.WriteCondition, current *filer.Entry) bool {
|
|
for _, c := range cond.Clauses {
|
|
if !clauseSatisfied(c, current) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// clauseSatisfied evaluates one primitive against the current entry. For the
|
|
// ETag kinds, etags is a set: IF_ETAG_MATCH holds when the current ETag equals
|
|
// any member, IF_ETAG_NOT_MATCH when it equals none. The IF_EXTENDED_* kinds are
|
|
// generic guards on an extended attribute used to enforce object-lock (legal
|
|
// hold and retention) without S3 knowledge in the filer.
|
|
func clauseSatisfied(c *filer_pb.WriteCondition_Clause, current *filer.Entry) bool {
|
|
exists := current != nil
|
|
switch c.Kind {
|
|
case filer_pb.WriteCondition_NONE:
|
|
return true
|
|
case filer_pb.WriteCondition_IF_NOT_EXISTS:
|
|
return !exists
|
|
case filer_pb.WriteCondition_IF_EXISTS:
|
|
return exists
|
|
case filer_pb.WriteCondition_IF_ETAG_MATCH:
|
|
return exists && etagInSet(storedEntryETag(current), c.Etags, c.AllowWeak)
|
|
case filer_pb.WriteCondition_IF_ETAG_NOT_MATCH:
|
|
return !exists || !etagInSet(storedEntryETag(current), c.Etags, c.AllowWeak)
|
|
case filer_pb.WriteCondition_IF_UNMODIFIED_SINCE:
|
|
return !exists || current.Attr.Mtime.Unix() <= c.UnixTime
|
|
case filer_pb.WriteCondition_IF_MODIFIED_SINCE:
|
|
return !exists || current.Attr.Mtime.Unix() > c.UnixTime
|
|
case filer_pb.WriteCondition_IF_EXTENDED_NOT_EQUAL:
|
|
if !exists {
|
|
return true
|
|
}
|
|
v, ok := current.Extended[c.ExtKey]
|
|
return !ok || string(v) != c.ExtValue
|
|
case filer_pb.WriteCondition_IF_EXTENDED_TIME_ELAPSED:
|
|
if !exists {
|
|
return true
|
|
}
|
|
// An optional gate scopes the guard: when gate_key is set, the time check
|
|
// only applies if extended[gate_key] == gate_value. This lets the gateway
|
|
// express governance bypass (enforce retention only for COMPLIANCE mode)
|
|
// without reading the entry — the filer decides under the lock.
|
|
if c.GateKey != "" {
|
|
gv, gok := current.Extended[c.GateKey]
|
|
if !gok || string(gv) != c.GateValue {
|
|
return true
|
|
}
|
|
}
|
|
v, ok := current.Extended[c.ExtKey]
|
|
if !ok {
|
|
return true
|
|
}
|
|
deadline, err := strconv.ParseInt(strings.TrimSpace(string(v)), 10, 64)
|
|
if err != nil {
|
|
// An unparseable retention deadline is treated as still in force, so
|
|
// a malformed attribute fails safe (write blocked) rather than open.
|
|
return false
|
|
}
|
|
return deadline <= time.Now().Unix()
|
|
case filer_pb.WriteCondition_IF_CHUNKS_EQUAL:
|
|
return chunkFidsEqual(current, c.Fids)
|
|
case filer_pb.WriteCondition_IF_ENTRY_EQUAL:
|
|
if !exists || c.ExpectedEntry == nil {
|
|
return !exists && c.ExpectedEntry == nil
|
|
}
|
|
// Compare both sides in serialized form on clones: chunks are matched
|
|
// by their fid only — the stored entry may carry the restored file_id
|
|
// while an expected one built from a metadata event does not.
|
|
expected := proto.Clone(c.ExpectedEntry).(*filer_pb.Entry)
|
|
filer_pb.BeforeEntrySerialization(expected.Chunks)
|
|
actual := proto.Clone(current.ToProtoEntry()).(*filer_pb.Entry)
|
|
filer_pb.BeforeEntrySerialization(actual.Chunks)
|
|
return proto.Equal(actual, filer.FromPbEntry("", expected).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
|
|
// closed so the write is blocked rather than slipping through.
|
|
return false
|
|
}
|
|
}
|
|
|
|
// chunkFidsEqual compares the stored chunk fids (absent entry = none) against
|
|
// expected as multisets; chunk order carries no meaning for needle liveness.
|
|
func chunkFidsEqual(current *filer.Entry, expected []string) bool {
|
|
var chunks []*filer_pb.FileChunk
|
|
if current != nil {
|
|
chunks = current.GetChunks()
|
|
}
|
|
if len(chunks) != len(expected) {
|
|
return false
|
|
}
|
|
counts := make(map[string]int, len(expected))
|
|
for _, fid := range expected {
|
|
counts[fid]++
|
|
}
|
|
for _, chunk := range chunks {
|
|
fid := chunk.GetFileIdString()
|
|
if counts[fid] == 0 {
|
|
return false
|
|
}
|
|
counts[fid]--
|
|
}
|
|
return true
|
|
}
|
|
|
|
// etagInSet reports whether stored matches any candidate. A strong comparison
|
|
// (allowWeak false) treats a weak ETag as never equal; a weak comparison
|
|
// ignores the W/ marker on both sides.
|
|
func etagInSet(stored string, candidates []string, allowWeak bool) bool {
|
|
for _, c := range candidates {
|
|
if etagEqual(stored, c, allowWeak) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func etagEqual(stored, expected string, allowWeak bool) bool {
|
|
sv, sWeak := canonicalETag(stored)
|
|
ev, eWeak := canonicalETag(expected)
|
|
// RFC 7232 strong comparison: a weak ETag on either side never matches.
|
|
if !allowWeak && (sWeak || eWeak) {
|
|
return false
|
|
}
|
|
return sv == ev
|
|
}
|
|
|
|
// canonicalETag splits off the weak (W/) marker before stripping quotes, so a
|
|
// weak ETag like W/"abc" yields ("abc", true).
|
|
func canonicalETag(etag string) (value string, weak bool) {
|
|
etag = strings.TrimSpace(etag)
|
|
if strings.HasPrefix(etag, "W/") {
|
|
return strings.Trim(etag[len("W/"):], `"`), true
|
|
}
|
|
return strings.Trim(etag, `"`), false
|
|
}
|
|
|
|
// storedEntryETag mirrors the S3 gateway's ETag precedence — the stored
|
|
// Seaweed ETag extended attribute, then the remote ETag while the entry still
|
|
// holds the remote bytes, then Md5, then the chunk hash (an empty object has
|
|
// no ETag) — so conditional comparisons match what the gateway computes,
|
|
// without coupling the filer to S3 request handling.
|
|
func storedEntryETag(entry *filer.Entry) string {
|
|
if v, ok := entry.Extended[s3_constants.ExtETagKey]; ok && len(v) > 0 {
|
|
return normalizeETag(string(v))
|
|
}
|
|
if remote := entry.Remote; remote != nil && remote.RemoteETag != "" &&
|
|
entry.FileSize == uint64(remote.RemoteSize) &&
|
|
(remote.LastLocalSyncTsNs > 0 || (len(entry.GetChunks()) == 0 && len(entry.Content) == 0)) {
|
|
return normalizeETag(remote.RemoteETag)
|
|
}
|
|
if len(entry.Md5) > 0 {
|
|
return fmt.Sprintf("%x", entry.Md5)
|
|
}
|
|
if len(entry.GetChunks()) == 0 {
|
|
return ""
|
|
}
|
|
return normalizeETag(filer.ETagChunks(entry.GetChunks()))
|
|
}
|
|
|
|
func normalizeETag(etag string) string {
|
|
return strings.Trim(etag, `"`)
|
|
}
|