Skip to main content

README.md

# multipart/form-data Content Parsing Plugin for Nova Framework

`nova_multipart_plugin` is a middleware plugin for the [Nova Web Framework](https://github.com/novaframework/nova) that handles `multipart/form-data` requests.

## Features

When processing `multipart/form-data` content, this plugin automatically uploads files to a temporary directory and augments the Nova `Req` (Request) object with the following maps:

* `files` — A list of uploaded temporary files.
* `fields` — A list of parsed form fields.

---

## Installation

### 1. Add Dependency

Add `nova_multipart_plugin` to your `rebar.config` dependencies:

```erlang
{deps, [
    %% ...
    {nova, "0.14.1"},
    nova_multipart_plugin,
    %% ...
]}.

```

### 2. Configure sys.config

Register the plugin under the `plugins` section for the `pre_request` stage in your `sys.config`. You must specify the `tmp_dir` option (where temporary files will be stored) as a **binary string**.

```erlang
[
    {kernel, [{logger_level, error}]},
    {nova, [
        {cowboy_configuration, #{port => 8080}},
        {bootstrap_application, my_app},
        {json_lib, thoas},
        %% ...
        {plugins, [
            {pre_request, nova_multipart_plugin, #{tmp_dir => <<"/tmp/nova/">>}},
            {pre_request, nova_request_plugin, #{decode_json_body => true, parse_qs => true}}
            %% ...
        ]}
    ]}
].

```

---

## Usage

You can extract the `files` and `fields` maps directly from the Nova `Req` object within your controllers:

```erlang
upload_file(#{files := Files, fields := Fields} = Req) ->
    Body = lists:foldl(
        fun(File, Acc) ->
            {FieldName, {FileName, ContentType, TmpPath}} = File,
            SubDir = binary:encode_hex(crypto:strong_rand_bytes(16)),
            Path = filename:join(SubDir, FileName),
            NewPath = filename:join(<<"./uploads/">>, Path),
            filelib:ensure_dir(NewPath),
            move_file(TmpPath, NewPath),
            Acc#{
                FieldName => #{
                    <<"path">> => Path,
                    <<"file_name">> => FileName,
                    <<"content_type">> => ContentType
                }
            }
        end,
        #{},
        Files
    ),
    {json, 200, #{}, Body}.

move_file(Source, Dest) ->
    case file:rename(Source, Dest) of
        ok -> ok;
        {error, exdev} ->
            case file:copy(Source, Dest) of
                {ok, _Bytes} -> file:delete(Source);
                {error, Reason} -> {error, Reason}
            end;
        {error, Reason} -> {error, Reason}
    end.

```

---

## Design Philosophy

The output generated by this plugin mirrors the exact structure of the incoming `multipart/form-data` payload without introducing opinions.

Decisions regarding persistent storage architecture, file access control, handling duplicate filenames, or parsing field names into hierarchical structures are highly domain-specific. Therefore, this plugin leaves those responsibilities entirely to the application developer.