Packages

phoenix_kit

2.62.2
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 repair_log.ex
Raw

lib/modules/storage/repair_log.ex

defmodule PhoenixKit.Modules.Storage.RepairLog do
  @moduledoc """
  The trail of storage that went wrong, read back for people: every copy found
  missing or damaged (`storage.copy.damaged`) and every repair of a file
  (`storage.file.repaired`), with the file, the library and the bucket each
  concerns, so a page can link to all three.

  The entries are written by `PhoenixKit.Modules.Storage.Audit.log_damage/3` and
  `log_repair/4` into the Activity log; this module is the reading side. It
  filters (`:file_uuid`, `:bucket_uuid`, `:library_uuid`, `:kind`), pages, and
  resolves what each entry points at: a file or bucket that is gone is a name
  without a link, and an entry written before library names were recorded finds
  its library through the file.

  Options of `list/1`: `:file_uuid`, `:bucket_uuid`, `:library_uuid`, `:kind`
  (`:damaged` or `:repaired`), `:page` (1) and `:per_page` (25).
  Returns `%{rows, page, total_pages, total}`; a row is

      %{uuid, at, kind, actor, file: %{uuid, name, exists?}, library: %{uuid, name, site?},
        bucket: %{uuid, name} | nil, rendition, problem, found_by,
        actions: [%{rendition, kind, bucket, from}], problems_left}
  """

  import Ecto.Query

  alias PhoenixKit.Activity
  alias PhoenixKit.Activity.Entry
  alias PhoenixKit.Modules.Storage.Bucket
  alias PhoenixKit.Modules.Storage.File, as: StorageFile
  alias PhoenixKit.Modules.Storage.Library

  @damaged "storage.copy.damaged"
  @repaired "storage.file.repaired"

  @spec list(keyword()) :: map()
  def list(opts \\ []) do
    result =
      Activity.list(
        query: query(opts),
        page: Keyword.get(opts, :page, 1),
        per_page: Keyword.get(opts, :per_page, 25),
        preload: [:actor]
      )

    %{
      rows: rows(result.entries),
      page: result.page,
      total_pages: result.total_pages,
      total: result.total
    }
  end

  @doc "The entry actions this log reads."
  @spec actions() :: [String.t()]
  def actions, do: [@damaged, @repaired]

  defp query(opts) do
    from(e in Entry, where: e.action in ^actions())
    |> kind(opts[:kind])
    |> file(opts[:file_uuid])
    |> bucket(opts[:bucket_uuid])
    |> library(opts[:library_uuid])
  end

  defp kind(query, :damaged), do: where(query, [e], e.action == ^@damaged)
  defp kind(query, :repaired), do: where(query, [e], e.action == ^@repaired)
  defp kind(query, _), do: query

  defp file(query, nil), do: query

  defp file(query, uuid) do
    uuid = to_string(uuid)

    where(
      query,
      [e],
      (e.resource_type == "file" and e.resource_uuid == ^uuid) or
        fragment("?->>'file_uuid' = ?", e.metadata, ^uuid)
    )
  end

  # A damaged copy is filed against its bucket; a repair names the buckets it
  # touched in the entry.
  defp bucket(query, nil), do: query

  defp bucket(query, uuid) do
    uuid = to_string(uuid)
    touched = %{"bucket_uuids" => [uuid]}

    where(
      query,
      [e],
      (e.resource_type == "bucket" and e.resource_uuid == ^uuid) or
        fragment("? @> ?", e.metadata, type(^touched, :map))
    )
  end

  defp library(query, nil), do: query

  defp library(query, uuid),
    do: where(query, [e], fragment("?->>'library_uuid' = ?", e.metadata, ^to_string(uuid)))

  # ── rows ──────────────────────────────────────────────────────

  defp rows(entries) do
    files = files_by_uuid(entries)
    libraries = libraries_by_uuid(entries, files)
    buckets = buckets_by_uuid(entries)

    Enum.map(entries, &row(&1, files, libraries, buckets))
  end

  defp row(%Entry{} = entry, files, libraries, buckets) do
    meta = entry.metadata || %{}
    file_uuid = if entry.resource_type == "file", do: entry.resource_uuid, else: meta["file_uuid"]
    file = Map.get(files, file_uuid)

    library_uuid =
      meta["library_uuid"] || (file && file.library_uuid && to_string(file.library_uuid))

    %{
      uuid: entry.uuid,
      at: entry.inserted_at,
      kind: if(entry.action == @damaged, do: :damaged, else: :repaired),
      actor: entry.actor,
      file: %{
        uuid: file_uuid,
        name: (file && file.name) || meta["file_name"],
        exists?: file != nil
      },
      library: library_ref(library_uuid, libraries),
      bucket: if(entry.resource_type == "bucket", do: bucket(entry, buckets, meta)),
      rendition: meta["rendition"],
      problem: meta["problem"],
      found_by: meta["found_by"],
      actions: actions(meta["actions"], buckets),
      problems_left: meta["problems_left"]
    }
  end

  defp library_ref(nil, _libraries), do: nil

  defp library_ref(uuid, libraries) do
    case Map.get(libraries, uuid) do
      nil -> %{uuid: uuid, name: nil, site?: false}
      library -> library
    end
  end

  defp bucket(entry, buckets, meta) do
    current = Map.get(buckets, entry.resource_uuid)

    %{
      uuid: entry.resource_uuid,
      name: (current && current.name) || meta["bucket"],
      exists?: current != nil
    }
  end

  defp actions(nil, _buckets), do: []

  defp actions(list, buckets) when is_list(list) do
    Enum.map(list, fn a ->
      %{
        rendition: a["rendition"],
        kind: a["kind"],
        bucket: a["bucket"],
        bucket_uuid: a["bucket_uuid"],
        bucket_exists?: Map.has_key?(buckets, a["bucket_uuid"]),
        from: a["from"],
        from_uuid: a["from_uuid"]
      }
    end)
  end

  defp actions(_other, _buckets), do: []

  # ── lookups, one query each for the whole page ─────────────────

  defp files_by_uuid(entries) do
    uuids =
      for e <- entries,
          uuid =
            if(e.resource_type == "file",
              do: e.resource_uuid,
              else: (e.metadata || %{})["file_uuid"]
            ),
          uniq: true,
          do: uuid

    if uuids == [] do
      %{}
    else
      from(f in StorageFile,
        where: f.uuid in ^uuids,
        select:
          {f.uuid,
           %{library_uuid: f.library_uuid, name: coalesce(f.original_file_name, f.file_name)}}
      )
      |> repo().all()
      |> Map.new(fn {uuid, file} -> {to_string(uuid), file} end)
    end
  end

  defp libraries_by_uuid(entries, files) do
    uuids =
      (for(e <- entries, uuid = (e.metadata || %{})["library_uuid"], do: uuid) ++
         for({_uuid, %{library_uuid: uuid}} <- files, uuid != nil, do: to_string(uuid)))
      |> Enum.uniq()

    if uuids == [] do
      %{}
    else
      from(l in Library,
        where: l.uuid in ^uuids,
        select: {l.uuid, %{name: l.name, kind: l.kind}}
      )
      |> repo().all()
      |> Map.new(fn {uuid, l} ->
        uuid = to_string(uuid)
        {uuid, %{uuid: uuid, name: l.name, site?: l.kind == "system"}}
      end)
    end
  end

  defp buckets_by_uuid(entries) do
    uuids =
      Enum.flat_map(entries, fn e ->
        meta = e.metadata || %{}

        [if(e.resource_type == "bucket", do: e.resource_uuid)] ++
          Enum.flat_map(meta["actions"] || [], &[&1["bucket_uuid"], &1["from_uuid"]])
      end)
      |> Enum.reject(&is_nil/1)
      |> Enum.uniq()

    if uuids == [] do
      %{}
    else
      from(b in Bucket,
        where: b.uuid in ^uuids and is_nil(b.owner_uuid),
        select: {b.uuid, %{name: b.name}}
      )
      |> repo().all()
      |> Map.new(fn {uuid, b} -> {to_string(uuid), b} end)
    end
  end

  defp repo, do: PhoenixKit.RepoHelper.repo()
end