upload.ex 6.4 KB
Newer Older
lain's avatar
lain committed
1
defmodule Pleroma.Upload do
2
  alias Ecto.UUID
href's avatar
href committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  require Logger

  @type upload_option ::
          {:dedupe, boolean()} | {:size_limit, non_neg_integer()} | {:uploader, module()}
  @type upload_source ::
          Plug.Upload.t() | data_uri_string() ::
          String.t() | {:from_local, name :: String.t(), uuid :: String.t(), path :: String.t()}

  @spec store(upload_source, options :: [upload_option()]) :: {:ok, Map.t()} | {:error, any()}
  def store(upload, opts \\ []) do
    opts = get_opts(opts)

    with {:ok, name, uuid, path, content_type} <- process_upload(upload, opts),
         _ <- strip_exif_data(content_type, path),
         {:ok, url_spec} <- opts.uploader.put_file(name, uuid, path, content_type, opts) do
      {:ok,
       %{
         "type" => "Image",
         "url" => [
           %{
             "type" => "Link",
             "mediaType" => content_type,
             "href" => url_from_spec(url_spec)
           }
         ],
         "name" => name
       }}
    else
      {:error, error} ->
        Logger.error(
          "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
        )
lain's avatar
lain committed
35

href's avatar
href committed
36
37
        {:error, error}
    end
38
39
  end

href's avatar
href committed
40
41
42
43
44
45
46
  defp get_opts(opts) do
    %{
      dedupe: Keyword.get(opts, :dedupe, Pleroma.Config.get([:instance, :dedupe_media])),
      size_limit: Keyword.get(opts, :size_limit, Pleroma.Config.get([:instance, :upload_limit])),
      uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader]))
    }
  end
47

href's avatar
href committed
48
49
50
51
52
53
  defp process_upload(%Plug.Upload{} = file, opts) do
    with :ok <- check_file_size(file.path, opts.size_limit),
         uuid <- get_uuid(file, opts.dedupe),
         content_type <- get_content_type(file.path),
         name <- get_name(file, uuid, content_type, opts.dedupe) do
      {:ok, name, uuid, file.path, content_type}
54
    end
lain's avatar
lain committed
55
  end
Thurloat's avatar
Thurloat committed
56

href's avatar
href committed
57
  defp process_upload(%{"img" => "data:image/" <> image_data}, opts) do
lain's avatar
lain committed
58
    parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
Sir_Boops's avatar
Sir_Boops committed
59
    data = Base.decode64!(parsed["data"], ignore: :whitespace)
href's avatar
href committed
60
    hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
61

href's avatar
href committed
62
63
64
    with :ok <- check_binary_size(data, opts.size_limit),
         tmp_path <- tempfile_for_image(data),
         content_type <- get_content_type(tmp_path),
65
         uuid <- UUID.generate(),
href's avatar
href committed
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
         name <- create_name(hash, parsed["filetype"], content_type) do
      {:ok, name, uuid, tmp_path, content_type}
    end
  end

  # For Mix.Tasks.MigrateLocalUploads
  defp process_upload({:from_local, name, uuid, path}, _opts) do
    with content_type <- get_content_type(path) do
      {:ok, name, uuid, path, content_type}
    end
  end

  defp check_binary_size(binary, size_limit)
       when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
    {:error, :file_too_large}
  end

  defp check_binary_size(_, _), do: :ok
84

href's avatar
href committed
85
86
87
88
  defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
    with {:ok, %{size: size}} <- File.stat(path),
         true <- size <= size_limit do
      :ok
89
    else
href's avatar
href committed
90
91
      false -> {:error, :file_too_large}
      error -> error
92
    end
lain's avatar
lain committed
93
  end
94

href's avatar
href committed
95
96
97
98
99
  defp check_file_size(_, _), do: :ok

  # Creates a tempfile using the Plug.Upload Genserver which cleans them up
  # automatically.
  defp tempfile_for_image(data) do
