# Formidable Mini

Formidable Mini is a lightweight and modular implementation of the Formidable library, designed for
handling file uploads and form data parsing in Node.js applications. It provides a simplified API
while maintaining flexibility and extensibility. It's the same old `multipart` parser file, except
the bloat of Node Core streams handling and whatnot.

## Features

- **Lightweight**: Minimal dependencies and optimized for performance.
- **Modular**: Includes separate modules for multipart parsing, file handling, and more.
- **Extensible**: Supports hooks for custom behavior during parsing.
- **ESModules & Type-Safe**: Fully supports ESModule syntax, and TypeScript soon.
- **Modern**: Uses FormData and File APIs.
- **Secure**: No writes to disk, safe filenames with `cuid2`
- **Pure**: Doesn't mess with your Request stream, it just reads the headers.

## Installation

```bash
npm install formidable-mini
```

## Usage

### Basic Example

This example uses the familiar `form.parse(req, options)` method, which uses the `form.formData`
under the hood.

> ![NOTE] Better use the `form.formData(req, options)` because the `.parse` loads into memory, while
> the `.formData` streams it directly.

```javascript
import formidable from 'formidable-mini';
import http from 'http';

const server = http.createServer(async (req, res) => {
  if (req.method.toLowerCase() === 'post') {
    const form = formidable();

    try {
      // both files and fields are Javascript Sets
      // NOTE: This loads into memory
      const { files, fields } = await form.parse(req);
      conole.log({ files, fields });

      res.writeHead(200, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ files: Array.from(files), fields: Array.from(fields) }));
    } catch (err) {
      console.log('err:', err);

      res.writeHead(500, { 'Content-Type': 'text/plain' });
      res.end('Error parsing form data');
    }
  } else {
    res.writeHead(405, { 'Content-Type': 'text/plain' });
    res.end('Method Not Allowed');
  }
});

server.listen(3000, () => {
  console.log('Server listening on http://localhost:3000');
});
```

### Redirect upload to somewhere else (eg. S3)

```js
import { formDataToBlob } from 'formdata-polyfill/esm.min.js';

const blob = formDataToBlob(await form.formData(req));
const response = await fetch('https://httpbin.org/post', {
  method: 'POST',
  body: blob,
});

console.log('result:', await response.json());
```

### Using `form.formData`

In this example you can see how you can stream files and eventually write to disk, or somewhere
else.

```js
const form = formidable();

// Similar to WHATWG `request.fromData()`
const formData = await form.formData(req);

for (const [key, value] of formData) {
  const isFile = value instanceof FormidableFile;

  if (isFile) {
    console.log('file:', value);
    // NOTE: You can write to disk if you want.
    // const stream = Readable.from(value.stream());
    // stream.pipe(fs.createWriteStream(value.path));
  } else {
    console.log('key:', key);
  }
}
```

## API

### `formidable(options)`

Creates a new instance of the Formidable class with optional configuration.

- `formidable.isFile(val)` - check if a given `val` is of instance of `FormidableFile`; also named
  export `isFile`
- `formidable.writeToDisk(val, 'new-name')` - write a value (file) to disk; also named export
  `writeToDisk`

#### Options

The `formidable()` and methods accept these options:

- **hooks**: Object - Customize the parsing process with hook functions (see [Hooks](#hooks) below)
- **randomName**: Boolean - Generate random filename using cuid2 (default: `true`)
- **filenamify**: Object - Options for the [filenamify](https://github.com/sindresorhus/filenamify)
- **rename**: Function - Customize filename, signature:
  `(filepath: string, contents: unknown, options: FormidableFileOptions) => string`
- **signal**: AbortSignal - Abort controller signal for file operations
- **metadata**: Object - Additional metadata to associate with files
- **types**: String|Array - A list of conten types, does not support globs yet; eg. `'image/png'` or
  `['image/png', 'text/html']`
- **maxBodySize**: Number - Max request body size; throws if exceeded
- **maxFiles**: Number - Maximum number of files allowed
- **maxFields**: Number - Maximum number of fields allowed
- **maxFileSize**: Number - Maximum bytes for each file

Yet to be implemented, but could be done through [Hooks](#hooks), or we will share a preset of
hooks:

- **maxFieldSize**: Number - Maximum bytes for each field's value
- **maxHeaderSize**: Number - Maximum bytes for each header's value
- **maxHeaderKeySize**: Number - Maximum bytes for each header key
- **maxFieldKeySize**: Number - Maximum bytes for each field key

### `form.parse(req, options)`

Parses the incoming HTTP request and returns a promise that resolves with `files` and `fields` Sets.

**Note**: This method loads the entire content into memory. For streaming, use `form.formData()`.

### `form.formData(req, options)`

Parses the request and returns a Web API `FormData` object. This is the preferred method as it
supports streaming.

### Hooks

The hooks system allows you to customize or intercept various stages of the parsing process. You can
return `true` in the `onFileFirstChunk,` `onField`, `onFile`, and `onHeader` hooks which would mean
it will be skipped and won't be streamed down (added to) the FormData you are later iterating on.

> ![TIP] Use the `onFileFirstChunk` hook to detect the real type of a file stream. Return the `SKIP`
> Symbol found as named export and `formidable.SKIP` to skip the stream of it entirely and thus it
> will not even end up in the `FormData` that you are stream reading later. You can use packages
> like [`magic-bytes.js`](https://github.com/LarsKoelpin/magic-bytes) or
> [`file-type`](https://github.com/sindresorhus/file-type)

```js
import { formidable, SKIP } from 'formidable-mini';

const isNotPNG = (chunk) => !bufToHex(chunk.slice(0, 10)).startsWith('89504e47');
const bufToHex = (chunk) =>
  Array.from(chunk)
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');

const form = formidable({
  randomName: false, // uses the original file name
  types: ['image/png', 'image/jpg'], // does not support globs; but could be string too

  // Limit options

  // maxBodySize: 2_000,
  maxFiles: 2,
  // maxFields: 3,
  // maxFileSize: 500,

  // through hooks you ucan implement the following limits:
  // - maxFileKeySize
  // - maxFieldKeySize, maxFieldValueSize (or just maxFieldSize)
  // - maxHeaderKeySize, maxHeaderValueSize (or just maxHeaderSize)

  hooks: {
    onFileFirstChunk(chunk) {
      // Call on the very first chunk when file. Use it to detect if the file,
      // is the correct real type you looking for. Do not rely on extensions.
      // Return SKIP or formidable.SKIP to skip adding to formData

      console.log('magic bytes:', chunk);

      // skip if NOT starts with PNG-like (all magic bytes of PNG is "89 50 4e 47")
      if (isNotPNG(chunk)) {
        return formidable.SKIP;
      }
    },

    // NOTE: Use `maxFileSize` directly, or make a custom checker
    // onFileProgress(partialFileBuf, options) {
    //   console.log('partial file buf:', partialFileBuf.byteLength);

    //   if (partialFileBuf.byteLength > 1000) {
    //     return formidable.SKIP;
    //   }
    // },

    onPartBegin(chunk, start, end) {
      // Called when a new multipart section begins
    },

    onFile(key, file) {
      // Called when a file is encountered
      console.log({ size: file.size, file: { key, value: file } });
    },

    onField(key, value) {
      // Called for each form field
      console.log({ field: { key, value } });
    },

    onHeader(key, value) {
      // Called for header
      console.log({ header: { key, value } });
    },

    onHeaderField(chunk) {
      // Called with raw header field chunks
      console.log('onHeaderField:', chunk);
    },
  },
});
```

### FormidableFile

When a file is parsed, it's represented as a `FormidableFile` class that extends the Web API's
`File` interface. It has these additional properties:

- **path**: String - Full filepath
- **basename**: String - Filename with extension
- **dirname**: String - Directory path
- **extname**: String - File extension
- **stem**: String - Filename without extension (same as `name`)
- **mime**: String - MIME type
- **metadata**: Object - Custom metadata
- **contents**: Any - Raw file contents in bytes
- **options**: Object - The options used

Example of creating a `FormidableFile`:

```js
const file = new FormidableFile('photo.jpg', buffer, {
  type: 'image/jpeg',
  randomName: true,
  metadata: { uploadedBy: 'user123' },
});

console.log(file.name); // Random cuid2 name
console.log(file.type); // 'image/jpeg'
console.log(file.stem); // Same as file.name
console.log(file.path); // Full sanitized path
console.log(file.metadata); // { uploadedBy: 'user123' }
```

## Security

Formidable Mini includes several security features:

- Good default limits, super customizable if needed
- Random filenames using [`cuid2`](https://npmjs.com/package/@paralleldrive/cuid2) by default
- Filename sanitization via [`filenamify`](https://npmjs.com/package/filenamify)
- File validation through hooks to check
  [file magic bytes](https://github.com/LarsKoelpin/magic-bytes)
- No automatic disk writes, everything is streaming
- Content-Type validation, use `options.types`, or `onHeader` / `onFile`, and return
  `formidable.SKIP` to skip
- Streaming support to prevent memory issues
- Abortable operations, through `AbortController` and `AbortSignal`

## License

This project is licensed under the MPL-2.0 License.
