Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates for force delete and prefix and delim bug #100

Merged
merged 9 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ type Backend interface {
// AWS does not validate the bucket's name for anything other than existence.
DeleteBucket(name string) error

// ForceDeleteBucket must delete a bucket and all its contents, regardless of
// whether the bucket is empty or not. This is useful for testing purposes
// where you need to clean up after yourself.
ForceDeleteBucket(name string) error

// GetObject must return a gofakes3.ErrNoSuchKey error if the object does
// not exist. See gofakes3.KeyNotFound() for a convenient way to create
// one.
Expand Down
30 changes: 30 additions & 0 deletions backend/s3afero/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,36 @@
return rerr
}

func (db *MultiBucketBackend) ForceDeleteBucket(name string) error {
db.lock.Lock()
defer db.lock.Unlock()

Check warning on line 281 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L279-L281

Added lines #L279 - L281 were not covered by tests

// Delete all objects in the bucket
entries, err := afero.ReadDir(db.bucketFs, name)
if err != nil {
return err

Check warning on line 286 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L284-L286

Added lines #L284 - L286 were not covered by tests
}

for _, entry := range entries {
fullPath := path.Join(name, entry.Name())
if err := db.bucketFs.RemoveAll(fullPath); err != nil {
return err

Check warning on line 292 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L289-L292

Added lines #L289 - L292 were not covered by tests
}
}

// Delete the bucket itself
if err := db.bucketFs.RemoveAll(name); err != nil {
return err

Check warning on line 298 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L297-L298

Added lines #L297 - L298 were not covered by tests
}

// Delete bucket metadata
if err := db.metaStore.deleteBucket(name); err != nil {
return err

Check warning on line 303 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L302-L303

Added lines #L302 - L303 were not covered by tests
}

return nil

Check warning on line 306 in backend/s3afero/multi.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/multi.go#L306

Added line #L306 was not covered by tests
}

func (db *MultiBucketBackend) BucketExists(name string) (exists bool, err error) {
db.lock.Lock()
defer db.lock.Unlock()
Expand Down
37 changes: 37 additions & 0 deletions backend/s3afero/single.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,43 @@
return gofakes3.ErrNotImplemented
}

func (db *SingleBucketBackend) ForceDeleteBucket(name string) error {
if name != db.name {
return gofakes3.BucketNotFound(name)

Check warning on line 464 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L462-L464

Added lines #L462 - L464 were not covered by tests
}

db.lock.Lock()
defer db.lock.Unlock()

Check warning on line 468 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L467-L468

Added lines #L467 - L468 were not covered by tests

// Delete all objects in the bucket
var objects []string
err := afero.Walk(db.fs, ".", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err

Check warning on line 474 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L471-L474

Added lines #L471 - L474 were not covered by tests
}
if !info.IsDir() {
objects = append(objects, path)

Check warning on line 477 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L476-L477

Added lines #L476 - L477 were not covered by tests
}
return nil

Check warning on line 479 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L479

Added line #L479 was not covered by tests
})
if err != nil {
return err

Check warning on line 482 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L481-L482

Added lines #L481 - L482 were not covered by tests
}

for _, object := range objects {
if err := db.deleteObjectLocked(name, object); err != nil {
return err

Check warning on line 487 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L485-L487

Added lines #L485 - L487 were not covered by tests
}
}

// Delete the bucket itself
if err := db.fs.RemoveAll("."); err != nil {
return err

Check warning on line 493 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L492-L493

Added lines #L492 - L493 were not covered by tests
}

return nil

Check warning on line 496 in backend/s3afero/single.go

View check run for this annotation

Codecov / codecov/patch

backend/s3afero/single.go#L496

Added line #L496 was not covered by tests
}

func (db *SingleBucketBackend) BucketExists(name string) (exists bool, err error) {
return db.name == name, nil
}
40 changes: 40 additions & 0 deletions backend/s3bolt/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,46 @@ func (db *Backend) DeleteBucket(name string) error {
})
}

func (db *Backend) ForceDeleteBucket(name string) error {
nameBts := []byte(name)

if bytes.Equal(nameBts, db.metaBucketName) {
return gofakes3.ResourceError(gofakes3.ErrInvalidBucketName, name)
}

return db.bolt.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(nameBts)
if b == nil {
return gofakes3.BucketNotFound(name)
}

// Delete all objects in the bucket
c := b.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
if err := b.Delete(k); err != nil {
return fmt.Errorf("gofakes3: delete failed for object %q in bucket %q", k, name)
}
}

// Delete bucket metadata
metaBucket, err := db.metaBucket(tx)
if err != nil {
return err
}

// FIXME: assumes a legacy database, where the bucket may not exist. Clean
// this up when there is a DB upgrade script.
if metaBucket != nil {
if err := metaBucket.deleteS3Bucket(name); err != nil {
return err
}
}

