Coelho.Attachment (coelho v0.3.1)

Copy Markdown View Source

Metadata of one uploaded file, addressed by key.

Coelho records what the editor and the renderer need to show an attachment — key, filename, content type, size — and nothing about where the bytes are. Storage stays the application's: this row is what a resolver looks up to build a URL. See Coelho.Attachments.

Create the table with mix coelho.gen.migration.

{:ok, attachment} =
  %Coelho.Attachment{}
  |> Coelho.Attachment.changeset(%{
    key: Coelho.Attachment.generate_key(),
    filename: "plan.pdf",
    content_type: "application/pdf",
    byte_size: 91_233
  })
  |> Repo.insert()

Coelho.Attachment.to_node(attachment)
#=> %{"type" => "attachment", "attrs" => %{"key" => "...", ...}}

Nothing here is tied to an owner. A document references attachments by key, and Coelho.Attachments.keys/2 answers which keys a document still uses — which is what a cleanup job needs, and is why the row carries no polymorphic association. generate_key/1 takes a prefix when an application needs to list its own objects by tenant; read what that is and is not before reaching for it.

Summary

Functions

Casts and validates attachment metadata.

The document node referencing this attachment.

Types

t()

@type t() :: %Coelho.Attachment{
  __meta__: term(),
  byte_size: term(),
  checksum: term(),
  content_type: term(),
  filename: term(),
  id: term(),
  inserted_at: term(),
  key: term(),
  metadata: term(),
  updated_at: term()
}

Functions

changeset(attachment, attrs)

@spec changeset(t(), map()) :: Ecto.Changeset.t()

Casts and validates attachment metadata.

generate_key(opts \\ [])

@spec generate_key(keyword()) :: String.t()

A fresh key.

Keys are opaque and URL safe, and deliberately not derived from the filename: the key is what ends up inside stored documents, so it must survive a rename and must not leak what it points at.

Prefixing, and what a prefix is not

A key is global and opaque, which answers "what does this document point at" and never "whose is it". An application that backs up or purges per organisation has no way to list one organisation's objects without walking every document it owns, and Coelho.Attachments.keys/2 is that walk. A prefix gives object storage something to list on instead:

Coelho.Attachment.generate_key(prefix: "org_" <> org.slug)
#=> "org_acme-QVmR0T7oKQyJ0y7Zzz5Wig"

A prefix is an inventory aid and never an authorization boundary. The key arrives from the URL, which is to say from whoever sends the request, so a plug that read the organisation out of it would be asking the attacker which organisation they are in. What decides that is the connection — see the :authorize option of Coelho.Plug.Attachments.

The prefix has to be URL safe and free of separators, because the key is one path segment and Coelho.Storage.Disk refuses anything else.

Coelho.Storage.Disk is the wrong place for one. It shards on the first two characters of the key, which for org_acme-…, org_beta-… and every other organisation is or — so every prefixed attachment in the application lands in a single directory, which is precisely what the sharding is there to prevent. A prefix earns its keep against object storage, where listing by prefix is the operation it exists for; against Disk, either leave it off, or start it with something that varies.

to_node(attachment)

@spec to_node(t()) :: map()

The document node referencing this attachment.

Insert the result into the editor once an upload has been consumed.