Koa - Upload files using koa-multer (upload restrictions, error handling)

Keywords: Javascript JQuery npm IE github

Preface

Uploading files is a very common operation in development. Today I choose to use koa-multer middleware to achieve this function. In addition to uploading files, I will also restrict file upload and handle upload errors.

Since the original koa-multer has stopped maintenance, we need to use the latest @koa/multer. This module is a branch of koa-multer, which is branched into the official Koa organization and provided under the @koa/multer package name.

@ koa/multer depends on multer. When installing multer, install multer together. The installation command is as follows

 npm install --save @koa/multer multer

 

Upload files

Front-end code:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <input type="file" id="file" accept="image/*"/>
    <button id="submit">Submission</button>
</body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
    $(function(){
        $('#submit').click(()=>{
            let file = $('#file')[0].files[0]
            let formData = new FormData()
            formData.set('file',file)
            formData.set('name',file.name)
            formData.set('timestamp',Date.now())
            $.ajax({
                url:'http://localhost:3000/user/file',
                type:'post',
                data: formData,
                cache: false,
                contentType: false,
                processData:false,
                success(res){
                    console.log(res)
                }
            })
        })
    })
</script>
</html>

node code:

const Koa = require('koa')
const Router = require('koa-router')
const route = new Router()
const multer = require('@koa/multer')
const path = require('path')

//Upload File Storage Path and File Naming
const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        cb(null, path.join(__dirname ,'/public'))
    },
    filename: function (req, file, cb) {
        let type = file.originalname.split('.')[1]
        cb(null, `${file.fieldname}-${Date.now().toString(16)}.${type}`)
    }
})
//File upload restrictions
const limits = {
    fields: 10,//Number of non-file fields
    fileSize: 500 * 1024,//File Size Unit b
    files: 1//Number of documents
}
const upload = multer({storage,limits})

route.post('/user/file', upload.single('file'), async (ctx,next)=>{
    ctx.body = {
        code: 1,
        data: ctx.file
    }
})

app.use(router.routes()).use(router.allowedMethods())

app.listen(3000)

What did the above code do?

1. The folder that stores the uploaded files needs to exist. Here I create a public folder to save the files.

2. Uploaded files do not have suffix names by default, so they need to be added manually; in order to name them without duplication, I use the time stamp to be converted to hexadecimal as the file name.

3. Restrict file upload. Specifying restrictions can help protect your site from DoS attacks.

4. Use Middleware in the routing of uploaded files. Because I upload only one file here, use the single method. The single method accepts a string. This string is the field name of uploaded files. In addition, uploaded multiple files can use array and fileds.

5. In routing, the uploaded file information can be obtained by ctx.file, and multi-file upload can be obtained by ctx.files.

After successful upload, you can see the uploaded files in the folder

For more configuration and method usage, please refer to: https://github.com/expressjs/multer/blob/master/doc/README-zh-cn.md

 

Upload error handling

If the field of the front-end upload file is inconsistent with the field of the back-end configuration, it will cause an error, and node.js will rush directly. In order to handle some unexpected errors when uploading files, we need to make some error handling.

@ koa/multer is based on the Koa version encapsulated by multer, so multer's error handling is not applicable in koa. The document description of multer's error handling is as follows:

 

I've tried this method too, and it really doesn't catch errors.

 

After online search and official documents, no similar error handling methods were found. Later, only by looking at the @koa/multer source code to find some solutions.

You can see that @koa/multer encapsulates about 60 lines of multer code. Here is the encapsulated code:

const originalMulter = require('multer');

function multer(options) {
  const m = originalMulter(options);

  makePromise(m, 'any');
  makePromise(m, 'array');
  makePromise(m, 'fields');
  makePromise(m, 'none');
  makePromise(m, 'single');

  return m;
}

function makePromise(multer, name) {
  if (!multer[name]) return;

  const fn = multer[name];

  multer[name] = function() {
    const middleware = fn.apply(this, arguments);

    return (ctx, next) => {
      return new Promise((resolve, reject) => {
        middleware(ctx.req, ctx.res, err => {
          if (err) return reject(err);
          if ('request' in ctx) {
            if (ctx.req.body) {
              ctx.request.body = ctx.req.body;
              delete ctx.req.body;
            }

            if (ctx.req.file) {
              ctx.request.file = ctx.req.file;
              ctx.file = ctx.req.file;
              delete ctx.req.file;
            }

            if (ctx.req.files) {
              ctx.request.files = ctx.req.files;
              ctx.files = ctx.req.files;
              delete ctx.req.files;
            }
          }

          resolve(ctx);
        });
      }).then(next);
    };
  };
}

multer.diskStorage = originalMulter.diskStorage;
multer.memoryStorage = originalMulter.memoryStorage;

module.exports = multer;

Because the koa grammar is async/await, encapsulation is to use Promise instead of callback, and then I see a piece of code to give me a clue.

 

Is this encapsulated error handling very similar to the original version of error handling, then we can catch the error by then/catch. After several attempts, the routing code is changed to the following form:

route.post('/user/file', async (ctx,next)=>{
    let err = await upload.single('file')(ctx, next)
                .then(res=>res)
                .catch(err=>err)
    if(err){
        ctx.body = {
            code:0,
            msg : err.message
        }
    }else{
        ctx.body = {
            code:1,
            data:ctx.file
        }
    }
})

What changes have I made?

1. Change the way of using middleware to manual method call. The single method returns a function, which corresponds to the function of the screenshot above. So it needs to be executed by ctx and next. After execution, it returns Promise and catch es errors.

2. Use the err variable to accept the uploaded results. Only the uploaded errors, err will be assigned as an error information object, otherwise undefined. By judging whether err exists, we can know whether errors have occurred.

Posted by Penelope on Sun, 01 Sep 2019 03:41:19 -0700