multipart/form-data Content Parsing Plugin for Nova Framework

View Source

nova_multipart_plugin is a middleware plugin for the Nova Web Framework 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:

{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.

[
    {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:

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.