// Delete the bucket itself
return tx.DeleteBucket(nameBts)
})
}

func (db *Backend) BucketExists(name string) (exists bool, err error) {
err = db.bolt.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(name))
Expand Down
13 changes: 13 additions & 0 deletions backend/s3mem/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@
return nil
}

func (db *Backend) ForceDeleteBucket(name string) error {
db.lock.Lock()
defer db.lock.Unlock()

Check warning on line 170 in backend/s3mem/backend.go

View check run for this annotation

Codecov / codecov/patch

backend/s3mem/backend.go#L168-L170

Added lines #L168 - L170 were not covered by tests

if db.buckets[name] == nil {
return gofakes3.ErrNoSuchBucket

Check warning on line 173 in backend/s3mem/backend.go

View check run for this annotation

Codecov / codecov/patch

backend/s3mem/backend.go#L172-L173

Added lines #L172 - L173 were not covered by tests
}

delete(db.buckets, name)

Check warning on line 176 in backend/s3mem/backend.go

View check run for this annotation

Codecov / codecov/patch

backend/s3mem/backend.go#L176

Added line #L176 was not covered by tests

return nil

Check warning on line 178 in backend/s3mem/backend.go

View check run for this annotation

Codecov / codecov/patch

backend/s3mem/backend.go#L178

Added line #L178 was not covered by tests
}

