carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "strings"
9
10 "github.com/aws/aws-sdk-go-v2/aws"
11 awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
12 "github.com/carlosframework/platform/internal/safefile"
13)
14
15var (
16 ErrConditionalDeleteUnsupported = errors.New("store: conditional delete unsupported")
17 ErrInvalidETag = errors.New("store: conditional delete requires one exact etag")
18)
19
20// ConditionalDeleter is optional. Callers must not replace a refused conditional
21// delete with an ordinary Delete: that would discard the observed revision.
22type ConditionalDeleter interface {
23 DeleteIfMatch(ctx context.Context, key, etag string) error
24}
25
26// DeleteIfMatch deletes only the observed revision, or refuses if the backend
27// cannot enforce the condition. Known absence is success, not proof that an object
28// previously existed. It never falls back to an unconditional delete.
29func DeleteIfMatch(ctx context.Context, s Store, key, etag string) error {
30 if !exactDeleteETag(etag) {
31 return ErrInvalidETag
32 }
33 conditional, ok := s.(ConditionalDeleter)
34 if !ok {
35 return ErrConditionalDeleteUnsupported
36 }
37 return conditional.DeleteIfMatch(ctx, key, etag)
38}
39
40func exactDeleteETag(etag string) bool {
41 if len(etag) == 0 || strings.HasPrefix(etag, "W/") {
42 return false
43 }
44 quoted := etag[0] == '"'
45 if quoted {
46 if len(etag) < 3 || etag[len(etag)-1] != '"' {
47 return false
48 }
49 etag = etag[1 : len(etag)-1]
50 }
51 for _, c := range []byte(etag) {
52 if c <= 32 || c == 127 || c == '"' || c == '*' || (!quoted && c == ',') {
53 return false
54 }
55 }
56 return true
57}
58
59func (p prefixStore) DeleteIfMatch(ctx context.Context, key, etag string) error {
60 return DeleteIfMatch(ctx, p.Store, p.full(key), etag)
61}
62
63func (d *DirStore) DeleteIfMatch(ctx context.Context, key, etag string) error {
64 err := d.deleteIfMatchRaw(ctx, key, etag)
65 if err == nil {
66 d.bump(ctx, key)
67 }
68 return err
69}
70
71func (d *DirStore) deleteIfMatchRaw(_ context.Context, key, etag string) error {
72 if !exactDeleteETag(etag) {
73 return ErrInvalidETag
74 }
75 path, err := d.path(key)
76 if err != nil {
77 return err
78 }
79 d.mu.Lock()
80 defer d.mu.Unlock()
81 body, err := safefile.ReadFile(path)
82 if errors.Is(err, os.ErrNotExist) {
83 return nil
84 }
85 if err != nil {
86 return err
87 }
88 if etagOf(body) != etag {
89 return fmt.Errorf("dirstore: %s: %w", key, ErrCASConflict)
90 }
91 return safefile.Remove(path)
92}
93
94func (s *S3Store) DeleteIfMatch(ctx context.Context, key, etag string) error {
95 err := s.deleteIfMatchRaw(ctx, key, etag)
96 if err == nil {
97 s.bump(ctx, key)
98 }
99 return err
100}
101
102func (s *S3Store) deleteIfMatchRaw(ctx context.Context, key, etag string) error {
103 if !exactDeleteETag(etag) {
104 return ErrInvalidETag
105 }
106 _, err := s.Client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: &s.Bucket, Key: &key, IfMatch: aws.String(etag)})
107 if s3NotFound(err) {
108 return nil
109 }
110 if s3CASConflict(err) {
111 return fmt.Errorf("s3store: %s: %w", key, ErrCASConflict)
112 }
113 return err
114}
115
116var (
117 _ ConditionalDeleter = (*DirStore)(nil)
118 _ ConditionalDeleter = (*S3Store)(nil)
119 _ ConditionalDeleter = prefixStore{}
120)
121