mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-24 20:37:59 +01:00
121 lines
3.8 KiB
Elixir
121 lines
3.8 KiB
Elixir
defmodule Philomena.Uploader do
|
|
@moduledoc """
|
|
Upload and processing callback logic for image files.
|
|
"""
|
|
|
|
alias Philomena.Filename
|
|
alias Philomena.Analyzers
|
|
alias Philomena.Sha512
|
|
import Ecto.Changeset
|
|
|
|
@doc """
|
|
Performs analysis of the passed Plug.Upload, and invokes a changeset
|
|
callback on the model or changeset passed in with attributes set on
|
|
the field_name.
|
|
"""
|
|
@spec analyze_upload(any(), String.t(), Plug.Upload.t(), (any(), map() -> Ecto.Changeset.t())) ::
|
|
Ecto.Changeset.t()
|
|
def analyze_upload(model_or_changeset, field_name, upload_parameter, changeset_fn) do
|
|
with {:ok, analysis} <- Analyzers.analyze(upload_parameter),
|
|
analysis <- extra_attributes(analysis, upload_parameter) do
|
|
removed =
|
|
model_or_changeset
|
|
|> change()
|
|
|> get_field(field(field_name))
|
|
|
|
attributes =
|
|
%{
|
|
"name" => analysis.name,
|
|
"width" => analysis.width,
|
|
"height" => analysis.height,
|
|
"size" => analysis.size,
|
|
"format" => analysis.extension,
|
|
"mime_type" => analysis.mime_type,
|
|
"duration" => analysis.duration,
|
|
"aspect_ratio" => analysis.aspect_ratio,
|
|
"orig_sha512_hash" => analysis.sha512,
|
|
"sha512_hash" => analysis.sha512,
|
|
"is_animated" => analysis.animated?
|
|
}
|
|
|> prefix_attributes(field_name)
|
|
|> Map.put(field_name, analysis.new_name)
|
|
|> Map.put(upload_key(field_name), upload_parameter.path)
|
|
|> Map.put(remove_key(field_name), removed)
|
|
|
|
changeset_fn.(model_or_changeset, attributes)
|
|
else
|
|
{:unsupported_mime, mime} ->
|
|
attributes = prefix_attributes(%{"mime_type" => mime}, field_name)
|
|
changeset_fn.(model_or_changeset, attributes)
|
|
|
|
_error ->
|
|
changeset_fn.(model_or_changeset, %{})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Writes the file to permanent storage. This should be the second-to-last step
|
|
in the transaction.
|
|
"""
|
|
@spec persist_upload(any(), String.t(), String.t()) :: any()
|
|
|
|
# sobelow_skip ["Traversal"]
|
|
def persist_upload(model, file_root, field_name) do
|
|
source = Map.get(model, field(upload_key(field_name)))
|
|
dest = Map.get(model, field(field_name))
|
|
target = Path.join(file_root, dest)
|
|
dir = Path.dirname(target)
|
|
|
|
# Create the target directory if it doesn't exist yet,
|
|
# then write the file.
|
|
File.mkdir_p!(dir)
|
|
File.cp!(source, target)
|
|
end
|
|
|
|
@doc """
|
|
Removes the old file from permanent storage. This should be the last step in
|
|
the transaction.
|
|
"""
|
|
@spec unpersist_old_upload(any(), String.t(), String.t()) :: any()
|
|
def unpersist_old_upload(model, file_root, field_name) do
|
|
model
|
|
|> Map.get(field(remove_key(field_name)))
|
|
|> try_remove(file_root)
|
|
end
|
|
|
|
defp extra_attributes(analysis, %Plug.Upload{path: path, filename: filename}) do
|
|
{width, height} = analysis.dimensions
|
|
aspect_ratio = aspect_ratio(width, height)
|
|
|
|
stat = File.stat!(path)
|
|
sha512 = Sha512.file(path)
|
|
new_name = Filename.build(analysis.extension)
|
|
|
|
analysis
|
|
|> Map.put(:size, stat.size)
|
|
|> Map.put(:name, filename)
|
|
|> Map.put(:width, width)
|
|
|> Map.put(:height, height)
|
|
|> Map.put(:sha512, sha512)
|
|
|> Map.put(:new_name, new_name)
|
|
|> Map.put(:aspect_ratio, aspect_ratio)
|
|
end
|
|
|
|
defp aspect_ratio(_, 0), do: 0.0
|
|
defp aspect_ratio(w, h), do: w / h
|
|
|
|
defp try_remove("", _file_root), do: nil
|
|
defp try_remove(nil, _file_root), do: nil
|
|
|
|
# sobelow_skip ["Traversal.FileModule"]
|
|
defp try_remove(file, file_root), do: File.rm(Path.join(file_root, file))
|
|
|
|
defp prefix_attributes(map, prefix),
|
|
do: Map.new(map, fn {key, value} -> {"#{prefix}_#{key}", value} end)
|
|
|
|
defp upload_key(field_name), do: "uploaded_#{field_name}"
|
|
|
|
defp remove_key(field_name), do: "removed_#{field_name}"
|
|
|
|
defp field(field_name), do: String.to_existing_atom(field_name)
|
|
end
|