upload.ex 7.05 KB
Newer Older
lain's avatar
lain committed
1
defmodule Pleroma.Upload do
href's avatar
href committed
2
3
4
5
6
7
  @moduledoc """
  # Upload

  Options:
  * `:type`: presets for activity type (defaults to Document) and size limits from app configuration
  * `:description`: upload alternative text
href's avatar
href committed
8
  * `:base_url`: override base url
href's avatar
href committed
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  * `:uploader`: override uploader
  * `:filters`: override filters
  * `:size_limit`: override size limit
  * `:activity_type`: override activity type

  The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:

  * `:id` - the upload id.
  * `:name` - the upload file name.
  * `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
    is once created permanent and changing it (especially in uploaders) is probably a bad idea!
  * `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
  path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.

  Related behaviors:

  * `Pleroma.Uploaders.Uploader`
  * `Pleroma.Upload.Filter`

  """
29
  alias Ecto.UUID
href's avatar
href committed
30
31
  require Logger

href's avatar
href committed
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  @type source ::
          Plug.Upload.t() | data_uri_string ::
          String.t() | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}

  @type option ::
          {:type, :avatar | :banner | :background}
          | {:description, String.t()}
          | {:activity_type, String.t()}
          | {:size_limit, nil | non_neg_integer()}
          | {:uploader, module()}
          | {:filters, [module()]}

  @type t :: %__MODULE__{
          id: String.t(),
          name: String.t(),
          tempfile: String.t(),
          content_type: String.t(),
          path: String.t()
        }
  defstruct [:id, :name, :tempfile, :content_type, :path]

  @spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
href's avatar
href committed
54
55
56
  def store(upload, opts \\ []) do
    opts = get_opts(opts)

href's avatar
href committed
57
58
59
60
    with {:ok, upload} <- prepare_upload(upload, opts),
         upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
         {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
         {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
href's avatar
href committed
61
62
      {:ok,
       %{
href's avatar
href committed
63
         "type" => opts.activity_type,
href's avatar
href committed
64
65
66
         "url" => [
           %{
             "type" => "Link",
href's avatar
href committed
67
             "mediaType" => upload.content_type,
href's avatar
href committed
68
             "href" => url_from_spec(opts.base_url, url_spec)
href's avatar
href committed
69
70
           }
         ],
href's avatar
href committed
71
         "name" => Map.get(opts, :description) || upload.name
href's avatar
href committed
72
73
74
75
76
77
       }}
    else
      {:error, error} ->
        Logger.error(
          "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
        )
lain's avatar
lain committed
78

href's avatar
href committed
79
80
        {:error, error}
    end
81
82
  end

href's avatar
href committed
83
  defp get_opts(opts) do
href's avatar
href committed
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
    {size_limit, activity_type} =
      case Keyword.get(opts, :type) do
        :banner ->
          {Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}

        :avatar ->
          {Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}

        :background ->
          {Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}

        _ ->
          {Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
      end

    opts = %{
      activity_type: Keyword.get(opts, :activity_type, activity_type),
      size_limit: Keyword.get(opts, :size_limit, size_limit),
      uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
      filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
href's avatar
href committed
104
105
106
107
108
109
110
      description: Keyword.get(opts, :description),
      base_url:
        Keyword.get(
          opts,
          :base_url,
          Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
        )
href's avatar
href committed
111
    }
href's avatar
href committed
112
113
114
115
116
117
118
119

    # TODO: 1.0+ : remove old config compatibility
    opts =
      if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
           !Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
        Logger.warn("""
        Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:

href's avatar
href committed
120
          :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
href's avatar
href committed
121

href's avatar
href committed
122
          :pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip"
href's avatar
href committed
123
124
125
126
127
128
129
130
        """)

        Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip")
        Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
      else
        opts
      end

Maksim's avatar
Maksim committed
131
132
133
134
    if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
         !Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
      Logger.warn("""
      Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
href's avatar
href committed
135

Maksim's avatar
Maksim committed
136
137
      :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
      """)
href's avatar
href committed
138

Maksim's avatar
Maksim committed
139
140
141
142
      Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
    else
      opts
    end
href's avatar
href committed
143
  end
144

href's avatar
href committed
145
  defp prepare_upload(%Plug.Upload{} = file, opts) do
href's avatar
href committed
146
    with :ok <- check_file_size(file.path, opts.size_limit),
href's avatar
href committed
147
148
149
150
151
152
153
154
         {:ok, content_type, name} <- Pleroma.MIME.file_mime_type(file.path, file.filename) do
      {:ok,
       %__MODULE__{
         id: UUID.generate(),
         name: name,
         tempfile: file.path,
         content_type: content_type
       }}
155
    end
lain's avatar
lain committed
156
  end
Thurloat's avatar
Thurloat committed
157

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

href's avatar
href committed
163
164
    with :ok <- check_binary_size(data, opts.size_limit),
         tmp_path <- tempfile_for_image(data),
href's avatar
href committed
165
166
167
168
169
170
171
172
173
         {:ok, content_type, name} <-
           Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
      {:ok,
       %__MODULE__{
         id: UUID.generate(),
         name: name,
         tempfile: tmp_path,
         content_type: content_type
       }}
href's avatar
href committed
174
175
176
177
    end
  end

  # For Mix.Tasks.MigrateLocalUploads
href's avatar
href committed
178
179
180
  defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
    with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
      {:ok, %__MODULE__{upload | content_type: content_type}}
href's avatar
href committed
181
182
183
184
185
186
187
188
189
    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
190

href's avatar
href committed
191
192
193
194
  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
195
    else
href's avatar
href committed
196
197
      false -> {:error, :file_too_large}
      error -> error
198
    end
lain's avatar
lain committed
199
  end
200

href's avatar
href committed
201
202
203
204
205
  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
206
    {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
207
208
209
    {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
    IO.binwrite(tmp_file, data)

210
211
    tmp_path
  end
lain's avatar
lain committed
212

href's avatar
href committed
213
214
  defp url_from_spec(base_url, {:file, path}) do
    [base_url, "media", path]
href's avatar
href committed
215
216
    |> Path.join()
  end
lain's avatar
lain committed
217
end