func (db *Backend) BucketExists(name string) (exists bool, err error) {
db.lock.RLock()
defer db.lock.RUnlock()
Expand Down
18 changes: 9 additions & 9 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html
//
// If you add a code to this list, please also add it to ErrorCode.Status().
//
const (
ErrNone ErrorCode = ""

Expand Down Expand Up @@ -74,6 +73,9 @@ const (
// See BucketNotFound() for a helper function for this error:
ErrNoSuchBucket ErrorCode = "NoSuchBucket"

// The specified bucket does not exist.
ErrNonExistentBucket ErrorCode = "NonExistentBucket"

// See KeyNotFound() for a helper function for this error:
ErrNoSuchKey ErrorCode = "NoSuchKey"

Expand Down Expand Up @@ -150,13 +152,12 @@ type Error interface {
// Code and Message:
//
// func NotQuiteRight(at time.Time, max time.Duration) error {
// code := ErrNotQuiteRight
// return &notQuiteRightResponse{
// ErrorResponse{Code: code, Message: code.Message()},
// 123456789,
// }
// }
//
// code := ErrNotQuiteRight
// return &notQuiteRightResponse{
// ErrorResponse{Code: code, Message: code.Message()},
// 123456789,
// }
// }
type ErrorResponse struct {
XMLName xml.Name `xml:"Error"`

Expand Down Expand Up @@ -291,7 +292,6 @@ func (e ErrorCode) Status() int {
// }
//
// If err is nil and code is ErrNone, HasErrorCode returns true.
//
func HasErrorCode(err error, code ErrorCode) bool {
if err == nil && code == "" {
return true
Expand Down
28 changes: 25 additions & 3 deletions gofakes3.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@

isVersion2 := q.Get("list-type") == "2"

g.log.Print(LogInfo, "bucketname:", bucketName, "prefix:", prefix, "page:", fmt.Sprintf("%+v", page))
g.log.Print(LogInfo, "bucketName:", bucketName, "prefix:", prefix, "page:", fmt.Sprintf("%+v", page))

objects, err := g.storage.ListBucket(bucketName, &prefix, page)
if err != nil {
Expand Down Expand Up @@ -400,6 +400,20 @@
if err := g.ensureBucketExists(bucket); err != nil {
return err
}

// Support for Minio's DeleteBucket with force-delete header.
forceDelete := r.Header.Get("x-minio-force-delete")
if forceDelete == "true" {
type forceDeleter interface {
ForceDeleteBucket(name string) error

Check warning on line 408 in gofakes3.go

View check run for this annotation

Codecov / codecov/patch

gofakes3.go#L407-L408

Added lines #L407 - L408 were not covered by tests
}
if f, ok := g.storage.(forceDeleter); ok {
if err := f.ForceDeleteBucket(bucket); err != nil {
return err

Check warning on line 412 in gofakes3.go

View check run for this annotation

Codecov / codecov/patch

gofakes3.go#L410-L412

Added lines #L410 - L412 were not covered by tests
}
}
}

if err := g.storage.DeleteBucket(bucket); err != nil {
return err
}
Expand All @@ -417,7 +431,10 @@
return err
}

w.Write([]byte{})
_, err := w.Write([]byte{})
if err != nil {
return err

Check warning on line 436 in gofakes3.go

View check run for this annotation

Codecov / codecov/patch

gofakes3.go#L436

Added line #L436 was not covered by tests
}
return nil
}

Expand Down Expand Up @@ -463,7 +480,12 @@
g.log.Print(LogErr, "unexpected nil object for key", bucket, object)
return ErrInternal
}
defer obj.Contents.Close()
defer func(Contents io.ReadCloser) {
err := Contents.Close()
if err != nil {
g.log.Print(LogErr, "contents close", err.Error())

Check warning on line 486 in gofakes3.go

View check run for this annotation

Codecov / codecov/patch

gofakes3.go#L486

Added line #L486 was not covered by tests
}
}(obj.Contents)

if err := g.writeGetOrHeadObjectResponse(obj, w, r); err != nil {
return err
Expand Down
47 changes: 46 additions & 1 deletion gofakes3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func TestCreateObjectMD5(t *testing.T) {
}
}

if ts.backendObjectExists(defaultBucket, "invalid") {
if exists, _ := ts.backendObjectExists(defaultBucket, "invalid"); exists {
t.Fatal("unexpected object")
}
}
Expand Down Expand Up @@ -509,6 +509,22 @@ func TestDeleteBucket(t *testing.T) {
t.Fatal("expected ErrBucketNotEmpty, found", err)
}
})

t.Run("force-delete-does-not-fail-if-not-empty", func(t *testing.T) {
ts := newTestServer(t, withoutInitialBuckets())
defer ts.Close()
svc := ts.s3Client()

ts.backendCreateBucket("test")
ts.backendPutString("test", "test", nil, "test")
_, err := svc.DeleteBucket(&s3.DeleteBucketInput{
Bucket: aws.String("test"),
})
if !hasErrorCode(err, gofakes3.ErrBucketNotEmpty) {
t.Fatal("expected ErrBucketNotEmpty, found", err)
}
})

}

func TestDeleteMulti(t *testing.T) {
Expand Down Expand Up @@ -575,6 +591,35 @@ func TestDeleteMulti(t *testing.T) {
})
}

func TestForceDeleteBucket_BucketExists(t *testing.T) {
// Create a test server with no initial buckets
ts := newTestServer(t, withoutInitialBuckets())
ts.Helper()
bucketName := "test-bucket"
ts.backendCreateBucket(bucketName)

// Force delete the bucket
err := ts.ForceDeleteBucket(bucketName)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

exists, _ := ts.backendObjectExists(bucketName, "test-bucket")
if !exists {

}
}

func TestForceDeleteBucket_BucketDoesNotExist(t *testing.T) {
ts := newTestServer(t, withoutInitialBuckets())

bucketName := "nonexistent-bucket"
err := ts.ForceDeleteBucket(bucketName)
if err == gofakes3.ErrNonExistentBucket {
t.Fatalf("expected error %v, got %v", gofakes3.ErrNoSuchBucket, err)
}
}

func TestGetBucketLocation(t *testing.T) {
ts := newTestServer(t)
defer ts.Close()
Expand Down
30 changes: 25 additions & 5 deletions init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,17 @@ func (ts *testServer) backendCreateBucket(bucket string) {
}
}

func (ts *testServer) backendObjectExists(bucket, key string) bool {
func (ts *testServer) backendObjectExists(bucket, key string) (bool, bool) {
ts.Helper()
obj, err := ts.backend.HeadObject(bucket, key)
if err != nil {
if hasErrorCode(err, gofakes3.ErrNoSuchKey) {
return false
return false, false
} else {
ts.Fatal(err)
}
}
return obj != nil
return obj != nil, false
}

func (ts *testServer) backendPutString(bucket, key string, meta map[string]string, in string) {
Expand Down Expand Up @@ -771,15 +771,15 @@ func (ts *testServer) listBucketV2Pages(prefix *gofakes3.Prefix, maxKeys int64,
}

var rs listBucketResult
if err := (svc.ListObjectsV2Pages(in, func(out *s3.ListObjectsV2Output, lastPage bool) bool {
if err := svc.ListObjectsV2Pages(in, func(out *s3.ListObjectsV2Output, lastPage bool) bool {
pages++
if pages > pageLimit {
panic("stuck in a page loop")
}
rs.CommonPrefixes = append(rs.CommonPrefixes, out.CommonPrefixes...)
rs.Contents = append(rs.Contents, out.Contents...)
return !lastPage
})); err != nil {
}); err != nil {
return nil, err
}

Expand All @@ -790,6 +790,26 @@ func (ts *testServer) Close() {
ts.server.Close()
}

func (ts *testServer) ForceDeleteBucket(name string) interface{} {
return nil
}

type NoSuchBucketError struct {
Name string
}

func (ts *testServer) backendForceDeleteBucket(name string) interface{} {
ts.Helper()
err := ts.backend.DeleteBucket(name)
if err != nil {
if hasErrorCode(err, gofakes3.ErrNoSuchBucket) {
return NoSuchBucketError{Name: name}
}
ts.Fatal("delete bucket failed", err)
}
return true
}

func hashMD5Bytes(body []byte) hashValue {
h := md5.New()
h.Write(body)
Expand Down
Loading