[ANN] Image Resize App on Glitch

by Andrew Chilton


Posted a year ago in category apps.


Firstly, I'd like to announce a new app called Image Resize. As is customary, here are all the links you need for this app:

Now that you have all the links, feel free to view the source code, or remix it yourself so you can use it and follow along.

About

The image-resize app allows you to simply scale an image by choosing a width, uploading an image, and saving the result. It works on images up to 50MB and with over 20,000 pixels in both dimensions. That's a whopping 400,000,000 pixels.

Aims

The main two things I wanted to try here were file uploads and resizing images.

For file uploads I used a package called multer. Though I've used it many times before but I just wanted to get a feel for file uploads on Glitch, like how long they take and where to save them.

And for resizing images, though I've used packages like imagemagick, gm (graphics magick), and used node's exec package to shell-out to other image manipulation programs, for this project I wanted to use sharp.

Uploading Files

Firstly, let's talk about using multer and how that fits into the Glitch environment. As we know we are able to use the .data/ directory to save files that do not appear in the Editor. So, to be honest (and this is my mistake), I actually used this dir to save the uploads.

Whilst this seemed okay at the time, just remember that your /app mount point is only allowed up to 128MB of storage as shown below.

$ df -kh
Filesystem      Size  Used Avail Use% Mounted on
/dev/rbd59      128M   24M   79M  23% /app

Also remember that the /app area (which includes the .data directory here : /app/.data/) is permanent and is very much a sacred area for the files you really don't want to lose. For temporary files such as those uploaded this area is both overkill and unecessary.

And, ahah, re-reading that last sentence gives us a clue. The uploaded files should be temporary, so let's have another look at one specific line from the output of the df command above:

$ df -kh
Filesystem      Size  Used Avail Use% Mounted on
/dev/xvda1       49G   22G   28G  44% /tmp

And there we have it. A whopping 28GB we can play with! We're onto a winner.

So firstly, let's set up multer to use this area for file uploads:

const multer = require('multer')

const tmpDir = '/tmp/image-resize'
const upload = multer({ dest: tmpDir })

In our form, we're going to have the following file input such that the uploaded file is in the field named image. Since we're POSTing the form to '/' then we want multer to do it's thing prior to our own route handler running. Let's try this:

app.post("/", upload.single('image'), (req, res, next) => {
  // .. our route handler ...
})

This means that multer parses the incoming file, stores the contents inside /tmp/image-resize and sets the file information in req.file (since we told it to only expect a single file, not multiple files). Inside the route handler, we can access the file info in req.file as mentioned and here is an example of that:

file: {
  fieldname: 'image',
  originalname: 'pexels-photo.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: '/tmp/image-resize',
  filename: 'c77969beaf061313863216dbdc86997a',
  path: '/tmp/image-resize/c77969beaf061313863216dbdc86997a',
  size: 2468354
}

Great! Now we can see where the file has been placed req.file.destination and named req.file.filename by multer. Or for the full path we use req.file.path. It is this that gives us the filename we can use for sharp to use.

Reszing the Image

Now that the (slightly complicated) uploading is done, the resize is actually quite simple, so we'll just get straight to it. Imagine we've checked that the width is a valid number and we'll talk about outfile a bit later. Essentially once we have an outfile we'll serve it back to the user:

  sharp(req.file.path)
    .rotate()
    .resize(width)
    .toFile(outfile)
    .then(() => {
      res.sendFile(outfile, (err) => {
        if (err) return next(err)
      })
    })

As you can see, I'm using the promise version of sharp rather than callbacks since it made it easier to cope with all (currently omited) error conditions. But wait, what does happen if there is an error. We'll check right here:

    .catch(err => {
      next(err) 
     })

One thing to note here is that multer doesn't automatically remove any files itself after the request has finished, let alone the new file we've just written, so let's deal with those. What about if we set a timer for 5s after the request has finished to delete them. This sounds reasonable such that the files don't hang around for long, but we don't delete them too early either.

However, let's just think about the anatomy of a request. We only enter our handler when multer has completely written the file to disk, so we want to set our time to delete that file as soon as we know the image manipulation has finished (either correctly, or it has failed). Let's see what this looks like in the error case (imagine someone has uploaded a file named text.jpg which is a misnamed text file):

    .catch(err => {
      setTimeout(() => {
        fs.unlink(filename, (err) => console.log)
      }, 5 * 1000)
      next(err) 
     })

Remember that we don't need to remove the new outfile if processing fails since nothing will have been written to it.

And finally, for the happy path we want to remove both the original and the new file, but because we know when we've finished with either we can remove them at different times, such as:

      res.sendFile(outfile, (err) => {
        setTimeout(() => {
          fs.unlink(filename, (err) => console.log)
        }, 5 * 1000)
        if (err) return next(err)
        console.log('Sent:', outfile)
        setTimeout(() => {
          fs.unlink(outfile, (err) => console.log)
        }, 5 * 1000)
      })

Here' we're setting up a timer to remove the original as soon as we no longer need it (whether the res.sendFile() was successful or not). And as soon as the res.sendFile() has finished successfully we'll remove the outfile too.

Apart from some sanity checking of the incoming width parameter, and making sure that we only accept image/png and image/jpeg (and setting the extension correctly), we're pretty much done.

I hope you've enjoyed this post. Please follow us on @GlitchApps and I'd love to hear your feedback.


Tags: images.