diff --git a/.changeset/red-snails-tap.md b/.changeset/red-snails-tap.md new file mode 100644 index 0000000000..cd53efea2c --- /dev/null +++ b/.changeset/red-snails-tap.md @@ -0,0 +1,5 @@ +--- +'@directus/api': minor +--- + +Added a new `FILES_MAX_UPLOAD_SIZE` environment variable for setting a max value system-wide diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index cdfd013dec..61c7b00dc4 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -1,11 +1,13 @@ import formatTitle from '@directus/format-title'; import { toArray } from '@directus/utils'; import Busboy from 'busboy'; +import bytes from 'bytes'; import type { RequestHandler } from 'express'; import express from 'express'; import Joi from 'joi'; import path from 'path'; import env from '../env.js'; +import { ContentTooLargeException } from '../exceptions/content-too-large.js'; import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js'; import { respond } from '../middleware/respond.js'; import useCollection from '../middleware/use-collection.js'; @@ -34,7 +36,14 @@ export const multipartHandler: RequestHandler = (req, res, next) => { }; } - const busboy = Busboy({ headers, defParamCharset: 'utf8' }); + const busboy = Busboy({ + headers, + defParamCharset: 'utf8', + limits: { + fileSize: env['FILES_MAX_UPLOAD_SIZE'] ? bytes(env['FILES_MAX_UPLOAD_SIZE'] as string) : undefined, + }, + }); + const savedFiles: PrimaryKey[] = []; const service = new FilesService({ accountability: req.accountability, schema: req.schema }); @@ -88,6 +97,12 @@ export const multipartHandler: RequestHandler = (req, res, next) => { // Clear the payload for the next to-be-uploaded file payload = {}; + fileStream.on('limit', () => { + const error = new ContentTooLargeException(`Uploaded file is too large`); + fileStream.emit('error', error); + next(error); + }); + try { const primaryKey = await service.uploadOne(fileStream, payloadWithRequiredFields, existingPrimaryKey); savedFiles.push(primaryKey); diff --git a/api/src/env.ts b/api/src/env.ts index 510d7daef5..3214b53a47 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -107,6 +107,10 @@ const allowedEnvironmentVars = [ 'STORAGE_.+_HEALTHCHECK_THRESHOLD', // metadata 'FILE_METADATA_ALLOW_LIST', + + // files + 'FILES_MAX_UPLOAD_SIZE', + // assets 'ASSETS_CACHE_TTL', 'ASSETS_TRANSFORM_MAX_CONCURRENT', diff --git a/api/src/exceptions/content-too-large.ts b/api/src/exceptions/content-too-large.ts new file mode 100644 index 0000000000..2e64ade257 --- /dev/null +++ b/api/src/exceptions/content-too-large.ts @@ -0,0 +1,7 @@ +import { BaseException } from '@directus/exceptions'; + +export class ContentTooLargeException extends BaseException { + constructor(message: string) { + super(message, 413, 'CONTENT_TOO_LARGE'); + } +} diff --git a/api/src/services/files.ts b/api/src/services/files.ts index 7d2fd2bf63..dc9d864f5c 100644 --- a/api/src/services/files.ts +++ b/api/src/services/files.ts @@ -87,6 +87,9 @@ export class FilesService extends ItemsService { } catch (err: any) { logger.warn(`Couldn't save file ${payload.filename_disk}`); logger.warn(err); + + await this.deleteOne(primaryKey); + throw new ServiceUnavailableException(`Couldn't save file ${payload.filename_disk}`, { service: 'files' }); } diff --git a/app/src/components/v-upload.vue b/app/src/components/v-upload.vue index abca5eadd3..2f7e5687fe 100644 --- a/app/src/components/v-upload.vue +++ b/app/src/components/v-upload.vue @@ -197,6 +197,7 @@ function useUpload() { } } catch (err: any) { unexpectedError(err); + emit('input', null); } finally { uploading.value = false; done.value = 0; diff --git a/app/src/lang/translations/en-US.yaml b/app/src/lang/translations/en-US.yaml index f58ad81a9c..016ef4e045 100644 --- a/app/src/lang/translations/en-US.yaml +++ b/app/src/lang/translations/en-US.yaml @@ -626,6 +626,7 @@ errors: RANGE_NOT_SATISFIABLE: Invalid range METHOD_NOT_ALLOWED: Method not allowed REQUESTS_EXCEEDED: Requests limit reached + CONTENT_TOO_LARGE: Submitted item/file is too large security: Security value_hashed: Value Securely Hashed bookmark_name: Bookmark name... diff --git a/docs/self-hosted/config-options.md b/docs/self-hosted/config-options.md index 981637b508..505a0445b5 100644 --- a/docs/self-hosted/config-options.md +++ b/docs/self-hosted/config-options.md @@ -680,6 +680,12 @@ purposes, collection of additional metadata must be configured: [1]: Extracting all metadata might cause memory issues when the file has an unusually large set of metadata +### Upload Limits + +| Variable | Description | Default Value | +| ----------------------- | ------------------------------------------------------------------- | ------------- | +| `FILES_MAX_UPLOAD_SIZE` | Maximum file upload size allowed. For example `10mb`, `1gb`, `10kb` | | + ## Assets | Variable | Description | Default Value |