Kotoba.Attachments (Kotoba v0.8.0)

Copy Markdown View Source

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 (a JPEG's turned as its EXIF orientation says, as browsers draw it). 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.

The checks and the store are Storehouse's (Storehouse.Check, Storehouse.store/2): these functions give them as attachments.

Summary

Functions

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

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

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

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

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.

Builds an attachment node for a stored file.

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

Types

description()

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

What describe/2 finds.

Functions

clean_name(name)

@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(extension)

@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(path, client_type)

@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(content_type)

@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?(content_type)

@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(description, attrs)

Builds an attachment node for a stored file.

store(path, opts \\ [])

@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",

  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 store (a Storehouse.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 &Storehouse.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.