From aeea61544bea38dbd4fa04fee74786f2ea43cb91 Mon Sep 17 00:00:00 2001 From: Ben McClelland Date: Sat, 6 Apr 2024 11:53:40 -0700 Subject: [PATCH] feat: add s3err QuotaExceeded for posix/scoutfs When fileystem quota exceeded, the gateway will now return the error: S3 error: 403 (QuotaExceeded): Your request was denied due to quota exceeded. This will help clients to better detect upload errors due to quota exceeded. Fixes #483 --- backend/posix/posix.go | 27 +++++++++++++++++++++++++++ backend/scoutfs/scoutfs.go | 4 ++++ s3err/s3err.go | 6 ++++++ 3 files changed, 37 insertions(+) diff --git a/backend/posix/posix.go b/backend/posix/posix.go index d83e262..1ce09f3 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -419,6 +419,9 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM f, err := p.openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, totalsize, acct) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return nil, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -431,6 +434,9 @@ func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteM _, err = io.Copy(f, pf) pf.Close() if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return nil, fmt.Errorf("copy part %v: %v", p.PartNumber, err) } } @@ -910,6 +916,9 @@ func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (stri f, err := p.openTmpFile(filepath.Join(bucket, objdir), bucket, partPath, length, acct) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return "", s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return "", fmt.Errorf("open temp file: %w", err) } @@ -917,6 +926,9 @@ func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (stri tr := io.TeeReader(r, hash) _, err = io.Copy(f, tr) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return "", s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return "", fmt.Errorf("write part data: %w", err) } @@ -1009,6 +1021,9 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) f, err := p.openTmpFile(filepath.Join(*upi.Bucket, objdir), *upi.Bucket, partPath, length, acct) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return s3response.CopyObjectResult{}, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -1028,6 +1043,9 @@ func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) _, err = io.Copy(f, tr) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return s3response.CopyObjectResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return s3response.CopyObjectResult{}, fmt.Errorf("copy part data: %w", err) } @@ -1107,6 +1125,9 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e err = backend.MkdirAll(name, uid, gid, doChown) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return "", s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return "", err } @@ -1129,6 +1150,9 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e f, err := p.openTmpFile(filepath.Join(*po.Bucket, metaTmpDir), *po.Bucket, *po.Key, contentLength, acct) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return "", s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return "", fmt.Errorf("open temp file: %w", err) } defer f.cleanup() @@ -1137,6 +1161,9 @@ func (p *Posix) PutObject(ctx context.Context, po *s3.PutObjectInput) (string, e rdr := io.TeeReader(po.Body, hash) _, err = io.Copy(f, rdr) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return "", s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return "", fmt.Errorf("write object data: %w", err) } dir := filepath.Dir(name) diff --git a/backend/scoutfs/scoutfs.go b/backend/scoutfs/scoutfs.go index 89872c0..b5c065e 100644 --- a/backend/scoutfs/scoutfs.go +++ b/backend/scoutfs/scoutfs.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "strings" + "syscall" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" @@ -222,6 +223,9 @@ func (s *ScoutFS) CompleteMultipartUpload(ctx context.Context, input *s3.Complet // extents around. so we dont want to fallocate this. f, err := s.openTmpFile(filepath.Join(bucket, metaTmpDir), bucket, object, 0, acct) if err != nil { + if errors.Is(err, syscall.EDQUOT) { + return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded) + } return nil, fmt.Errorf("open temp file: %w", err) } defer f.cleanup() diff --git a/s3err/s3err.go b/s3err/s3err.go index 838ee0f..aa3d11a 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -116,6 +116,7 @@ const ( ErrExistingObjectIsDirectory ErrObjectParentIsFile ErrDirectoryObjectContainsData + ErrQuotaExceeded ) var errorCodeResponse = map[ErrorCode]APIError{ @@ -414,6 +415,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Directory object contains data payload.", HTTPStatusCode: http.StatusBadRequest, }, + ErrQuotaExceeded: { + Code: "QuotaExceeded", + Description: "Your request was denied due to quota exceeded.", + HTTPStatusCode: http.StatusForbidden, + }, } // GetAPIError provides API Error for input API error code.