Pechkin

Asynchronous Node.js file upload (multipart/form-data) handling.

README


Pechkin


Pechkin is a modern, asynchronous, flexible and configurable Node.js library for handling file uploads (i.e. multipart/form-data requests), written in TypeScript. It's perfect for complex usecases requiring lots of flexibility with fields and multiple files mixed together.

Features

- Fast, based on [busboy](https://www.npmjs.com/package/busboy).
- No temporary files are created, files are not loaded in memory.
- Asynchronous, Promise- and AsyncIterator-based. Fields and each file are available as Promises as soon as they're parsed.
- Flexible: you don't need to provide any storage engines, file handlers, etc. Pechkin only provides the parsed data in form of streams and promises, and you can do whatever you want with it.
- Highly configurable, with possibility to override some configuration options per-field (e.g. maxFileByteLength: 1MB for all files, but 5MB for file fieldname my_custom_video_file).
- Expressive TypeScript typings.
- Robust error handling: you can be sure that all errors have been caught, handled, and underlying resources (streams) were properly handled/closed.
- Only 1 dependency (busboy).

CHANGELOG


Check for tips on migration from v1.x to v2.x.

Requirements


- Node.js v13.6.0+ ([events.on() dependency](https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V13.md#13.6.0))

Installation


  1. ```npm install pechkin```

  2. ### Examples / Usage

  3. #### TL;DR

  4. - All fields in the `FormData` request should come before any files. Any fields submitted after the first file are lost.
  5. - `parseFormData()` returns a `Promise` that resolves when all fields are parsed, and the first file is encountered (or the request ended).
  6. - The promise contains a populated `fields` object, and a `files` `AsyncIterator`/`AsyncIterable`.
  7. - Asynchronously iterate over the `files` using the `for-await-of` loop or using the `next()` method.
  8. - File streams should always be consumed (e.g. by the code inside `for-await-of` loop, or before the subsequent `next()` call). Otherwise the request parsing will stall.

  9. #### FOR FULL WORKING EXAMPLES, SEE THE `examples/` FOLDER


  10. **Importing**

  11. The package provides both CommonJS and ESM modules.

  12. ```js
// ESM: index.mjs

import * as pechkin from 'pechkin';
// or
import { parseFormData } from 'pechkin';

// CommonJS: index.cjs

const pechkin = require('pechkin');
// or
const { parseFormData } = require('pechkin');
  1. ```

  2. #### Save to file system
  3. **Files are processed sequentially.**

  4. ```js

// Full working example: examples/fs.js

http.createServer(async (req, res) => {
  const { fields, files } = await pechkin.parseFormData(req, {
    maxTotalFileFieldCount: Infinity,
    maxFileCountPerField: Infinity,
    maxTotalFileCount: Infinity
  });

  const results = [];

  for await (const { filename: originalFilename, stream, ...file } of files) {
    const newFilename = ${Math.round(Math.random() * 1000)}-${originalFilename};
    const dest = path.join(os.tmpdir(), newFilename);

    // Pipe the stream to a file
    // The stream will start to be consumed after the current block of code
    // finishes executing...
    stream.pipe(fs.createWriteStream(dest));
    
    // ...which allows us to set up event handlers for the stream and wrap
    // the whole thing in a Promise, so that we can get the stream's length.
    const length = await new Promise((resolve, reject) => {
      // Since Node v15.0.0, you can use stream.finished(), instead of
      // manually setting up event listeners and resolving/rejecting inside
      // them.
      // https://nodejs.org/api/stream.html#streamfinishedstream-options
      stream
        // stream is an instance of Transform, which is a Duplex stream,
        // which means you can listen to both 'end' (Readable side)
        // and 'finish' (Writable side) events.
        .on('end', () => resolve(stream.bytesWritten))
        .on('finish', () => resolve(stream.bytesWritten))
        // You can either reject the Promise and handle the Promise rejection
        // using .catch() or await + try-catch block, or you can directly
        // somehow handle the error in the 'error' event handler.
        .on('error', reject);
    })

    results.push({ ...file, dest, originalFilename, newFilename, length});
  }

  console.log(results);

  /*
  OUTPUT:

  {
    "fields": { [fieldname: string]: string },
    "files": [
      {
        "field": string,
        "filename": string,
        "mimeType": string,
        "dest": string,
        "originalFilename": string,
        "newFilename": string,
        "length": number
      },
      ...
    ],
  }
  */
});
  1. ```