From 8ae566d44e1119cff641a9c55d8fa8e4da08bec1 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Mon, 27 Apr 2026 10:33:12 -0700 Subject: [PATCH] feat: add new ErrNoSpaceLeftOnDevice API error for ENOSPC errors Add a new non-AWS error ErrNoSpaceLeftOnDevice (HTTP 507 Insufficient Storage) to s3err. Update all call sites in the posix backend that could return ENOSPC and return the new error when the underlying filesystem has no space remaining. Fixes #2093 --- backend/meta/sidecar.go | 15 +++++++++++++++ backend/meta/xattr.go | 6 ++++++ backend/posix/posix.go | 27 +++++++++++++++++++++++++++ s3err/s3err.go | 6 ++++++ 4 files changed, 54 insertions(+) diff --git a/backend/meta/sidecar.go b/backend/meta/sidecar.go index 98082af8..3056ae3e 100644 --- a/backend/meta/sidecar.go +++ b/backend/meta/sidecar.go @@ -20,6 +20,9 @@ import ( "io" "os" "path/filepath" + "syscall" + + "github.com/versity/versitygw/s3err" ) // SideCar is a metadata storer that uses sidecar files to store metadata. @@ -71,12 +74,18 @@ func (s SideCar) StoreAttribute(_ *os.File, bucket, object, attribute string, va } err := os.MkdirAll(metadir, 0777) if err != nil { + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return fmt.Errorf("failed to create metadata directory: %v", err) } attr := filepath.Join(metadir, attribute) tempfile, err := os.CreateTemp(metadir, attribute) if err != nil { + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return fmt.Errorf("failed to create temporary file: %v", err) } defer os.Remove(tempfile.Name()) @@ -84,6 +93,9 @@ func (s SideCar) StoreAttribute(_ *os.File, bucket, object, attribute string, va _, err = tempfile.Write(value) if err != nil { tempfile.Close() + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return fmt.Errorf("failed to write attribute: %v", err) } @@ -176,6 +188,9 @@ func (s SideCar) RenameObject(bucket, oldObject, newObject string) error { newPath := filepath.Join(s.dir, bucket, newObject) if err := os.MkdirAll(filepath.Dir(newPath), 0777); err != nil { + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return fmt.Errorf("create parent for renamed metadata: %w", err) } diff --git a/backend/meta/xattr.go b/backend/meta/xattr.go index c2d66cfd..7be71d7a 100644 --- a/backend/meta/xattr.go +++ b/backend/meta/xattr.go @@ -57,6 +57,9 @@ func (x XattrMeta) StoreAttribute(f *os.File, bucket, object, attribute string, if errors.Is(err, syscall.EROFS) { return s3err.GetAPIError(s3err.ErrMethodNotAllowed) } + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return err } @@ -64,6 +67,9 @@ func (x XattrMeta) StoreAttribute(f *os.File, bucket, object, attribute string, if errors.Is(err, syscall.EROFS) { return s3err.GetAPIError(s3err.ErrMethodNotAllowed) } + if errors.Is(err, syscall.ENOSPC) { + return s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return err } diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 680dfa38..8ad71eed 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -1989,6 +1989,9 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C if errors.Is(err, syscall.EDQUOT) { return res, "", s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return res, "", s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return res, "", fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -2044,6 +2047,9 @@ func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.C if errors.Is(err, syscall.EDQUOT) { return res, "", s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return res, "", s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return res, "", fmt.Errorf("copy part %v: %v", part.PartNumber, err) } } @@ -2877,6 +2883,9 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart if errors.Is(err, syscall.EDQUOT) { return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return nil, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -2989,6 +2998,9 @@ func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPart if errors.Is(err, syscall.EDQUOT) { return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } // Return the error itself, if it's an 's3err.APIError' if _, ok := err.(s3err.APIError); ok { return nil, err @@ -3253,6 +3265,9 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) if errors.Is(err, syscall.EDQUOT) { return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return s3response.CopyPartResult{}, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -3298,6 +3313,9 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) if errors.Is(err, syscall.EDQUOT) { return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return s3response.CopyPartResult{}, fmt.Errorf("copy part data: %w", err) } @@ -3494,6 +3512,9 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje if errors.Is(err, syscall.EDQUOT) { return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return s3response.PutObjectOutput{}, err } @@ -3622,6 +3643,9 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje if errors.Is(err, syscall.EDQUOT) { return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } return s3response.PutObjectOutput{}, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -3646,6 +3670,9 @@ func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObje if errors.Is(err, syscall.EDQUOT) { return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) } + if errors.Is(err, syscall.ENOSPC) { + return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice) + } // Return the error itself, if it's an 's3err.APIError' if _, ok := err.(s3err.APIError); ok { return s3response.PutObjectOutput{}, err diff --git a/s3err/s3err.go b/s3err/s3err.go index ae9de4fd..0787e326 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -199,6 +199,7 @@ const ( ErrQuotaExceeded ErrVersioningNotConfigured ErrACLsDisabled + ErrNoSpaceLeftOnDevice // Admin api errors ErrAdminAccessDenied @@ -904,6 +905,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Access control lists are disabled at the gateway level", HTTPStatusCode: http.StatusBadRequest, }, + ErrNoSpaceLeftOnDevice: { + Code: "InsufficientStorage", + Description: "No space left on device.", + HTTPStatusCode: http.StatusInsufficientStorage, + }, // Admin api errors ErrAdminAccessDenied: {