File uploads

File uploads

AdonisJS has first-class support for processing user-uploaded files sent using the multipart/form-data content type. The files are auto-processed using the bodyparser middleware and saved inside your operating system's tmp directory.

Later, inside your controllers, you may access the files, validate them and move them to a persistent location or a cloud storage service like S3.

Working with user-uploaded files

You may access the user-uploaded files using the request.file method. The method accepts the field name and returns an instance of MultipartFile.

import { HttpContext } from '@adonisjs/core/http'
export default class UserAvatarsController {
update({ request }: HttpContext) {
const avatar = request.file('avatar')
console.log(avatar)
}
}

If a single input field is used to upload multiple files, you may access them using the request.files method. The method accepts the field name and returns an array of MultipartFile instances.

import { HttpContext } from '@adonisjs/core/http'
export default class InvoicesController {
update({ request }: HttpContext) {
const invoiceDocuments = request.files('documents')
for (let document of invoiceDocuments) {
console.log(document)
}
}
}

Validating files

You may validate files using the validator or define the validation rules via the request.file method.

In the following example, we will define the validation rules inline via the request.file method and use the file.errors property to access the validation errors.

const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
if (!avatar.isValid) {
return response.badRequest({
errors: avatar.errors
})
}

When working with an array of files, you can iterate over files and check if one or more files have failed the validation.

The validation options provided to the request.files method are applied to all files. In the following example, we expect each file to be under 2mb and must have one of the allowed file extensions.