100
    {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
101
102
103
    {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
    IO.binwrite(tmp_file, data)

104
105
    tmp_path
  end
lain's avatar
lain committed
106

href's avatar
href committed
107
  defp strip_exif_data(content_type, file) do
gled's avatar
gled committed
108
    settings = Application.get_env(:pleroma, Pleroma.Upload)
gled's avatar
gled committed
109
    do_strip = Keyword.fetch!(settings, :strip_exif)
Thurloat's avatar
Thurloat committed
110
    [filetype, _ext] = String.split(content_type, "/")
gled's avatar
gled committed
111

gled's avatar
gled committed
112
    if filetype == "image" and do_strip == true do
gled's avatar
gled committed
113
      Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
gled's avatar
gled committed
114
115
116
    end
  end

Sir_Boops's avatar
Sir_Boops committed
117
  defp create_name(uuid, ext, type) do
href's avatar
href committed
118
119
120
121
122
123
    extension =
      cond do
        type == "application/octect-stream" -> ext
        ext = mime_extension(ext) -> ext
        true -> String.split(type, "/") |> List.last()
      end
124

href's avatar
href committed
125
126
127
128
    [uuid, extension]
    |> Enum.join(".")
    |> String.downcase()
  end
129

href's avatar
href committed
130
131
  defp mime_extension(type) do
    List.first(MIME.extensions(type))
Sir_Boops's avatar
Sir_Boops committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
  end

  defp get_uuid(file, should_dedupe) do
    if should_dedupe do
      Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
    else
      UUID.generate()
    end
  end

  defp get_name(file, uuid, type, should_dedupe) do
    if should_dedupe do
      create_name(uuid, List.last(String.split(file.filename, ".")), type)
    else
146
147
148
149
150
151
152
      parts = String.split(file.filename, ".")

      new_filename =
        if length(parts) > 1 do
          Enum.drop(parts, -1) |> Enum.join(".")
        else
          Enum.join(parts)
153
        end
154

href's avatar
href committed
155
156
157
158
159
160
161
162
163
      cond do
        type == "application/octet-stream" ->
          file.filename

        ext = mime_extension(type) ->
          new_filename <> "." <> ext

        true ->
          Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
164
      end
Sir_Boops's avatar
Sir_Boops committed
165
166
167
    end
  end

168
  def get_content_type(file) do
lain's avatar
lain committed
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
    match =
      File.open(file, [:read], fn f ->
        case IO.binread(f, 8) do
          <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
            "image/png"

          <<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
            "image/gif"

          <<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
            "image/jpeg"

          <<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
            "video/webm"

          <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
            "video/mp4"

          <<0x49, 0x44, 0x33, _, _, _, _, _>> ->
            "audio/mpeg"

eal's avatar
eal committed
190
191
192
          <<255, 251, _, 68, 0, 0, 0, 0>> ->
            "audio/mpeg"

lain's avatar
lain committed
193
          <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
rinpatch's avatar
rinpatch committed
194
            case IO.binread(f, 27) do
rinpatch's avatar
rinpatch committed
195
              <<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
rinpatch's avatar
rinpatch committed
196
                "video/ogg"
rinpatch's avatar
rinpatch committed
197

rinpatch's avatar
rinpatch committed
198
199
200
              _ ->
                "audio/ogg"
            end
lain's avatar
lain committed
201
202
203
204
205
206
207
208

          <<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
            "audio/wav"

          _ ->
            "application/octet-stream"
        end
      end)
209
210
211
212
213
214

    case match do
      {:ok, type} -> type
      _e -> "application/octet-stream"
    end
  end
href's avatar
href committed
215
216
217
218

  defp uploader() do
    Pleroma.Config.get!([Pleroma.Upload, :uploader])
  end
href's avatar
href committed
219
220
221
222
223
224
225
226
227

  defp url_from_spec({:file, path}) do
    [Pleroma.Web.base_url(), "media", path]
    |> Path.join()
  end

  defp url_from_spec({:url, url}) do
    url
  end
lain's avatar
lain committed
228
end