# `Kotoba.Attachments`
[🔗](https://github.com/hattoriai/kotoba/blob/v0.5.0/lib/kotoba/attachments.ex#L1)

Server-side checks of an uploaded file, before it becomes an attachment
node.

The browser's content type is a claim. `describe/2` keeps only a type
that the bytes of the file prove, from a short allow-list of passive
types:

  * PNG, JPEG, GIF and WebP images, from their signatures, with their
    width and height when the header has them. The browser's claim does
    not matter: a PNG sent as `text/html` is a PNG.
  * PDF, from `%PDF-`.
  * MP4 video (`video/mp4`), from its `ftyp` box with an MP4 brand
    (`isom`, `mp41`, `mp42`, `avc1`, `M4V `, …), and WebM video
    (`video/webm`), from its EBML header with the `webm` document type.
    A QuickTime (`.mov`), Matroska (`.mkv`) or audio-only (`M4A `) file
    is not in the list.
  * ZIP files (`PK\x03\x04`): the Office Open XML and OpenDocument
    types when the browser claims one of them, else `application/zip`.
  * The older Office files (the OLE2 signature): `application/msword`,
    `application/vnd.ms-excel` or `application/vnd.ms-powerpoint` when the
    browser claims one of them.
  * `text/plain`, only when the browser claims it and the start of the
    file is valid UTF-8 with no NUL byte.

Every other file becomes `application/octet-stream`: `text/html`,
`application/xhtml+xml`, `image/svg+xml`, JavaScript, and any type that
is not in the list. Such a file renders as a download link, and its
storage key ends in `.bin`.

`extension/1` gives the file extension of each checked type, and
`inline?/1` tells which types a browser can show in the page (the
images, PDF and the videos). Serve every other file with
`Content-Disposition: attachment`.

`store/2` makes an attachment of a file on the server, as an upload
does: for seeds, imports, or files an app makes itself.

# `description`

```elixir
@type description() :: %{
  content_type: String.t(),
  bytes: non_neg_integer(),
  width: pos_integer() | nil,
  height: pos_integer() | nil
}
```

What `describe/2` finds.

# `clean_name`

```elixir
@spec clean_name(term()) :: String.t()
```

Returns a file name for display: control and format characters (bidi
controls included) removed, at most
200 characters, and `"file"` when nothing is left.

## Examples

    iex> Kotoba.Attachments.clean_name("cat\u0000.png")
    "cat.png"

    iex> Kotoba.Attachments.clean_name("a\u202Egnp.exe")
    "agnp.exe"

# `content_type`

```elixir
@spec content_type(String.t()) :: String.t()
```

Returns the checked content type for a file extension (as `extension/1`
gives it), or `"application/octet-stream"`.

## Examples

    iex> Kotoba.Attachments.content_type(".pdf")
    "application/pdf"

    iex> Kotoba.Attachments.content_type(".svg")
    "application/octet-stream"

# `describe`

```elixir
@spec describe(Path.t(), String.t() | nil) :: {:ok, description()} | {:error, term()}
```

Reads the file at `path` and returns its checked content type, its size
and, for an image, its width and height.

# `extension`

```elixir
@spec extension(String.t()) :: String.t()
```

Returns the file extension of a checked content type. A type that is
not in the allow-list gives `".bin"`.

## Examples

    iex> Kotoba.Attachments.extension("image/jpeg")
    ".jpg"

    iex> Kotoba.Attachments.extension("text/html")
    ".bin"

# `inline?`

```elixir
@spec inline?(String.t()) :: boolean()
```

Returns `true` for a checked type that a browser can show in the page:
the images, PDF and the videos. Serve every other type with
`Content-Disposition: attachment`.

## Examples

    iex> Kotoba.Attachments.inline?("video/webm")
    true

    iex> Kotoba.Attachments.inline?("application/pdf")
    true

    iex> Kotoba.Attachments.inline?("text/plain")
    false

# `node`

```elixir
@spec node(description(), keyword()) :: Kotoba.Nodes.Attachment.t()
```

Builds an attachment node for a stored file.

# `store`

```elixir
@spec store(Path.t(), keyword()) ::
  {:ok, Kotoba.Nodes.Attachment.t()} | {:error, term()}
```

Stores the file at `path` and returns its attachment node, as an upload
of the file would make it.

The file is checked with `describe/2`, stored with the storage adapter
under a new key, and its node checked. When the adapter stores the file
but its result does not make a valid attachment (a URL that is not a
safe link, say), the stored file is deleted. The file at `path` is left
where it is.

Put the node in a document with `Kotoba.Content.from_markdown/2` and
its `:attachments` option:

    {:ok, shot} = Kotoba.Attachments.store("priv/seeds/timeline.png")

    Kotoba.Content.from_markdown(
      "The timeline:

![Timeline](attachment:timeline)",
      attachments: %{"timeline" => shot}
    )

## Options

  * `:name` - the file name. The default is the base name of `path`.
  * `:content_type` - the claimed content type, as a browser sends it.
    The default is the type of the file's extension. Only the bytes of
    the file make an image an image (see `describe/2`).
  * `:storage` - the storage adapter. The default is
    `Kotoba.Storage.adapter/0`.
  * `:key` - a function of the file name and the checked content type
    that returns a storage key. The default is `&Kotoba.Storage.key/2`.
  * `:preview` - a function of the stored attachment and `path` that
    returns the URL of a preview image, or `nil`: as the `:preview`
    option of `Kotoba.Live.consume_uploads/4`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
