package release import ( "context" "errors" "fmt" "os" "strings" "github.com/aws/aws-sdk-go-v2/aws" awss3 "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/carlosframework/platform/internal/safefile" ) var ( ErrConditionalDeleteUnsupported = errors.New("store: conditional delete unsupported") ErrInvalidETag = errors.New("store: conditional delete requires one exact etag") ) // ConditionalDeleter is optional. Callers must not replace a refused conditional // delete with an ordinary Delete: that would discard the observed revision. type ConditionalDeleter interface { DeleteIfMatch(ctx context.Context, key, etag string) error } // DeleteIfMatch deletes only the observed revision, or refuses if the backend // cannot enforce the condition. Known absence is success, not proof that an object // previously existed. It never falls back to an unconditional delete. func DeleteIfMatch(ctx context.Context, s Store, key, etag string) error { if !exactDeleteETag(etag) { return ErrInvalidETag } conditional, ok := s.(ConditionalDeleter) if !ok { return ErrConditionalDeleteUnsupported } return conditional.DeleteIfMatch(ctx, key, etag) } func exactDeleteETag(etag string) bool { if len(etag) == 0 || strings.HasPrefix(etag, "W/") { return false } quoted := etag[0] == '"' if quoted { if len(etag) < 3 || etag[len(etag)-1] != '"' { return false } etag = etag[1 : len(etag)-1] } for _, c := range []byte(etag) { if c <= 32 || c == 127 || c == '"' || c == '*' || (!quoted && c == ',') { return false } } return true } func (p prefixStore) DeleteIfMatch(ctx context.Context, key, etag string) error { return DeleteIfMatch(ctx, p.Store, p.full(key), etag) } func (d *DirStore) DeleteIfMatch(ctx context.Context, key, etag string) error { err := d.deleteIfMatchRaw(ctx, key, etag) if err == nil { d.bump(ctx, key) } return err } func (d *DirStore) deleteIfMatchRaw(_ context.Context, key, etag string) error { if !exactDeleteETag(etag) { return ErrInvalidETag } path, err := d.path(key) if err != nil { return err } d.mu.Lock() defer d.mu.Unlock() body, err := safefile.ReadFile(path) if errors.Is(err, os.ErrNotExist) { return nil } if err != nil { return err } if etagOf(body) != etag { return fmt.Errorf("dirstore: %s: %w", key, ErrCASConflict) } return safefile.Remove(path) } func (s *S3Store) DeleteIfMatch(ctx context.Context, key, etag string) error { err := s.deleteIfMatchRaw(ctx, key, etag) if err == nil { s.bump(ctx, key) } return err } func (s *S3Store) deleteIfMatchRaw(ctx context.Context, key, etag string) error { if !exactDeleteETag(etag) { return ErrInvalidETag } _, err := s.Client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: &s.Bucket, Key: &key, IfMatch: aws.String(etag)}) if s3NotFound(err) { return nil } if s3CASConflict(err) { return fmt.Errorf("s3store: %s: %w", key, ErrCASConflict) } return err } var ( _ ConditionalDeleter = (*DirStore)(nil) _ ConditionalDeleter = (*S3Store)(nil) _ ConditionalDeleter = prefixStore{} )