logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://anongit.hacktivis.me/git/pleroma.git/

upload.ex (10048B)


  1. # Pleroma: A lightweight social networking server
  2. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  3. # SPDX-License-Identifier: AGPL-3.0-only
  4. defmodule Pleroma.Upload do
  5. @moduledoc """
  6. Manage user uploads
  7. Options:
  8. * `:type`: presets for activity type (defaults to Document) and size limits from app configuration
  9. * `:description`: upload alternative text
  10. * `:base_url`: override base url
  11. * `:uploader`: override uploader
  12. * `:filters`: override filters
  13. * `:size_limit`: override size limit
  14. * `:activity_type`: override activity type
  15. The `%Pleroma.Upload{}` struct: all documented fields are meant to be overwritten in filters:
  16. * `:id` - the upload id.
  17. * `:name` - the upload file name.
  18. * `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
  19. is once created permanent and changing it (especially in uploaders) is probably a bad idea!
  20. * `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
  21. path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
  22. * `:width` - width of the media in pixels
  23. * `:height` - height of the media in pixels
  24. * `:blurhash` - string hash of the image encoded with the blurhash algorithm (https://blurha.sh/)
  25. Related behaviors:
  26. * `Pleroma.Uploaders.Uploader`
  27. * `Pleroma.Upload.Filter`
  28. """
  29. alias Ecto.UUID
  30. alias Pleroma.Maps
  31. alias Pleroma.Utils.URIEncoding
  32. alias Pleroma.Web.ActivityPub.Utils
  33. require Logger
  34. @type source ::
  35. Plug.Upload.t()
  36. | (data_uri_string :: String.t())
  37. | {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
  38. | map()
  39. @type option ::
  40. {:type, :avatar | :banner | :background}
  41. | {:description, String.t()}
  42. | {:activity_type, String.t()}
  43. | {:size_limit, nil | non_neg_integer()}
  44. | {:uploader, module()}
  45. | {:filters, [module()]}
  46. | {:actor, String.t()}
  47. @type t :: %__MODULE__{
  48. id: String.t(),
  49. name: String.t(),
  50. tempfile: String.t(),
  51. content_type: String.t(),
  52. width: integer(),
  53. height: integer(),
  54. blurhash: String.t(),
  55. description: String.t(),
  56. path: String.t()
  57. }
  58. defstruct [
  59. :id,
  60. :name,
  61. :tempfile,
  62. :content_type,
  63. :width,
  64. :height,
  65. :blurhash,
  66. :description,
  67. :path
  68. ]
  69. @config_impl Application.compile_env(:pleroma, [__MODULE__, :config_impl], Pleroma.Config)
  70. defp get_description(upload) do
  71. case {upload.description, Pleroma.Config.get([Pleroma.Upload, :default_description])} do
  72. {description, _} when is_binary(description) -> description
  73. {_, :filename} -> upload.name
  74. {_, str} when is_binary(str) -> str
  75. _ -> ""
  76. end
  77. end
  78. @spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()}
  79. @doc "Store a file. If using a `Plug.Upload{}` as the source, be sure to use `Majic.Plug` to ensure its content_type and filename is correct."
  80. def store(upload, opts \\ []) do
  81. opts = get_opts(opts)
  82. with {:ok, upload} <- prepare_upload(upload, opts),
  83. upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
  84. {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
  85. description = get_description(upload),
  86. {_, true} <-
  87. {:description_limit,
  88. String.length(description) <= Pleroma.Config.get([:instance, :description_limit])},
  89. {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
  90. {:ok,
  91. %{
  92. "id" => Utils.generate_object_id(),
  93. "type" => opts.activity_type,
  94. "mediaType" => upload.content_type,
  95. "url" => [
  96. %{
  97. "type" => "Link",
  98. "mediaType" => upload.content_type,
  99. "href" => url_from_spec(upload, opts.base_url, url_spec)
  100. }
  101. |> Maps.put_if_present("width", upload.width)
  102. |> Maps.put_if_present("height", upload.height)
  103. ],
  104. "name" => description
  105. }
  106. |> Maps.put_if_present("blurhash", upload.blurhash)}
  107. else
  108. {:description_limit, _} ->
  109. {:error, :description_too_long}
  110. {:error, error} ->
  111. Logger.error(
  112. "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
  113. )
  114. {:error, error}
  115. end
  116. end
  117. def char_unescaped?(char) do
  118. URI.char_unreserved?(char) or char == ?/
  119. end
  120. defp get_opts(opts) do
  121. {size_limit, activity_type} =
  122. case Keyword.get(opts, :type) do
  123. :banner ->
  124. {Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
  125. :avatar ->
  126. {Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
  127. :background ->
  128. {Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
  129. _ ->
  130. {Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
  131. end
  132. %{
  133. activity_type: Keyword.get(opts, :activity_type, activity_type),
  134. size_limit: Keyword.get(opts, :size_limit, size_limit),
  135. uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
  136. filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
  137. description: Keyword.get(opts, :description),
  138. base_url: base_url()
  139. }
  140. end
  141. defp prepare_upload(%Plug.Upload{} = file, opts) do
  142. with :ok <- check_file_size(file.path, opts.size_limit) do
  143. {:ok,
  144. %__MODULE__{
  145. id: UUID.generate(),
  146. name: file.filename,
  147. tempfile: file.path,
  148. content_type: file.content_type,
  149. description: opts.description
  150. }}
  151. end
  152. end
  153. defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
  154. parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
  155. data = Base.decode64!(parsed["data"], ignore: :whitespace)
  156. hash = Base.encode16(:crypto.hash(:sha256, data), case: :upper)
  157. with :ok <- check_binary_size(data, opts.size_limit),
  158. tmp_path <- tempfile_for_image(data),
  159. {:ok, %{mime_type: content_type}} <-
  160. Majic.perform({:bytes, data}, pool: Pleroma.MajicPool),
  161. [ext | _] <- MIME.extensions(content_type) do
  162. {:ok,
  163. %__MODULE__{
  164. id: UUID.generate(),
  165. name: hash <> "." <> ext,
  166. tempfile: tmp_path,
  167. content_type: content_type,
  168. description: opts.description
  169. }}
  170. end
  171. end
  172. # For Mix.Tasks.MigrateLocalUploads
  173. defp prepare_upload(%__MODULE__{tempfile: path} = upload, _opts) do
  174. with {:ok, %{mime_type: content_type}} <- Majic.perform(path, pool: Pleroma.MajicPool) do
  175. {:ok, %__MODULE__{upload | content_type: content_type}}
  176. end
  177. end
  178. defp check_binary_size(binary, size_limit)
  179. when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
  180. {:error, :file_too_large}
  181. end
  182. defp check_binary_size(_, _), do: :ok
  183. defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
  184. with {:ok, %{size: size}} <- File.stat(path),
  185. true <- size <= size_limit do
  186. :ok
  187. else
  188. false -> {:error, :file_too_large}
  189. error -> error
  190. end
  191. end
  192. defp check_file_size(_, _), do: :ok
  193. # Creates a tempfile using the Plug.Upload Genserver which cleans them up
  194. # automatically.
  195. defp tempfile_for_image(data) do
  196. {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
  197. {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
  198. IO.binwrite(tmp_file, data)
  199. tmp_path
  200. end
  201. # Encoding the whole path here is fine since the path is in a
  202. # UUID/<file name> form.
  203. # The file at this point isn't %-encoded, so the path shouldn't
  204. # be decoded first like Pleroma.Utils.URIEncoding.encode_url/1 does.
  205. defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
  206. encode_opts = [bypass_decode: true, bypass_parse: true]
  207. path =
  208. URIEncoding.encode_url(path, encode_opts) <>
  209. if Pleroma.Config.get([__MODULE__, :link_name], false) do
  210. enum = %{name: name}
  211. "?#{URI.encode_query(enum)}"
  212. else
  213. ""
  214. end
  215. if String.contains?(base_url, Pleroma.Uploaders.IPFS.placeholder()) do
  216. String.replace(base_url, Pleroma.Uploaders.IPFS.placeholder(), path)
  217. else
  218. [base_url, path]
  219. |> Path.join()
  220. end
  221. end
  222. defp url_from_spec(_upload, _base_url, {:url, url}), do: url
  223. @spec base_url() :: binary
  224. def base_url do
  225. uploader = @config_impl.get([Pleroma.Upload, :uploader])
  226. upload_fallback_url = Pleroma.Web.Endpoint.url() <> "/media/"
  227. upload_base_url = @config_impl.get([Pleroma.Upload, :base_url]) || upload_fallback_url
  228. public_endpoint = @config_impl.get([uploader, :public_endpoint])
  229. case uploader do
  230. Pleroma.Uploaders.Local ->
  231. upload_base_url
  232. Pleroma.Uploaders.S3 ->
  233. bucket = @config_impl.get([Pleroma.Uploaders.S3, :bucket])
  234. truncated_namespace = @config_impl.get([Pleroma.Uploaders.S3, :truncated_namespace])
  235. namespace = @config_impl.get([Pleroma.Uploaders.S3, :bucket_namespace])
  236. bucket_with_namespace =
  237. cond do
  238. !is_nil(truncated_namespace) ->
  239. truncated_namespace
  240. !is_nil(namespace) and !is_nil(bucket) ->
  241. namespace <> ":" <> bucket
  242. !is_nil(bucket) ->
  243. bucket
  244. true ->
  245. ""
  246. end
  247. if public_endpoint do
  248. Path.join([public_endpoint, bucket_with_namespace])
  249. else
  250. Path.join([upload_base_url, bucket_with_namespace])
  251. end
  252. Pleroma.Uploaders.IPFS ->
  253. @config_impl.get([Pleroma.Uploaders.IPFS, :get_gateway_url])
  254. _ ->
  255. public_endpoint || upload_base_url
  256. end
  257. end
  258. end