File Uploads

Uploading content from your users browser to your server can be a tricky task, however, horsepower makes it easy to do this by allowing you to use a storage driver.

To make a minimalist upload application, we will need to create a few things: two mix files and one controller. The mix files will render a form and a success page, while the controller will process the upload request.

The Routes

We will start by modifying our routes file, lets define three routes that will handle a file upload, these routes will do the following:

  • Render a page to display a form
  • Process the file upload
  • Render a page to display a success message
const { Router } = require('@horsepower/router')

// Create an upload group to contain the routes within a group
// to help separate the the uploads from the rest of the app.
Router.group('/upload', () => {

  // This route will display the form for uploading
  // Path: GET /upload
  Router.get('/', client => client.response.render('upload.mix')).name('upload-form')

  // This route will handle the processing of the file.
  // Path: POST /upload/handle
  Router.post('/handle', 'upload@handle').name('upload-handler')

  // This route will display the success page.
  // Path: GET /upload/success
  Router.get('/success', client => client.response.render('success.mix')).name('upload-success')
})

The Upload Form

Lets create a mix file located at resources/views/upload.mix that will display a form when the route is accessed allowing the user to upload an image by selecting one from the their own computer.

<html>
  <head><title>Upload File</title></head>
  <body>
    <form method="post" action="{{route('upload-handler')}}" enctype="multipart/form-data">
      <p><input type="file" name="image" accept="image/*" required></p>
      <p><input type="submit" value="Upload Image"></p>
    </form>
  </body>
</html>

Remember to add the enctype="multipart/form-data" attribute to your form tag otherwise the file will not be sent to the server.

The Upload Handler

The storage driver has a built in disk called tmp that points to the operating system's tmp directory. We will use that here to move the file from the tmp directory to the actual location of where we want the file to be stored.

There are multiple commands that move a file from one storage driver to another. The best one to use when uploading a file is moveFrom(), as this will also remove the file from the source directory. Using copyFrom() will keep the file within the source directory.

const path = require('path')
const { Storage } = require('@horsepower/storage')

// module.exports.main = ...

module.exports.handle = async function (client) {
  // Get the file information from the file upload
  // This is just information about where the file is
  // located on the server and not the actual file itself.
  let img = client.data.files('image')

  // If there was no file attached to the request
  // redirect the user back to the upload form.
  if (!img) return client.response.to('upload-form')

  // This will move the file from tmp storage to the location
  // of where we want to actually store the file. In this case,
  // we want to store the file in local storage.
  await Storage.mount('local').moveFrom('tmp', img.tmpStoragePath, path.join('test', img.filename))

  // Redirect the user to a success page
  return client.response.redirect.to('upload-success')
}

// module.exports.success = ...

The Success Page

Once the upload completes, we will let the user know by rendering a page that lets the user know that their upload completed successfully. The page will also display a link that will allow the user to upload another image.

Create this simple mix file located at resources/views/success.mix to show the success message to the user.

<html>
  <head><title>Upload Success</title></head>
  <body>
    <h1>Success</h1>
    <p>Your file was successfully uploaded!</p>
    <p><a href="{{route('upload-form')}}">Upload another file</a></p>
  </body>
</html>