diff options
| author | dcc <dcc@logografos.com> | 2023-09-02 00:52:52 -0700 |
|---|---|---|
| committer | dcc <dcc@logografos.com> | 2023-09-02 00:52:52 -0700 |
| commit | 3a4773c3c2bd0bbef244eb519b07208da9108e49 (patch) | |
| tree | 973567a6f3abb37bfb0f785b1cad14ed55840ef5 /lib/pleroma/uploaders | |
| download | anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.gz anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.bz2 anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.zip | |
First
Diffstat (limited to 'lib/pleroma/uploaders')
| -rw-r--r-- | lib/pleroma/uploaders/local.ex | 49 | ||||
| -rw-r--r-- | lib/pleroma/uploaders/s3.ex | 85 | ||||
| -rw-r--r-- | lib/pleroma/uploaders/uploader.ex | 84 |
3 files changed, 218 insertions, 0 deletions
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex new file mode 100644 index 0000000..e4a309c --- /dev/null +++ b/lib/pleroma/uploaders/local.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.Local do + @behaviour Pleroma.Uploaders.Uploader + + @impl true + def get_file(_) do + {:ok, {:static_dir, upload_path()}} + end + + @impl true + def put_file(upload) do + {local_path, file} = + case Enum.reverse(Path.split(upload.path)) do + [file] -> + {upload_path(), file} + + [file | folders] -> + path = Path.join([upload_path()] ++ Enum.reverse(folders)) + File.mkdir_p!(path) + {path, file} + end + + result_file = Path.join(local_path, file) + + if not File.exists?(result_file) do + File.cp!(upload.tempfile, result_file) + end + + :ok + end + + def upload_path do + Pleroma.Config.get!([__MODULE__, :uploads]) + end + + @impl true + def delete_file(path) do + upload_path() + |> Path.join(path) + |> File.rm() + |> case do + :ok -> :ok + {:error, posix_error} -> {:error, to_string(posix_error)} + end + end +end diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex new file mode 100644 index 0000000..19287c5 --- /dev/null +++ b/lib/pleroma/uploaders/s3.ex @@ -0,0 +1,85 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.S3 do + @behaviour Pleroma.Uploaders.Uploader + require Logger + + alias Pleroma.Config + + # The file name is re-encoded with S3's constraints here to comply with previous + # links with less strict filenames + @impl true + def get_file(file) do + {:ok, + {:url, + Path.join([ + Pleroma.Upload.base_url(), + strict_encode(URI.decode(file)) + ])}} + end + + @impl true + def put_file(%Pleroma.Upload{} = upload) do + config = Config.get([__MODULE__]) + bucket = Keyword.get(config, :bucket) + streaming = Keyword.get(config, :streaming_enabled) + + s3_name = strict_encode(upload.path) + + op = + if streaming do + op = + upload.tempfile + |> ExAws.S3.Upload.stream_file() + |> ExAws.S3.upload(bucket, s3_name, [ + {:acl, :public_read}, + {:content_type, upload.content_type} + ]) + + if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Gun do + # set s3 upload timeout to respect :upload pool timeout + # timeout should be slightly larger, so s3 can retry upload on fail + timeout = Pleroma.HTTP.AdapterHelper.Gun.pool_timeout(:upload) + 1_000 + opts = Keyword.put(op.opts, :timeout, timeout) + Map.put(op, :opts, opts) + else + op + end + else + {:ok, file_data} = File.read(upload.tempfile) + + ExAws.S3.put_object(bucket, s3_name, file_data, [ + {:acl, :public_read}, + {:content_type, upload.content_type} + ]) + end + + case ExAws.request(op) do + {:ok, _} -> + {:ok, {:file, s3_name}} + + error -> + Logger.error("#{__MODULE__}: #{inspect(error)}") + {:error, "S3 Upload failed"} + end + end + + @impl true + def delete_file(file) do + [__MODULE__, :bucket] + |> Config.get() + |> ExAws.S3.delete_object(file) + |> ExAws.request() + |> case do + {:ok, %{status_code: 204}} -> :ok + error -> {:error, inspect(error)} + end + end + + @regex Regex.compile!("[^0-9a-zA-Z!.*/'()_-]") + def strict_encode(name) do + String.replace(name, @regex, "-") + end +end diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex new file mode 100644 index 0000000..77f6f02 --- /dev/null +++ b/lib/pleroma/uploaders/uploader.ex @@ -0,0 +1,84 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.Uploader do + import Pleroma.Web.Gettext + + @mix_env Mix.env() + + @moduledoc """ + Defines the contract to put and get an uploaded file to any backend. + """ + + @doc """ + Instructs how to get the file from the backend. + + Used by `Pleroma.Web.Plugs.UploadedMedia`. + """ + @type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()} + @callback get_file(file :: String.t()) :: {:ok, get_method()} + + @doc """ + Put a file to the backend. + + Returns: + + * `:ok` which assumes `{:ok, upload.path}` + * `{:ok, spec}` where spec is: + * `{:file, filename :: String.t}` to handle reads with `get_file/1` (recommended) + + This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL. + * `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity. + * `{:error, String.t}` error information if the file failed to be saved to the backend. + * `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method. + + """ + @type file_spec :: {:file | :url, String.t()} + @callback put_file(upload :: struct()) :: + :ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback + + @callback delete_file(file :: String.t()) :: :ok | {:error, String.t()} + + @callback http_callback(Plug.Conn.t(), Map.t()) :: + {:ok, Plug.Conn.t()} + | {:ok, Plug.Conn.t(), file_spec()} + | {:error, Plug.Conn.t(), String.t()} + @optional_callbacks http_callback: 2 + + @spec put_file(module(), upload :: struct()) :: {:ok, file_spec()} | {:error, String.t()} + def put_file(uploader, upload) do + case uploader.put_file(upload) do + :ok -> {:ok, {:file, upload.path}} + :wait_callback -> handle_callback(uploader, upload) + {:ok, _} = ok -> ok + {:error, _} = error -> error + end + end + + defp handle_callback(uploader, upload) do + :global.register_name({__MODULE__, upload.path}, self()) + + receive do + {__MODULE__, pid, conn, params} -> + case uploader.http_callback(conn, params) do + {:ok, conn, ok} -> + send(pid, {__MODULE__, conn}) + {:ok, ok} + + {:error, conn, error} -> + send(pid, {__MODULE__, conn}) + {:error, error} + end + after + callback_timeout() -> {:error, dgettext("errors", "Uploader callback timeout")} + end + end + + defp callback_timeout do + case @mix_env do + :test -> 1_000 + _ -> 30_000 + end + end +end |
