-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprecondition.go
More file actions
159 lines (135 loc) · 4.79 KB
/
Copy pathprecondition.go
File metadata and controls
159 lines (135 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package fs
import (
"strings"
"time"
)
// ObjectState is the current state of a key, as seen by a backend holding the
// lock that serializes writes to it. It is what conditional requests are
// evaluated against.
type ObjectState struct {
// Exists reports whether the object is present. The other fields are
// meaningful only when it is true.
Exists bool
// ETag is the stored ETag, quoted or bare.
ETag string
Size int64
LastModified time.Time
}
// Conditions carries the conditional headers a mutating request may specify.
// The zero value imposes no condition.
//
// Backends MUST evaluate them while holding the lock that serializes writes to
// the key, so the evaluation is atomic with the mutation. Evaluating the
// condition in a separate step before the write (check-then-act) races:
// several concurrent If-None-Match: * writers can all observe "absent" and all
// succeed.
type Conditions struct {
// IfMatch and IfNoneMatch are the raw header values: "*" or a
// comma-separated entity-tag list.
IfMatch string
IfNoneMatch string
// Size, when set, requires the object to be exactly this many bytes
// (x-amz-if-match-size).
Size *int64
// LastModified, when set, requires the object's modification time to match
// to the second (x-amz-if-match-last-modified-time).
LastModified *time.Time
}
// IsZero reports whether no condition is set.
func (c Conditions) IsZero() bool {
return c.IfMatch == "" && c.IfNoneMatch == "" && c.Size == nil && c.LastModified == nil
}
// CheckWrite evaluates the conditions for a write (PUT, multipart completion)
// against state. It returns:
//
// - ErrObjectNotFound when If-Match is set and the object is absent. S3
// reports a conditional write against a missing key as 404 NoSuchKey, not
// as a failed precondition.
// - ErrPreconditionFailed when the object is present and a condition does
// not hold.
// - nil when the write may proceed.
func (c Conditions) CheckWrite(state ObjectState) error {
if c.IsZero() {
return nil
}
if strings.TrimSpace(c.IfMatch) != "" && !state.Exists {
return ErrObjectNotFound
}
if !c.holds(state) {
return ErrPreconditionFailed
}
return nil
}
// CheckDelete evaluates the conditions for a delete against state.
//
// Deletion is idempotent: a condition against a key that is already gone is
// not a failure, so an absent object always passes. A present object that does
// not satisfy a condition yields ErrPreconditionFailed.
func (c Conditions) CheckDelete(state ObjectState) error {
if c.IsZero() || !state.Exists {
return nil
}
if !c.holds(state) {
return ErrPreconditionFailed
}
return nil
}
// holds reports whether every set condition is satisfied by state. An absent
// object satisfies If-None-Match and fails everything else.
func (c Conditions) holds(state ObjectState) bool {
if ifNoneMatch := strings.TrimSpace(c.IfNoneMatch); ifNoneMatch != "" {
if ifNoneMatch == "*" && state.Exists {
return false
}
if ifNoneMatch != "*" && state.Exists && etagInList(ifNoneMatch, state.ETag) {
return false
}
}
if ifMatch := strings.TrimSpace(c.IfMatch); ifMatch != "" {
if !state.Exists {
return false
}
if ifMatch != "*" && !etagInList(ifMatch, state.ETag) {
return false
}
}
if c.Size != nil && (!state.Exists || state.Size != *c.Size) {
return false
}
if c.LastModified != nil {
if !state.Exists ||
!state.LastModified.Truncate(time.Second).Equal(c.LastModified.Truncate(time.Second)) {
return false
}
}
return true
}
// Conditions returns the conditional-write headers carried by the request.
func (r *PutObjectRequest) Conditions() Conditions {
return Conditions{IfMatch: r.IfMatch, IfNoneMatch: r.IfNoneMatch}
}
// PreconditionFailed reports whether the request's If-None-Match / If-Match
// conditions fail against the current object state, where exists reports
// whether the target object is present and currentETag is its ETag (quoted or
// bare; only meaningful when exists is true).
//
// Deprecated: use Conditions().CheckWrite, which distinguishes a failed
// precondition from a conditional write against a missing key — S3 reports the
// latter as 404 NoSuchKey, not 412. Kept for backends outside this repository.
func (r *PutObjectRequest) PreconditionFailed(exists bool, currentETag string) bool {
return r.Conditions().CheckWrite(ObjectState{Exists: exists, ETag: currentETag}) != nil
}
// etagInList reports whether the raw ETag matches any entity-tag in a
// comma-separated If-Match / If-None-Match header value, tolerating quotes and
// the weak-validator prefix (W/).
func etagInList(header, raw string) bool {
raw = strings.Trim(raw, `"`)
for tok := range strings.SplitSeq(header, ",") {
tok = strings.TrimSpace(tok)
tok = strings.TrimPrefix(tok, "W/")
if strings.Trim(tok, `"`) == raw {
return true
}
}
return false
}