File Uploads

File uploads have been reimagined in 4.0 and give you enough ways to process files securely and without wasting the server resources.

Basic example

Let’s see how to handle files uploaded by HTML form.

<form method="POST" action="upload" enctype="multipart/form-data">
  <input type="file" name="profile_pic" />
  <button type="submit"> Submit </button>
</form>

Registering route

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 an object of validations to be performed, before moving the file.

  2. Next, we call profilePic.move method, which attempts to move the file to the defined directory. You can call it with options to save file with a new name and to overwrite the file if needed.

  3. Finally, we check whether the move operation was successful or not using profilePic.moved() method and return errors (if any).

Multiple file uploads

Uploading multiple files at a time is as simple as uploading one file. The request.file method returns a File jar in case of multiple files.

<form method="POST" action="upload" enctype="multipart/form-data">
  <input type="file" name="profile_pics[]" multiple />
  <button type="submit"> Submit </button>
</form>
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()
  }
})

The route handler code has been changed a bit in case of multiple file uploads.

  1. Instead of using move, we make use of moveAll method. Which parallelly moves all of the uploaded files to a given directory.

  2. Also, other methods have been changed as moved → movedAll and error → errors.

Changing file names

When moving a single file, you can pass an object with name property on it. However, it can get tricky with multiple files.

AdonisJs allows you to pass a callback to moveAll method, which is executed for each file inside the fileJar.

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

Moved list

In case of multiple file uploads, it’s possible that only a few files are successfully moved, and other are rejected due to validation failures.

In that case, you can make use of 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 the file before complete the move operation.

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

// this is when validation occurs
await avatar.move()
Table 1. validationOptions
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 maxium size allowed for the file. The value is parsed using 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.

Errors

Following is the list of formats in which error(s) are returned.

When using FileJar the errors method returns an array of following errors.
Type error
{
  fieldName: "profile_pic",
  clientName: "GitHub-Mark.ai",
  message: "Invalid file type postscript or application. Only image is allowed",
  type: "type"
}
Size error
{
  fieldName: "profile_pic",
  clientName: "adonis_landing.sketch",
  message: "File size should be less than 2MB",
  type: "size"
}

Also, multiple file uploads errors method returns an array of errors in the same format.

File properties

Below is the list of file properties you can access 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 fails

pending

consumed

moved

extname File extension

String

String

String

Validating using Route Validator

Route validators offers a neat way to validate user data before passing it to the controller. You can also validate files using the route validator too.

'use strict'

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

module.exports = StoreUser
  1. The file rule ensures the 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

Majority of file uploading libraries/frameworks process the files for multiple times when you want to stream them to an external service like s3.

Here’s how the file uploading workflows are usually designed.

  1. Process all the request files and save them into the tmp directory.

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

  3. Use aws SDK and then stream the file to s3.

This process wastes a bunch of server resources since a single file is getting read and written for multiple times. On the other hand, AdonisJs makes the process of streaming files smooth and efficient.

Disable auto processing

The config file config/bodyparser.js has a section to disable auto-processing of files for some selected routes.

processManually: ['/upload']

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

Process inside the controller

Next thing we need to do is call the process method inside the controller/route handler.

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()
})
Always make sure to call await request.multipart.process() to start processing the files.

The request.multipart.file method lets you select a specific file, and the readable stream is accessed via file.stream property. Now you are free to consume the stream by piping it to s3 or any other service you want.

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