Skip to content

Commit

Permalink
feat: Add error for files larger than FileMaxSize (#4069)
Browse files Browse the repository at this point in the history
When uploading a file we can receive a `413` error response in return
in 2 cases:
1. uploading the file would exceed the quota of the Cozy
2. the file size is larger than the server's filesystem's maximum file
size

We want clients to be able to make the difference between the 2
situations as they otherwise need to make an extra request to the
server to fetch the disk quota or hardcode the server's filesystem's
maximum file size.

Thus we introduce a new `ErrMaxFileSize` error which will still be
returned with a `413` HTTP status but with a different body.
  • Loading branch information
taratatach authored Jul 26, 2023
2 parents 1491663 + f7d676d commit 60bc67f
Show file tree
Hide file tree
Showing 10 changed files with 53 additions and 16 deletions.
26 changes: 18 additions & 8 deletions docs/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,8 @@ Hello world!
- 412 Precondition Failed, when the md5sum is `Content-MD5` is not equal to
the md5sum computed by the server
- 413 Payload Too Large, when there is not enough available space on the cozy
to upload the file
to upload the file or the file is larger than the server's filesystem maximum
file size
- 422 Unprocessable Entity, when the sent data is invalid (for example, the
parent doesn't exist, `Type` or `Name` parameter is missing or invalid,
etc.)
Expand Down Expand Up @@ -901,15 +902,21 @@ The `updated_at` field will be the first value in this list:
- the `Date` HTTP header
- the current time from the server.

/!\ If the `updated_at` field is older than the `created_at` one, then the
`updated_at` will be set with the value of the `created_at`.

#### Query-String

| Parameter | Description |
| ---------- | -------------------------------------------------- |
| Tags | an array of tags |
| Executable | `true` if the file is executable (UNIX permission) |
| Encrypted | `true` if the file is client-side encrypted |
| MetadataID | the identifier of a metadata object |
| UpdatedAt | the modification date of the file |
| Parameter | Description |
| ----------------------- | -------------------------------------------------------------- |
| Size | the file size (when `Content-Length` can't be used) |
| Tags | an array of tags |
| Executable | `true` if the file is executable (UNIX permission) |
| Encrypted | `true` if the file is client-side encrypted |
| MetadataID | the identifier of a metadata object |
| UpdatedAt | the modification date of the file |
| SourceAccount | the id of the source account used by a konnector |
| SourceAccountIdentifier | the unique identifier of the account targeted by the connector |

#### HTTP headers

Expand All @@ -935,6 +942,9 @@ HELLO WORLD!
- 404 Not Found, when the file wasn't existing
- 412 Precondition Failed, when the `If-Match` header is set and doesn't match
the last revision of the file
- 413 Payload Too Large, when there is not enough available space on the cozy
to upload the file or the file is larger than the server's filesystem maximum
file size

#### Response

Expand Down
2 changes: 1 addition & 1 deletion model/sharing/sharing.go
Original file line number Diff line number Diff line change
Expand Up @@ -1708,7 +1708,7 @@ func isFileTooBigForInstance(inst *instance.Instance, doc couchdb.JSONDoc) bool
}

_, _, _, err = vfs.CheckAvailableDiskSpace(inst.VFS(), file)
return errors.Is(err, vfs.ErrFileTooBig)
return errors.Is(err, vfs.ErrFileTooBig) || errors.Is(err, vfs.ErrMaxFileSize)
}

