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'
  })

  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 new name.

  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.

Moved list

Since some files have been moved successfully, we can optimize our workflow by processing those file or deleting them, so that there are no duplicate files.

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()
}

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

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.fileName, 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.