brazier/upload

Types

A file type recognised by its signature (magic bytes).

pub type FileKind {
  Png
  Jpeg
  Gif
  Webp
  Pdf
}

Constructors

  • Png
  • Jpeg
  • Gif
  • Webp
  • Pdf

A parsed multipart form: text fields and validated files, in order.

pub type MultipartForm {
  MultipartForm(
    fields: List(#(String, String)),
    files: List(UploadedFile),
  )
}

Constructors

  • MultipartForm(
      fields: List(#(String, String)),
      files: List(UploadedFile),
    )

A file stored by save.

pub type StoredFile {
  StoredFile(
    path: String,
    stored_name: String,
    original_filename: String,
    content_type: String,
    size: Int,
  )
}

Constructors

  • StoredFile(
      path: String,
      stored_name: String,
      original_filename: String,
      content_type: String,
      size: Int,
    )

    Arguments

    path

    Full path of the stored file.

    stored_name

    The random server-side name (<32 hex>.<ext>); pass it to serve.

    original_filename

    The client’s display name (metadata only).

    content_type

    The sniffed MIME type.

    size

    Size in bytes.

Upload configuration.

pub type UploadConfig {
  UploadConfig(
    max_size: Int,
    allowed_types: List(String),
    max_request_bytes: Int,
  )
}

Constructors

  • UploadConfig(
      max_size: Int,
      allowed_types: List(String),
      max_request_bytes: Int,
    )

    Arguments

    max_size

    Maximum size of one file in bytes (default 10 MB).

    allowed_types

    Allowed MIME types. Each must be a supported kind (see kind_mime); an empty list allows nothing. Default: every supported kind.

    max_request_bytes

    Maximum size of the whole multipart request body in bytes, checked against Content-Length before the body is read (default 11 MB).

Upload errors.

pub type UploadError {
  FileTooLarge(max: Int, actual: Int)
  TypeNotAllowed(content_type: String, allowed: List(String))
  NoFile
  ParseError(reason: String)
  UnrecognizedContent(declared: String)
  ContentTypeMismatch(declared: String, detected: String)
  InvalidFilename(reason: String)
  NotMultipart(content_type: String)
  RequestTooLarge(max: Int, actual: Int)
  BodyReadError(reason: String)
  StorageError(reason: String)
  InvalidStoredName(name: String)
  InvalidConfig(reason: String)
}

Constructors

  • FileTooLarge(max: Int, actual: Int)

    A file exceeds max size.

  • TypeNotAllowed(content_type: String, allowed: List(String))

    The sniffed type is not in the allow-list.

  • NoFile

    No file was provided.

  • ParseError(reason: String)

    The multipart body is malformed.

  • UnrecognizedContent(declared: String)

    The file’s bytes match no supported signature.

  • ContentTypeMismatch(declared: String, detected: String)

    The declared Content-Type disagrees with the file’s bytes.

  • InvalidFilename(reason: String)

    The client’s filename cannot be used even as metadata.

  • NotMultipart(content_type: String)

    The request is not multipart/form-data.

  • RequestTooLarge(max: Int, actual: Int)

    The declared request body exceeds max_request_bytes.

  • BodyReadError(reason: String)

    The request body could not be read.

  • StorageError(reason: String)

    Storing or reading a file failed.

  • InvalidStoredName(name: String)

    A stored name is not one save produced.

  • InvalidConfig(reason: String)

    The upload configuration is invalid.

An uploaded file, as parsed from a multipart request.

pub type UploadedFile {
  UploadedFile(
    filename: String,
    content_type: String,
    data: BitArray,
    size: Int,
    field_name: String,
  )
}

Constructors

  • UploadedFile(
      filename: String,
      content_type: String,
      data: BitArray,
      size: Int,
      field_name: String,
    )

    Arguments

    filename

    The client’s filename, reduced to a display name (no directories, no control characters, at most 255 characters). Metadata only: never use it as a path.

    content_type

    The Content-Type the client declared for the part. Not trusted: validate compares it with the sniffed type.

    data

    File contents as bytes.

    size

    File size in bytes.

    field_name

    The form field name the file was sent under.

Values

pub fn default_config() -> UploadConfig

Default upload config: 10 MB per file, 11 MB per request, every supported kind allowed.

pub fn error_status(error: UploadError) -> Int

The HTTP status that best describes an upload error.

pub fn error_to_string(error: UploadError) -> String

Describe an upload error.

pub fn extension(filename: String) -> option.Option(String)

Get the file extension from a filename, lowercased in ASCII. File extensions in type tables are ASCII; a character outside ASCII is kept as it is ("x.\u{212A}B" gives "\u{212A}b", not "kb"), so such an extension never equals an ASCII one.

pub fn file(
  form: MultipartForm,
  field_name: String,
) -> Result(UploadedFile, UploadError)

The first file sent under field_name.

pub fn format_size(bytes: Int) -> String

Format file size for display.

pub fn kind_extension(kind: FileKind) -> String

The extension save gives a stored file of this kind.

pub fn kind_mime(kind: FileKind) -> String

The canonical MIME type of a kind.

pub fn parse_multipart(
  body: BitArray,
  content_type: String,
  config: UploadConfig,
) -> Result(MultipartForm, UploadError)

Parse an already-read multipart/form-data body, validating every file.

pub fn read_multipart(
  req: request.Request(server.Connection),
  config: UploadConfig,
) -> Result(MultipartForm, UploadError)

Read and parse a multipart/form-data request, validating every file. The Content-Type is checked before the body is touched, and the declared Content-Length against max_request_bytes before any byte is read.

pub fn save(
  file: UploadedFile,
  directory: String,
) -> Result(StoredFile, UploadError)

Store a file under a random name in directory. The file’s content must be a supported kind (it gives the extension). The file is created exclusively, so nothing is ever overwritten. Call validate (or use read_multipart) first to apply the allow-list and size limit.

pub fn serve(
  directory: String,
  stored_name: String,
  download_name: String,
) -> Result(response.Response(server.ResponseBody), UploadError)

Serve a stored file as a download. stored_name must be a name save produced (anything else, including traversal, is refused); the response type comes from its extension, never from the request. The file is always an attachment, with nosniff and a sandboxing CSP, so a browser never renders it inline. download_name is the name the browser offers (typically StoredFile.original_filename); it is reduced to a display name and encoded per RFC 6266, and one with no usable name is refused.

pub fn sniff(data: BitArray) -> Result(FileKind, Nil)

Identify a file by its leading bytes.

pub fn supported_kinds() -> List(FileKind)

Every supported kind, in signature-check order.

pub fn validate(
  file: UploadedFile,
  config: UploadConfig,
) -> Result(UploadedFile, UploadError)

Validate an uploaded file: size, recognised content, declared type agreeing with the content, and the sniffed type in the allow-list.

pub fn validate_config(
  config: UploadConfig,
) -> Result(Nil, UploadError)

Check a configuration: positive limits and an allow-list of supported MIME types only (a type that cannot be sniffed could never be accepted, so listing one is a configuration bug).

pub fn with_allowed_types(
  config: UploadConfig,
  types: List(String),
) -> UploadConfig

Set the allowed MIME types (each must be a supported kind).

pub fn with_max_request_bytes(
  config: UploadConfig,
  bytes: Int,
) -> UploadConfig

Set the maximum size of the whole multipart request body.

pub fn with_max_size(
  config: UploadConfig,
  bytes: Int,
) -> UploadConfig

Set the maximum size of one file.

✨ Search Document