const invoiceDocuments = request.files('documents', {
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
/**
* Creating a collection of invalid documents
*/
let invalidDocuments = invoiceDocuments.filter((document) => {
return !document.isValid
})
if (invalidDocuments.length) {
/**
* Response with the file name and errors next to it
*/
return response.badRequest({
errors: invalidDocuments.map((document) => {
name: document.clientName,
errors: document.errors,
})
})
}

Using validator

Instead of validating files manually (as seen in the previous section), you may use the validator to validate files as part of the validation pipeline. You do not have to manually check for errors when using the validator; the validation pipeline takes care of that.

// app/validators/user_validator.ts
import vine from '@vinejs/vine'
export const updateAvatarValidator = vine.compile(
vine.object({
avatar: vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
})
)
import { HttpContext } from '@adonisjs/core/http'
import { updateAvatarValidator } from '#validators/user_validator'
export default class UserAvatarsController {
update({ request }: HttpContext) {
const { avatar } = await request.validateUsing(
updateAvatarValidator
)
}
}

Using validator to validate multiple files

An array of files can be validated using the vine.array type. For example:

import vine from '@vinejs/vine'
export const createInvoiceValidator = vine.compile(
vine.object({
documents: vine.array(
vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
)
})
)

Moving files to a persistent location

By default, the user-uploaded files are saved in your operating system's tmp directory and may get deleted as your computer cleans up the tmp directory.

Therefore, it is recommended to store files in a persistent location. You may use the file.move to move a file within the same filesystem. The method accepts an absolute path to the directory to move the file.

import app from '@adonisjs/core/services/app'
const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
await avatar.move(app.makePath('uploads'))

Also, it is recommended to provide a unique random name to the moved file. For this, you may use the cuid helper.

import { cuid } from '@adonisjs/core/helpers'
import app from '@adonisjs/core/services/app'
await avatar.move(app.makePath('uploads'), {
name: `${cuid()}.${avatar.extname}`
})

Once the file has been moved, you may store its name inside the database for later reference.

await avatar.move(app.makePath('uploads'))
auth.user!.avatarFileName = avatar.fileName!
await auth.user.save()

File properties

Following is the list of properties you may access on the MultipartFile instance.

PropertyDescription
fieldNameThe name of the HTML input field.
clientNameThe file name on the user's computer.
sizeThe size of the file in bytes.
extnameThe file extname
errorsAn array of errors associated with a given file.
typeThe mime type of the file
subtypeThe mime subtype of the file.
filePathThe absolute path to the file after the move operation.
fileNameThe file name after the move operation.
tmpPathThe absolute path to the file inside the tmp directory.
metaMetadata associated with the file as a key-value pair. The object is empty by default.
validatedA boolean to know if the file has been validated.
isValidA boolean to know if the file has passed the validation rules.
hasErrorsA boolean to know if one or more errors are associated with a given file.

Serving files

If you have persisted user-uploaded files in the same filesystem as your application code, you may serve files by creating a route and using the response.download method.

import { sep, normalize } from 'node:path'
import app from '@adonisjs/core/services/app'
import router from '@adonisjs/core/services/router'
const PATH_TRAVERSAL_REGEX = /(?:^|[\\/])\.\.(?:[\\/]|$)/
router.get('/uploads/*', ({ request, response }) => {
const filePath = request.param('*').join(sep)
const normalizedPath = normalize(filePath)
if (PATH_TRAVERSAL_REGEX.test(normalizedPath)) {
return response.badRequest('Malformed path')
}
const absolutePath = app.makePath('uploads', normalizedPath)
return response.download(absolutePath)
})
  • We get the file path using the wildcard route param and convert the array into a string.
  • Next, we normalize the path using the Node.js path module.
  • Using the PATH_TRAVERSAL_REGEX we protect this route against path traversal.
  • Finally, we convert the normalizedPath to an absolute path inside the uploads directory and serve the file using the response.download method.

Self-processing multipart stream

You can turn off the automatic processing of multipart requests and self-process the stream for advanced use cases. Open the config/bodyparser.ts file and change one of the following options to disable auto-processing.

{
multipart: {
/**
* Set to false, if you want to self-process all the
* multipart stream
*/
autoProcess: false
}
}
{
multipart: {
/**
* Define an array of route patterns for which you want to self
* process the multipart stream
*/
processManually: ['/assets']
}
}

Once you have disabled the auto-processing, you can use the request.multipart object to process individual files.

In the following example, we use the stream.pipeline method from Node.js to process the multipart readable stream and write it to a file on the disk. However, you can stream this file to some external service like s3.

import { createWriteStream } from 'node:fs'
import app from '@adonisjs/core/services/app'
import { pipeline } from 'node:stream/promises'
import { HttpContext } from '@adonisjs/core/http'
export default class AssetsController {
async store({ request }: HttpContext) {
/**
* Step 1: Define a file listener
*/
request.multipart.onFile('*', {}, async (part, reporter) => {
part.pause()
part.on('data', reporter)
const filePath = app.makePath(part.file.clientName)
await pipeline(part, createWriteStream(filePath))
return { filePath }
})
/**
* Step 2: Process the stream
*/
await request.multipart.process()
/**
* Step 3: Access processed files
*/
return request.allFiles()
}
}
  • The multipart.onFile method accepts the input field name for which you want to process the files. You can use the wildcard * to process all the files.

  • The onFile listener receives the part (readable stream) as the first parameter and a reporter function as the second parameter.

  • The reporter function is used to track the stream progress so that AdonisJS can provide you access to the processed bytes, file extension, and other meta-data after the stream has been processed.

  • Finally, you can return an object of properties from the onFile listener, and they will be merged with the file object you access using the request.file or request.allFiles() methods.

Error handling

You must listen to the error event on the part object and handle the errors manually. Usually, the stream reader (the writeable stream) will internally listen for this event and abort the write operation.

Validating files

AdonisJS allows you to validate the files even when you process the multipart stream manually. In case of an error, the error event is emitted on the part object.

The multipart.onFile method accepts the validation options as the second parameter. Also, make sure to listen for the data event and bind the reporter method to it. Otherwise, no validations will occur.

request.multipart.onFile('*', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
}, async (part, reporter) => {
/**
* The following two lines are required to perform
* the stream validation
*/
part.pause()
part.on('data', reporter)
})