151 lines
4.7 KiB
Elixir
151 lines
4.7 KiB
Elixir
defmodule BDS.Media.FileOps do
|
|
@moduledoc false
|
|
|
|
alias BDS.Persistence
|
|
alias BDS.Projects
|
|
|
|
@typedoc "An attribute map that may use atom or string keys."
|
|
@type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
|
|
|
|
@spec attr(attrs(), atom()) :: term()
|
|
def attr(attrs, key) do
|
|
cond do
|
|
Map.has_key?(attrs, key) -> Map.get(attrs, key)
|
|
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
|
|
true -> nil
|
|
end
|
|
end
|
|
|
|
@spec maybe_put(map(), atom(), term()) :: map()
|
|
def maybe_put(map, _key, nil), do: map
|
|
def maybe_put(map, key, value), do: Map.put(map, key, value)
|
|
|
|
@spec blank_to_nil(term()) :: term()
|
|
def blank_to_nil(nil), do: nil
|
|
def blank_to_nil(""), do: nil
|
|
def blank_to_nil(value), do: value
|
|
|
|
@spec atomic_write(Path.t(), iodata()) :: :ok | {:error, term()}
|
|
def atomic_write(path, contents), do: Persistence.atomic_write(path, contents)
|
|
|
|
@spec delete_file_if_present(String.t(), Path.t()) :: :ok | {:error, term()}
|
|
def delete_file_if_present(project_id, relative_path) do
|
|
project = Projects.get_project!(project_id)
|
|
full_path = Path.join(Projects.project_data_dir(project), relative_path)
|
|
|
|
case File.rm(full_path) do
|
|
:ok -> :ok
|
|
{:error, :enoent} -> :ok
|
|
{:error, reason} -> {:error, reason}
|
|
end
|
|
end
|
|
|
|
@spec list_matching_files(Path.t(), String.t()) :: [Path.t()]
|
|
def list_matching_files(dir, pattern) do
|
|
if File.dir?(dir) do
|
|
Path.join([dir, "**", pattern])
|
|
|> Path.wildcard()
|
|
|> Enum.sort()
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
@spec media_file_path(String.t(), integer()) :: Path.t()
|
|
def media_file_path(file_name, timestamp) do
|
|
datetime = Persistence.from_unix_ms!(timestamp)
|
|
year = Integer.to_string(datetime.year)
|
|
month = datetime.month |> Integer.to_string() |> String.pad_leading(2, "0")
|
|
Path.join(["media", year, month, file_name])
|
|
end
|
|
|
|
@spec detect_mime(String.t()) :: String.t()
|
|
def detect_mime(file_name) do
|
|
case String.downcase(Path.extname(file_name)) do
|
|
".txt" -> "text/plain"
|
|
".md" -> "text/markdown"
|
|
".jpg" -> "image/jpeg"
|
|
".jpeg" -> "image/jpeg"
|
|
".png" -> "image/png"
|
|
".gif" -> "image/gif"
|
|
".webp" -> "image/webp"
|
|
".tif" -> "image/tiff"
|
|
".tiff" -> "image/tiff"
|
|
".bmp" -> "image/bmp"
|
|
".heic" -> "image/heic"
|
|
".heif" -> "image/heif"
|
|
_ -> "application/octet-stream"
|
|
end
|
|
end
|
|
|
|
@spec image_dimensions(Path.t(), String.t() | nil) ::
|
|
{non_neg_integer() | nil, non_neg_integer() | nil}
|
|
def image_dimensions(source_path, mime_type) do
|
|
if image_mime?(mime_type) do
|
|
case Image.open(source_path) do
|
|
{:ok, image} -> {Image.width(image), Image.height(image)}
|
|
{:error, _reason} -> {nil, nil}
|
|
end
|
|
else
|
|
{nil, nil}
|
|
end
|
|
end
|
|
|
|
@spec image_mime?(String.t() | nil) :: boolean()
|
|
def image_mime?(mime_type), do: String.starts_with?(mime_type || "", "image/")
|
|
|
|
@spec progress_callback(keyword()) :: (float(), String.t() -> any()) | nil
|
|
def progress_callback(opts) do
|
|
case Keyword.get(opts, :on_progress) do
|
|
callback when is_function(callback, 2) -> callback
|
|
_other -> nil
|
|
end
|
|
end
|
|
|
|
@spec scaled_progress_reporter((float(), String.t() -> any()) | nil, float(), float()) ::
|
|
(float(), String.t() -> any()) | nil
|
|
def scaled_progress_reporter(nil, _start_value, _end_value), do: nil
|
|
|
|
def scaled_progress_reporter(report, start_value, end_value) when is_function(report, 2) do
|
|
fn value, message ->
|
|
scaled_value = start_value + (end_value - start_value) * value
|
|
report.(scaled_value, message)
|
|
end
|
|
end
|
|
|
|
@spec report_rebuild_started((float(), String.t() -> any()) | nil, non_neg_integer(), String.t()) :: :ok
|
|
def report_rebuild_started(nil, _total, _label), do: :ok
|
|
|
|
def report_rebuild_started(callback, 0, label) do
|
|
callback.(1.0, "No #{label} found")
|
|
:ok
|
|
end
|
|
|
|
def report_rebuild_started(callback, total, label) do
|
|
callback.(0.05, "Rebuilding #{label} (0/#{total})")
|
|
:ok
|
|
end
|
|
|
|
@spec report_rebuild_progress(
|
|
(float(), String.t() -> any()) | nil,
|
|
non_neg_integer(),
|
|
non_neg_integer(),
|
|
String.t()
|
|
) :: :ok
|
|
def report_rebuild_progress(nil, _current, _total, _label), do: :ok
|
|
def report_rebuild_progress(_callback, _current, 0, _label), do: :ok
|
|
|
|
def report_rebuild_progress(callback, current, total, label) do
|
|
callback.(0.05 + 0.95 * (current / total), "Rebuilding #{label} (#{current}/#{total})")
|
|
:ok
|
|
end
|
|
|
|
@spec report_rebuild_phase((float(), String.t() -> any()) | nil, float(), String.t()) :: :ok
|
|
def report_rebuild_phase(nil, _progress, _message), do: :ok
|
|
|
|
def report_rebuild_phase(callback, progress, message) do
|
|
callback.(progress, message)
|
|
:ok
|
|
end
|
|
end
|