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
This commit is contained in:
Ben McClelland
2026-04-27 10:33:12 -07:00
parent b7b5a347ae
commit 8ae566d44e
4 changed files with 54 additions and 0 deletions

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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: {