// wasUpdatedRecently returns true if the given document's latest update, given
Expand Down
2 changes: 2 additions & 0 deletions model/vfs/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ var (
ErrWrongCouchdbState = errors.New("Wrong couchdb reduce value")
// ErrFileTooBig is used when there is no more space left on the filesystem
ErrFileTooBig = errors.New("The file is too big and exceeds the disk quota")
// ErrMaxFileSize is used when a file is larger than the filesystem's maximum file size
ErrMaxFileSize = errors.New("The file is too big and exceeds the filesystem maximum file size")
// ErrFsckFailFast is used when the FSCK is stopped by the fail-fast option
ErrFsckFailFast = errors.New("FSCK has been stopped on first failure")
// ErrWrongToken is used when a key is not found on the store
Expand Down
2 changes: 1 addition & 1 deletion model/vfs/vfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -908,7 +908,7 @@ func CheckAvailableDiskSpace(fs VFS, doc *FileDoc) (newsize, maxsize, capsize in

maxsize = fs.MaxFileSize()
if maxsize > 0 && newsize > maxsize {
return 0, 0, 0, ErrFileTooBig
return 0, 0, 0, ErrMaxFileSize
}

diskQuota := fs.DiskQuota()
Expand Down
2 changes: 1 addition & 1 deletion model/vfs/vfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@ func TestVfs(t *testing.T) {
require.NoError(t, err)
_, _, _, err = vfs.CheckAvailableDiskSpace(fs, doc)
assert.Error(t, err)
assert.Equal(t, vfs.ErrFileTooBig, err)
assert.Equal(t, vfs.ErrMaxFileSize, err)
}
})
})
Expand Down
2 changes: 1 addition & 1 deletion web/files/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -1946,7 +1946,7 @@ func wrapVfsError(err error) *jsonapi.Error {
case vfs.ErrFileInTrash, vfs.ErrNonAbsolutePath,
vfs.ErrDirNotEmpty:
return jsonapi.BadRequest(err)
case vfs.ErrFileTooBig:
case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
case vfs.ErrWrongToken:
return jsonapi.BadRequest(err)
Expand Down
25 changes: 25 additions & 0 deletions web/files/files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"
"time"

"github.com/cozy/cozy-stack/model/instance/lifecycle"
"github.com/cozy/cozy-stack/model/permission"
"github.com/cozy/cozy-stack/model/vfs"
"github.com/cozy/cozy-stack/pkg/config/config"
Expand Down Expand Up @@ -595,6 +596,30 @@ func TestFiles(t *testing.T) {
assert.Equal(t, "foo", string(buf))
})

t.Run("UploadExceedingQuota", func(t *testing.T) {
e := testutils.CreateTestClient(t, ts.URL)

lifecycle.Patch(testInstance, &lifecycle.Options{DiskQuota: 3})

e.POST("/files/").
WithQuery("Type", "file").
WithQuery("Name", "too-large").
WithQuery("Size", 3).
WithHeader("Content-Type", "text/plain").
WithHeader("Authorization", "Bearer "+token).
WithTransformer(func(r *http.Request) { r.ContentLength = -1 }).
WithBytes([]byte("baz")).
Expect().
Status(413).
Body().Contains(vfs.ErrFileTooBig.Error())

storage := testInstance.VFS()
_, err := readFile(storage, "/toolarge")
assert.Error(t, err)

lifecycle.Patch(testInstance, &lifecycle.Options{DiskQuota: -1})
})

t.Run("UploadImage", func(t *testing.T) {
e := testutils.CreateTestClient(t, ts.URL)

Expand Down
4 changes: 2 additions & 2 deletions web/notes/notes.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ func UploadImage(c echo.Context) error {
return jsonapi.InvalidParameter(echo.HeaderContentLength, err)
}
if size > note.MaxImageWeight {
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", vfs.ErrFileTooBig)
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", vfs.ErrMaxFileSize)
}
used, err := inst.VFS().FilesUsage()
if err != nil {
Expand Down Expand Up @@ -472,7 +472,7 @@ func wrapError(err error) *jsonapi.Error {
return jsonapi.Conflict(err)
case os.ErrNotExist, vfs.ErrParentDoesNotExist, vfs.ErrParentInTrash:
return jsonapi.NotFound(err)
case vfs.ErrFileTooBig:
case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
case sharing.ErrMemberNotFound:
return jsonapi.NotFound(err)
Expand Down
2 changes: 1 addition & 1 deletion web/sharings/sharings.go
Original file line number Diff line number Diff line change
Expand Up @@ -904,7 +904,7 @@ func wrapErrors(err error) error {
return jsonapi.PreconditionFailed("Content-Length", err)
case vfs.ErrConflict:
return jsonapi.Conflict(err)
case vfs.ErrFileTooBig:
case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
case permission.ErrExpiredToken:
return jsonapi.BadRequest(err)
Expand Down
2 changes: 1 addition & 1 deletion web/shortcuts/shortcuts.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func wrapError(err error) *jsonapi.Error {
switch err {
case os.ErrNotExist, vfs.ErrParentDoesNotExist, vfs.ErrParentInTrash:
return jsonapi.NotFound(err)
case vfs.ErrFileTooBig:
case vfs.ErrFileTooBig, vfs.ErrMaxFileSize:
return jsonapi.Errorf(http.StatusRequestEntityTooLarge, "%s", err)
case shortcut.ErrInvalidShortcut:
return jsonapi.BadRequest(err)
Expand Down

0 comments on commit 60bc67f

Please sign in to comment.