File Uploads

AdonisJs processes file uploads securely without wasting server resources.

Basic Example

Let’s see how to handle files uploaded via HTML form:

<form method="POST" action="upload" enctype="multipart/form-data">
  <input type="file" name="profile_pic" />
  <button type="submit"> Submit </button>
</form>
start/routes.js
const Helpers = use('Helpers')

Route.post('upload', async ({ request }) => {
  const profilePic = request.file('profile_pic', {
    types: ['image'],
    size: '2mb'
  })

  await profilePic.move(Helpers.tmpPath('uploads'), {
    name: 'custom-name.jpg',
    overwrite: true
  })

  if (!profilePic.moved()) {
    return profilePic.error()
  }
  return 'File moved'
})
  1. The request.file method accepts two arguments (a field name and a validation object to apply to the uploaded file), and returns a File instance.

  2. Next, we call the profilePic.move method which attempts to move the file to the defined directory (in this case, called with options to save the file with a new name and to overwrite the file if needed).

  3. Finally, we check whether the move operation was successful by calling the profilePic.moved() method (returning errors if any were found).

Multiple File Uploads

AdonisJs makes uploading multiple files as simple as uploading a single file.

When multiple files are uploaded together, request.file returns a FileJar instance instead of a File instance:

<form method="POST" action="upload" enctype="multipart/form-data">
  <input type="file" name="profile_pics[]" multiple />
  <button type="submit"> Submit </button>
</form>
start/routes.js
const Helpers = use('Helpers')

Route.post('upload', async ({ request }) => {
  const profilePics = request.file('profile_pics', {
    types: ['image'],
    size: '2mb'
  })

  await profilePics.moveAll(Helpers.tmpPath('uploads'))

  if (!profilePics.movedAll()) {
    return profilePics.errors()
  }
})

In the example above, compared to the way we handle a single file:

  1. Instead of move, we use the moveAll method (which moves all uploaded files in parallel to a given directory).

  2. Single file methods have been changed to multiple file methods (such as moved → movedAll and error → errors).

Changing file names

To move and rename a single file upload, pass an options object to the move method defining the file’s new name:

await profilePic.move(Helpers.tmpPath('uploads'), {
  name: 'my-new-name.jpg'
})

To move and rename multiple file uploads, pass a callback to the moveAll method to create a custom options object for each file inside your FileJar instance:

profilePics.moveAll(Helpers.tmpPath('uploads'), (file) => {
  return {
    name: `${new Date().getTime()}.${file.subtype}`
  }
})

Moved list

When moving multiple file uploads, it’s possible some files move successfully while others reject due to validation failures.

In that case, you can use the movedAll() and movedList() methods to optimize your workflow:

const removeFile = Helpers.promisify(fs.unlink)

if (!profilePics.movedAll()) {
  const movedFiles = profilePics.movedList()

  await Promise.all(movedFiles.map((file) => {
    return removeFile(path.join(file._location, file.fileName))
  }))

  return profilePics.errors()
}

Validation Options

The following validation options can be passed to validate a file before completing a move operation:

Key Value Description

types

String[]

An array of types to be allowed. The value will be checked against the file media type.

size

String OR Number

The maximum size allowed for the file. The value is parsed using the bytes.parse method.

extnames

String[]

To have to more granular control over the file type, you can define the allowed extensions over defining the type.

An example of how to apply validation rules is as follows:

const validationOptions = {
  types: ['image'],
  size: '2mb',
  extnames: ['png', 'gif']
}
const avatar = request.file('avatar', validationOptions)

// this is when validation occurs
await avatar.move()

Error Types

When upload validation fails, the File error method returns an object containing the failed fieldName, original clientName, an error message, and the rule type that triggered the error.

The FileJar errors method returns an array of errors.

Are few example error objects are listed below.

Type error

{
  fieldName: "field_name",
  clientName: "invalid-file-type.ai",
  message: "Invalid file type postscript or application. Only image is allowed",
  type: "type"
}

Size error

{
  fieldName: "field_name",
  clientName: "invalid-file-size.png",
  message: "File size should be less than 2MB",
  type: "size"
}

File Properties

The following file properties can be accessed on the File instance:

Property Unprocessed Inside tmp Moved

clientName File name on client machine

String

String

String

fileName File name after move operation

null

null

String

fieldName Form field name

String

String

String

tmpPath Temporary path

null

String

String

size File size in bytes

0

Number

Number

type File primary type

String

String

String

subtype File sub type

String

String

String

status File status (set to error when failed)

pending

consumed

moved

extname File extension

String

String

String

Route Validators

Route validators validate uploaded files before passing them to the controller.

In the example route validator below:

app/Validators/StoreUser.js
'use strict'

class StoreUser {
  get rules () {
    return {
      avatar: 'file|file_ext:png,jpg|file_size:2mb|file_types:image'
    }
  }
}

module.exports = StoreUser
  1. The file rule ensures the avatar field is a valid File.

  2. The file_ext rule defines the extnames allowed for the file.

  3. The file_size rule defines the maximum size for the file.

  4. The file_types rule defines the types allowed for the file.

Streaming Files

The majority of upload libraries/frameworks process files multiple times when streaming to an external service such as Amazon S3. Their upload workflows are usually designed like so:

  1. Process request files then save them to the tmp directory.

  2. Move each file from the tmp directory to the destination directory.

  3. Use the external service’s SDK to finally stream the file to the external service.

This process wastes server resources reading/writing single files multiple times.

AdonisJs makes the process of streaming uploaded files far more efficient.

Disable auto-processing

First, disable file auto-processing for your upload routes via the config/bodyparser.js file:

config/bodyparser.js
processManually: ['/upload']

The processManually option takes an array of routes or route patterns for which files should not be processed automatically.

Process the stream

Finally, call the request.multipart.process method inside the file upload controller/route handler:

start/routes.js
const Drive = use('Drive')

Route.post('upload', async ({ request }) => {

  request.multipart.file('profile_pic', {}, async (file) => {
    await Drive.disk('s3').put(file.clientName, file.stream)
  })

  await request.multipart.process()
})
You must call await request.multipart.process() to start processing uploaded files.

The request.multipart.file method lets you select a specific file and access its readable stream via the file.stream property so you can pipe the stream to Amazon S3 or any other external service you want.

The entire process is asynchronous and processes the file(s) only once.