16043a2648dfe262ddb2601144efc14d69415310
[akkoma] / lib / pleroma / upload.ex
1 defmodule Pleroma.Upload do
2 alias Ecto.UUID
3 require Logger
4
5 @type upload_option ::
6 {:dedupe, boolean()} | {:size_limit, non_neg_integer()} | {:uploader, module()}
7 @type upload_source ::
8 Plug.Upload.t() | data_uri_string() ::
9 String.t() | {:from_local, name :: String.t(), uuid :: String.t(), path :: String.t()}
10
11 @spec store(upload_source, options :: [upload_option()]) :: {:ok, Map.t()} | {:error, any()}
12 def store(upload, opts \\ []) do
13 opts = get_opts(opts)
14
15 with {:ok, name, uuid, path, content_type} <- process_upload(upload, opts),
16 _ <- strip_exif_data(content_type, path),
17 {:ok, url_spec} <- opts.uploader.put_file(name, uuid, path, content_type, opts) do
18 {:ok,
19 %{
20 "type" => "Image",
21 "url" => [
22 %{
23 "type" => "Link",
24 "mediaType" => content_type,
25 "href" => url_from_spec(url_spec)
26 }
27 ],
28 "name" => name
29 }}
30 else
31 {:error, error} ->
32 Logger.error(
33 "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
34 )
35
36 {:error, error}
37 end
38 end
39
40 defp get_opts(opts) do
41 %{
42 dedupe: Keyword.get(opts, :dedupe, Pleroma.Config.get([:instance, :dedupe_media])),
43 size_limit: Keyword.get(opts, :size_limit, Pleroma.Config.get([:instance, :upload_limit])),
44 uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader]))
45 }
46 end
47
48 defp process_upload(%Plug.Upload{} = file, opts) do
49 with :ok <- check_file_size(file.path, opts.size_limit),
50 uuid <- get_uuid(file, opts.dedupe),
51 content_type <- get_content_type(file.path),
52 name <- get_name(file, uuid, content_type, opts.dedupe) do
53 {:ok, name, uuid, file.path, content_type}
54 end
55 end
56
57 defp process_upload(%{"img" => "data:image/" <> image_data}, opts) do
58 parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
59 data = Base.decode64!(parsed["data"], ignore: :whitespace)
60 hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
61
62 with :ok <- check_binary_size(data, opts.size_limit),
63 tmp_path <- tempfile_for_image(data),
64 content_type <- get_content_type(tmp_path),
65 uuid <- UUID.generate(),
66 name <- create_name(hash, parsed["filetype"], content_type) do
67 {:ok, name, uuid, tmp_path, content_type}
68 end
69 end
70
71 # For Mix.Tasks.MigrateLocalUploads
72 defp process_upload({:from_local, name, uuid, path}, _opts) do
73 with content_type <- get_content_type(path) do
74 {:ok, name, uuid, path, content_type}
75 end
76 end
77
78 defp check_binary_size(binary, size_limit)
79 when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
80 {:error, :file_too_large}
81 end
82
83 defp check_binary_size(_, _), do: :ok
84
85 defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
86 with {:ok, %{size: size}} <- File.stat(path),
87 true <- size <= size_limit do
88 :ok
89 else
90 false -> {:error, :file_too_large}
91 error -> error
92 end
93 end
94
95 defp check_file_size(_, _), do: :ok
96
97 # Creates a tempfile using the Plug.Upload Genserver which cleans them up
98 # automatically.
99 defp tempfile_for_image(data) do
100 {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
101 {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
102 IO.binwrite(tmp_file, data)
103
104 tmp_path
105 end
106
107 defp strip_exif_data(content_type, file) do
108 settings = Application.get_env(:pleroma, Pleroma.Upload)
109 do_strip = Keyword.fetch!(settings, :strip_exif)
110 [filetype, _ext] = String.split(content_type, "/")
111
112 if filetype == "image" and do_strip == true do
113 Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
114 end
115 end
116
117 defp create_name(uuid, ext, type) do
118 extension =
119 cond do
120 type == "application/octect-stream" -> ext
121 ext = mime_extension(ext) -> ext
122 true -> String.split(type, "/") |> List.last()
123 end
124
125 [uuid, extension]
126 |> Enum.join(".")
127 |> String.downcase()
128 end
129
130 defp mime_extension(type) do
131 List.first(MIME.extensions(type))
132 end
133
134 defp get_uuid(file, should_dedupe) do
135 if should_dedupe do
136 Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
137 else
138 UUID.generate()
139 end
140 end
141
142 defp get_name(file, uuid, type, should_dedupe) do
143 if should_dedupe do
144 create_name(uuid, List.last(String.split(file.filename, ".")), type)
145 else
146 parts = String.split(file.filename, ".")
147
148 new_filename =
149 if length(parts) > 1 do
150 Enum.drop(parts, -1) |> Enum.join(".")
151 else
152 Enum.join(parts)
153 end
154
155 cond do
156 type == "application/octet-stream" ->
157 file.filename
158
159 ext = mime_extension(type) ->
160 new_filename <> "." <> ext
161
162 true ->
163 Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
164 end
165 end
166 end
167
168 def get_content_type(file) do
169 match =
170 File.open(file, [:read], fn f ->
171 case IO.binread(f, 8) do
172 <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
173 "image/png"
174
175 <<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
176 "image/gif"
177
178 <<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
179 "image/jpeg"
180
181 <<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
182 "video/webm"
183
184 <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
185 "video/mp4"
186
187 <<0x49, 0x44, 0x33, _, _, _, _, _>> ->
188 "audio/mpeg"
189
190 <<255, 251, _, 68, 0, 0, 0, 0>> ->
191 "audio/mpeg"
192
193 <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
194 case IO.binread(f, 27) do
195 <<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
196 "video/ogg"
197
198 _ ->
199 "audio/ogg"
200 end
201
202 <<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
203 "audio/wav"
204
205 _ ->
206 "application/octet-stream"
207 end
208 end)
209
210 case match do
211 {:ok, type} -> type
212 _e -> "application/octet-stream"
213 end
214 end
215
216 defp uploader() do
217 Pleroma.Config.get!([Pleroma.Upload, :uploader])
218 end
219
220 defp url_from_spec({:file, path}) do
221 [Pleroma.Web.base_url(), "media", path]
222 |> Path.join()
223 end
224
225 defp url_from_spec({:url, url}) do
226 url
227 end
228 end