Packages

phoenix_kit

2.37.2
2.64.0 2.63.0 2.62.2 2.62.1 2.62.0 2.61.0 2.60.3 2.60.2 2.60.1 2.60.0 2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage schemas file.ex
Raw

lib/modules/storage/schemas/file.ex

defmodule PhoenixKit.Modules.Storage.File do
  @moduledoc """
  Schema for original file uploads.

  Represents the original uploaded file with metadata. Each file can have multiple
  instances (variants) like thumbnails, resizes, or video quality variants.

  ## File Types

  - **image** - JPEG, PNG, WebP, GIF, HEIC (scalable)
  - **video** - MP4, WebM, MOV, AVI, MKV (scalable)
  - **document** - PDF, DOC, DOCX, TXT, MD (non-scalable)
  - **archive** - ZIP, RAR, 7Z, TAR, GZ (non-scalable)

  ## Status Flow

  - `processing` - File is being processed (variants being generated)
  - `active` - File is ready and available
  - `failed` - Processing failed
  - `trashed` - File is in trash, pending restoration or permanent deletion

  ## Fields

  - `original_file_name` - User's original filename
  - `file_name` - System filename (uuid_v7-original.ext)
  - `mime_type` - MIME type (image/jpeg, video/mp4, etc.)
  - `file_type` - High-level type (image, video, document, archive)
  - `ext` - File extension (jpg, mp4, pdf, etc.)
  - `file_checksum` - SHA256 hash of file content for integrity verification
  - `user_file_checksum` - SHA256 hash of (user_uuid + file_checksum) for per-user deduplication
  - `size` - File size in bytes
  - `width` - Image/video width in pixels (nullable)
  - `height` - Image/video height in pixels (nullable)
  - `duration` - Video duration in seconds (nullable)
  - `status` - Processing status
  - `metadata` - JSONB with EXIF, codec info, etc.
  - `taken_at`, `taken_on`, `taken_at_offset`, `taken_at_source` - When the
    photo or video was taken (V200); see `PhoenixKit.Modules.Storage.CaptureDate`
  - `data` - JSONB with the title, alt text and description per language
    (V199). Read and written through `PhoenixKit.Modules.Storage.FileDetails`
  - `user_uuid` - Owner of the file

  ## Examples

      # Image file
      %File{
        id: "018e3c4a-9f6b-7890-abcd-ef1234567890",
        original_file_name: "profile.jpg",
        file_name: "018e3c4a-9f6b-7890-original.jpg",
        mime_type: "image/jpeg",
        file_type: "image",
        ext: "jpg",
        file_checksum: "abc123def456...",
        user_file_checksum: "xyz789ghi012...",
        size: 524_288,  # 512 KB
        width: 2000,
        height: 2000,
        status: "active",
        metadata: %{"camera" => "Canon EOS"},
        user_uuid: "018e3c4a-1234-5678-abcd-ef1234567890"
      }

      # Video file
      %File{
        original_file_name: "intro.mp4",
        file_name: "018e3c4a-9f6b-7890-original.mp4",
        mime_type: "video/mp4",
        file_type: "video",
        ext: "mp4",
        file_checksum: "def456ghi789...",
        user_file_checksum: "mno345pqr678...",
        size: 10_485_760,  # 10 MB
        width: 1920,
        height: 1080,
        duration: 30,  # 30 seconds
        status: "processing",
        metadata: %{"codec" => "h264"}
      }

      # Document file
      %File{
        original_file_name: "report.pdf",
        file_name: "018e3c4a-9f6b-7890-original.pdf",
        mime_type: "application/pdf",
        file_type: "document",
        ext: "pdf",
        file_checksum: "ghi789jkl012...",
        user_file_checksum: "stu901vwx234...",
        size: 2_097_152,  # 2 MB
        status: "active"
      }
  """
  use Ecto.Schema
  use PhoenixKit.SchemaPrefix
  import Ecto.Changeset

  alias PhoenixKit.Modules.Storage.CaptureDate

  @primary_key {:uuid, UUIDv7, autogenerate: true}
  @foreign_key_type UUIDv7

  @type t :: %__MODULE__{
          uuid: UUIDv7.t() | nil,
          original_file_name: String.t(),
          file_name: String.t(),
          file_path: String.t() | nil,
          mime_type: String.t(),
          file_type: String.t(),
          ext: String.t(),
          file_checksum: String.t(),
          user_file_checksum: String.t(),
          size: integer(),
          width: integer() | nil,
          height: integer() | nil,
          duration: integer() | nil,
          status: String.t(),
          trashed_at: DateTime.t() | nil,
          metadata: map() | nil,
          data: map(),
          taken_at: DateTime.t() | nil,
          taken_on: Date.t() | nil,
          taken_at_offset: integer() | nil,
          taken_at_source: String.t() | nil,
          system_managed: boolean(),
          user_uuid: UUIDv7.t() | nil,
          folder_uuid: UUIDv7.t() | nil,
          parent_file_uuid: UUIDv7.t() | nil,
          user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t(),
          parent_file: t() | Ecto.Association.NotLoaded.t() | nil,
          instances:
            [PhoenixKit.Modules.Storage.FileInstance.t()] | Ecto.Association.NotLoaded.t(),
          inserted_at: DateTime.t() | nil,
          updated_at: DateTime.t() | nil
        }

  schema "phoenix_kit_files" do
    field :original_file_name, :string
    field :file_name, :string
    field :file_path, :string
    field :mime_type, :string
    field :file_type, :string
    field :ext, :string
    field :file_checksum, :string
    field :user_file_checksum, :string
    field :size, :integer
    field :width, :integer
    field :height, :integer
    field :duration, :integer
    field :status, :string, default: "processing"
    field :trashed_at, :utc_datetime
    field :metadata, :map

    # When the photo or video was taken (V200). `taken_on` is the LOCAL date,
    # which is what a library groups by; `taken_at` is UTC. Written only
    # through `CaptureDate.replace?/2`'s guard — see
    # `PhoenixKit.Modules.Storage.CaptureDate` for the sources and why a
    # date is never downgraded.
    field :taken_at, :utc_datetime
    field :taken_on, :date
    field :taken_at_offset, :integer
    field :taken_at_source, :string

    # The title, alt text and description per language (V199) —
    # `%{"en-US" => %{"title" => …, "alt" => …}, "et" => %{…}}`. Owned by
    # `PhoenixKit.Modules.Storage.FileDetails`; read it through that.
    field :data, :map, default: %{}

    # `true` for internally-generated media (e.g. Tessera DZI tile pyramids
    # and their per-tile chunks). System-managed rows are excluded from the
    # MediaBrowser's user-facing listings and skipped by the variant
    # generator.
    field :system_managed, :boolean, default: false

    # Image editing (V195; `PhoenixKit.Modules.Storage.ImageEditing`). An
    # edited image keeps this uuid and its original instance holds the edited
    # bytes; `edits` is always applied to the unedited original, which lives
    # on as the system-managed child named by `original_file_uuid`.
    # `edit_state` "pending" or "failed" makes the file serve a placeholder.
    field :edits, :map
    field :edit_revision, :integer, default: 0
    field :edit_state, :string

    belongs_to :original_file, __MODULE__,
      foreign_key: :original_file_uuid,
      references: :uuid,
      type: UUIDv7

    # On a "save as copy" result: the file it was edited from.
    belongs_to :edited_from, __MODULE__,
      foreign_key: :edited_from_uuid,
      references: :uuid,
      type: UUIDv7

    belongs_to :user, PhoenixKit.Users.Auth.User,
      foreign_key: :user_uuid,
      references: :uuid,
      type: UUIDv7

    belongs_to :folder, PhoenixKit.Modules.Storage.Folder,
      foreign_key: :folder_uuid,
      references: :uuid,
      type: UUIDv7

    # For system-managed children (e.g. tile chunks), points at the source
    # File the chunk was derived from. Cascade-deletes via the DB FK
    # `ON DELETE :delete_all`, so removing a source image clears its tiles.
    belongs_to :parent_file, __MODULE__,
      foreign_key: :parent_file_uuid,
      references: :uuid,
      type: UUIDv7

    has_many :instances, PhoenixKit.Modules.Storage.FileInstance, foreign_key: :file_uuid
    has_many :folder_links, PhoenixKit.Modules.Storage.FolderLink, foreign_key: :file_uuid

    timestamps(type: :utc_datetime)
  end

  @doc """
  Changeset for creating or updating a file.

  ## Required Fields

  - `original_file_name`
  - `file_name`
  - `mime_type`
  - `file_type` (must be: "image", "video", "document", "archive")
  - `ext`
  - `file_checksum`
  - `user_file_checksum`
  - `size`
  - `user_uuid`

  ## Validation Rules

  - File type must be valid
  - Status must be valid (processing, active, failed)
  - Size must be positive
  - Width/height must be positive (if provided)
  - Duration must be positive (if provided)
  """
  def changeset(file, attrs) do
    file
    |> cast(attrs, [
      :original_file_name,
      :file_name,
      :file_path,
      :mime_type,
      :file_type,
      :ext,
      :file_checksum,
      :user_file_checksum,
      :size,
      :width,
      :height,
      :duration,
      :status,
      :trashed_at,
      :metadata,
      :taken_at,
      :taken_on,
      :taken_at_offset,
      :taken_at_source,
      :system_managed,
      :user_uuid,
      :folder_uuid,
      :parent_file_uuid
    ])
    |> validate_required([
      :original_file_name,
      :file_name,
      :mime_type,
      :file_type,
      :ext,
      :file_checksum,
      :user_file_checksum,
      :size
    ])
    |> validate_inclusion(:file_type, [
      "image",
      "video",
      "audio",
      "document",
      "archive",
      "other",
      "tile"
    ])
    |> validate_inclusion(:status, ["processing", "active", "failed", "trashed"])
    |> validate_number(:size, greater_than: 0)
    |> validate_number(:width, greater_than: 0)
    |> validate_number(:height, greater_than: 0)
    |> validate_number(:duration, greater_than: 0)
    |> validate_inclusion(:taken_at_source, CaptureDate.sources())
    |> validate_system_managed_invariants()
    |> foreign_key_constraint(:user_uuid, name: :fk_files_user_uuid)
    |> foreign_key_constraint(:folder_uuid)
    |> foreign_key_constraint(:parent_file_uuid)
    # V113's `phoenix_kit_files_system_dedup_index` keeps concurrent
    # lazy-generators for the same Tessera tile from inserting duplicate
    # rows. Naming the constraint here lets `Storage.store_system_file/3`
    # detect the race via the changeset error and re-fetch the winning
    # row instead of bubbling a Postgrex unique-violation.
    |> unique_constraint([:parent_file_uuid, :file_name],
      name: :phoenix_kit_files_system_dedup_index
    )
  end

  @doc """
  Changeset for a file's translatable details: nothing but `metadata` and
  `data`, as `PhoenixKit.Modules.Storage.FileDetails.file_attrs/4` builds
  them. Use `PhoenixKit.Modules.Storage.update_file_details/3`, which holds
  the row while it merges.
  """
  def details_changeset(file, attrs) do
    cast(file, attrs, [:metadata, :data])
  end

  # User-uploaded files require `user_uuid` (existing invariant). System-
  # managed files (tile chunks) don't have a user owner — they live under
  # a parent File and inherit lifecycle from it.
  defp validate_system_managed_invariants(changeset) do
    case get_field(changeset, :system_managed) do
      true ->
        validate_required(changeset, [:parent_file_uuid])

      _ ->
        validate_required(changeset, [:user_uuid])
    end
  end

  @doc """
  Returns whether this file type supports variant generation (scalable).
  """
  def scalable?(%__MODULE__{file_type: file_type}) when file_type in ["image", "video"],
    do: true

  def scalable?(_), do: false

  @doc """
  Returns whether this file is an image.
  """
  def image?(%__MODULE__{file_type: "image"}), do: true
  def image?(_), do: false

  @doc """
  Returns whether this file is a video.
  """
  def video?(%__MODULE__{file_type: "video"}), do: true
  def video?(_), do: false

  @doc """
  Returns whether this file is a document.
  """
  def document?(%__MODULE__{file_type: "document"}), do: true
  def document?(_), do: false

  @doc """
  Returns whether this file is an archive.
  """
  def archive?(%__MODULE__{file_type: "archive"}), do: true
  def archive?(_), do: false
end