logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git
commit: ad605e3e16ba3f6ee3df7a0a3e6705036fef369f
parent c1976d5b19fbceaecf1f52711fe35e1c7d5312aa
Author: Egor Kislitsyn <egor@kislitsyn.com>
Date:   Tue, 20 Oct 2020 17:16:58 +0400

Rename `Pleroma.Backup` to `Pleroma.User.Backup`

Diffstat:

Mconfig/config.exs2+-
Mconfig/description.exs2+-
Mdocs/configuration/cheatsheet.md2+-
Dlib/pleroma/backup.ex258-------------------------------------------------------------------------------
Alib/pleroma/user/backup.ex258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/admin_api/controllers/admin_api_controller.ex2+-
Mlib/pleroma/web/pleroma_api/controllers/backup_controller.ex7++++---
Mlib/pleroma/web/pleroma_api/views/backup_view.ex2+-
Mlib/pleroma/workers/backup_worker.ex4++--
Dtest/pleroma/backup_test.exs244-------------------------------------------------------------------------------
Atest/pleroma/user/backup_test.exs244+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/pleroma/web/admin_api/controllers/admin_api_controller_test.exs6+++---
Mtest/pleroma/web/pleroma_api/controllers/backup_controller_test.exs2+-
13 files changed, 517 insertions(+), 516 deletions(-)

diff --git a/config/config.exs b/config/config.exs @@ -831,7 +831,7 @@ config :floki, :html_parser, Floki.HTMLParser.FastHtml config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator -config :pleroma, Pleroma.Backup, +config :pleroma, Pleroma.User.Backup, purge_after_days: 30, limit_days: 7, dir: nil diff --git a/config/description.exs b/config/description.exs @@ -3731,7 +3731,7 @@ config :pleroma, :config_description, [ }, %{ group: :pleroma, - key: Pleroma.Backup, + key: Pleroma.User.Backup, type: :group, description: "Account Backup", children: [ diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md @@ -1077,7 +1077,7 @@ Control favicons for instances. * `enabled`: Allow/disallow displaying and getting instances favicons -## Account Backup +## Pleroma.User.Backup !!! note Requires enabled email diff --git a/lib/pleroma/backup.ex b/lib/pleroma/backup.ex @@ -1,258 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Backup do - use Ecto.Schema - - import Ecto.Changeset - import Ecto.Query - import Pleroma.Web.Gettext - - require Pleroma.Constants - - alias Pleroma.Activity - alias Pleroma.Bookmark - alias Pleroma.Repo - alias Pleroma.User - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.ActivityPub.UserView - alias Pleroma.Workers.BackupWorker - - schema "backups" do - field(:content_type, :string) - field(:file_name, :string) - field(:file_size, :integer, default: 0) - field(:processed, :boolean, default: false) - - belongs_to(:user, User, type: FlakeId.Ecto.CompatType) - - timestamps() - end - - def create(user, admin_id \\ nil) do - with :ok <- validate_email_enabled(), - :ok <- validate_user_email(user), - :ok <- validate_limit(user, admin_id), - {:ok, backup} <- user |> new() |> Repo.insert() do - BackupWorker.process(backup, admin_id) - end - end - - def new(user) do - rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) - name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip" - - %__MODULE__{ - user_id: user.id, - content_type: "application/zip", - file_name: name - } - end - - def delete(backup) do - uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) - - with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do - Repo.delete(backup) - end - end - - defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok - - defp validate_limit(user, nil) do - case get_last(user.id) do - %__MODULE__{inserted_at: inserted_at} -> - days = Pleroma.Config.get([Pleroma.Backup, :limit_days]) - diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) - - if diff > days do - :ok - else - {:error, - dngettext( - "errors", - "Last export was less than a day ago", - "Last export was less than %{days} days ago", - days, - days: days - )} - end - - nil -> - :ok - end - end - - defp validate_email_enabled do - if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do - :ok - else - {:error, dgettext("errors", "Backups require enabled email")} - end - end - - defp validate_user_email(%User{email: nil}) do - {:error, dgettext("errors", "Email is required")} - end - - defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok - - def get_last(user_id) do - __MODULE__ - |> where(user_id: ^user_id) - |> order_by(desc: :id) - |> limit(1) - |> Repo.one() - end - - def list(%User{id: user_id}) do - __MODULE__ - |> where(user_id: ^user_id) - |> order_by(desc: :id) - |> Repo.all() - end - - def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do - __MODULE__ - |> where(user_id: ^user_id) - |> where([b], b.id != ^latest_id) - |> Repo.all() - |> Enum.each(&BackupWorker.delete/1) - end - - def get(id), do: Repo.get(__MODULE__, id) - - def process(%__MODULE__{} = backup) do - with {:ok, zip_file} <- export(backup), - {:ok, %{size: size}} <- File.stat(zip_file), - {:ok, _upload} <- upload(backup, zip_file) do - backup - |> cast(%{file_size: size, processed: true}, [:file_size, :processed]) - |> Repo.update() - end - end - - @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] - def export(%__MODULE__{} = backup) do - backup = Repo.preload(backup, :user) - name = String.trim_trailing(backup.file_name, ".zip") - dir = dir(name) - - with :ok <- File.mkdir(dir), - :ok <- actor(dir, backup.user), - :ok <- statuses(dir, backup.user), - :ok <- likes(dir, backup.user), - :ok <- bookmarks(dir, backup.user), - {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), - {:ok, _} <- File.rm_rf(dir) do - {:ok, to_string(zip_path)} - end - end - - def dir(name) do - dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!() - Path.join(dir, name) - end - - def upload(%__MODULE__{} = backup, zip_path) do - uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) - - upload = %Pleroma.Upload{ - name: backup.file_name, - tempfile: zip_path, - content_type: backup.content_type, - path: Path.join("backups", backup.file_name) - } - - with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), - :ok <- File.rm(zip_path) do - {:ok, upload} - end - end - - defp actor(dir, user) do - with {:ok, json} <- - UserView.render("user.json", %{user: user}) - |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) - |> Jason.encode() do - File.write(Path.join(dir, "actor.json"), json) - end - end - - defp write_header(file, name) do - IO.write( - file, - """ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "#{name}.json", - "type": "OrderedCollection", - "orderedItems": [ - - """ - ) - end - - defp write(query, dir, name, fun) do - path = Path.join(dir, "#{name}.json") - - with {:ok, file} <- File.open(path, [:write, :utf8]), - :ok <- write_header(file, name) do - total = - query - |> Pleroma.Repo.chunk_stream(100) - |> Enum.reduce(0, fn i, acc -> - with {:ok, data} <- fun.(i), - {:ok, str} <- Jason.encode(data), - :ok <- IO.write(file, str <> ",\n") do - acc + 1 - else - _ -> acc - end - end) - - with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do - File.close(file) - end - end - end - - defp bookmarks(dir, %{id: user_id} = _user) do - Bookmark - |> where(user_id: ^user_id) - |> join(:inner, [b], activity in assoc(b, :activity)) - |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) - |> write(dir, "bookmarks", fn a -> {:ok, a.object} end) - end - - defp likes(dir, user) do - user.ap_id - |> Activity.Queries.by_actor() - |> Activity.Queries.by_type("Like") - |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) - |> write(dir, "likes", fn a -> {:ok, a.object} end) - end - - defp statuses(dir, user) do - opts = - %{} - |> Map.put(:type, ["Create", "Announce"]) - |> Map.put(:actor_id, user.ap_id) - - [ - [Pleroma.Constants.as_public(), user.ap_id], - User.following(user), - Pleroma.List.memberships(user) - ] - |> Enum.concat() - |> ActivityPub.fetch_activities_query(opts) - |> write(dir, "outbox", fn a -> - with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do - {:ok, Map.delete(activity, "@context")} - end - end) - end -end diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex @@ -0,0 +1,258 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Backup do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + import Pleroma.Web.Gettext + + require Pleroma.Constants + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.UserView + alias Pleroma.Workers.BackupWorker + + schema "backups" do + field(:content_type, :string) + field(:file_name, :string) + field(:file_size, :integer, default: 0) + field(:processed, :boolean, default: false) + + belongs_to(:user, User, type: FlakeId.Ecto.CompatType) + + timestamps() + end + + def create(user, admin_id \\ nil) do + with :ok <- validate_email_enabled(), + :ok <- validate_user_email(user), + :ok <- validate_limit(user, admin_id), + {:ok, backup} <- user |> new() |> Repo.insert() do + BackupWorker.process(backup, admin_id) + end + end + + def new(user) do + rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now()) + name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip" + + %__MODULE__{ + user_id: user.id, + content_type: "application/zip", + file_name: name + } + end + + def delete(backup) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + + with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do + Repo.delete(backup) + end + end + + defp validate_limit(_user, admin_id) when is_binary(admin_id), do: :ok + + defp validate_limit(user, nil) do + case get_last(user.id) do + %__MODULE__{inserted_at: inserted_at} -> + days = Pleroma.Config.get([__MODULE__, :limit_days]) + diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days) + + if diff > days do + :ok + else + {:error, + dngettext( + "errors", + "Last export was less than a day ago", + "Last export was less than %{days} days ago", + days, + days: days + )} + end + + nil -> + :ok + end + end + + defp validate_email_enabled do + if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do + :ok + else + {:error, dgettext("errors", "Backups require enabled email")} + end + end + + defp validate_user_email(%User{email: nil}) do + {:error, dgettext("errors", "Email is required")} + end + + defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok + + def get_last(user_id) do + __MODULE__ + |> where(user_id: ^user_id) + |> order_by(desc: :id) + |> limit(1) + |> Repo.one() + end + + def list(%User{id: user_id}) do + __MODULE__ + |> where(user_id: ^user_id) + |> order_by(desc: :id) + |> Repo.all() + end + + def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do + __MODULE__ + |> where(user_id: ^user_id) + |> where([b], b.id != ^latest_id) + |> Repo.all() + |> Enum.each(&BackupWorker.delete/1) + end + + def get(id), do: Repo.get(__MODULE__, id) + + def process(%__MODULE__{} = backup) do + with {:ok, zip_file} <- export(backup), + {:ok, %{size: size}} <- File.stat(zip_file), + {:ok, _upload} <- upload(backup, zip_file) do + backup + |> cast(%{file_size: size, processed: true}, [:file_size, :processed]) + |> Repo.update() + end + end + + @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json'] + def export(%__MODULE__{} = backup) do + backup = Repo.preload(backup, :user) + name = String.trim_trailing(backup.file_name, ".zip") + dir = dir(name) + + with :ok <- File.mkdir(dir), + :ok <- actor(dir, backup.user), + :ok <- statuses(dir, backup.user), + :ok <- likes(dir, backup.user), + :ok <- bookmarks(dir, backup.user), + {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir), + {:ok, _} <- File.rm_rf(dir) do + {:ok, to_string(zip_path)} + end + end + + def dir(name) do + dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!() + Path.join(dir, name) + end + + def upload(%__MODULE__{} = backup, zip_path) do + uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + + upload = %Pleroma.Upload{ + name: backup.file_name, + tempfile: zip_path, + content_type: backup.content_type, + path: Path.join("backups", backup.file_name) + } + + with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload), + :ok <- File.rm(zip_path) do + {:ok, upload} + end + end + + defp actor(dir, user) do + with {:ok, json} <- + UserView.render("user.json", %{user: user}) + |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"}) + |> Jason.encode() do + File.write(Path.join(dir, "actor.json"), json) + end + end + + defp write_header(file, name) do + IO.write( + file, + """ + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "#{name}.json", + "type": "OrderedCollection", + "orderedItems": [ + + """ + ) + end + + defp write(query, dir, name, fun) do + path = Path.join(dir, "#{name}.json") + + with {:ok, file} <- File.open(path, [:write, :utf8]), + :ok <- write_header(file, name) do + total = + query + |> Pleroma.Repo.chunk_stream(100) + |> Enum.reduce(0, fn i, acc -> + with {:ok, data} <- fun.(i), + {:ok, str} <- Jason.encode(data), + :ok <- IO.write(file, str <> ",\n") do + acc + 1 + else + _ -> acc + end + end) + + with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do + File.close(file) + end + end + end + + defp bookmarks(dir, %{id: user_id} = _user) do + Bookmark + |> where(user_id: ^user_id) + |> join(:inner, [b], activity in assoc(b, :activity)) + |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)}) + |> write(dir, "bookmarks", fn a -> {:ok, a.object} end) + end + + defp likes(dir, user) do + user.ap_id + |> Activity.Queries.by_actor() + |> Activity.Queries.by_type("Like") + |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)}) + |> write(dir, "likes", fn a -> {:ok, a.object} end) + end + + defp statuses(dir, user) do + opts = + %{} + |> Map.put(:type, ["Create", "Announce"]) + |> Map.put(:actor_id, user.ap_id) + + [ + [Pleroma.Constants.as_public(), user.ap_id], + User.following(user), + Pleroma.List.memberships(user) + ] + |> Enum.concat() + |> ActivityPub.fetch_activities_query(opts) + |> write(dir, "outbox", fn a -> + with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do + {:ok, Map.delete(activity, "@context")} + end + end) + end +end diff --git a/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex b/lib/pleroma/web/admin_api/controllers/admin_api_controller.ex @@ -685,7 +685,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_by_nickname(nickname), - {:ok, _} <- Pleroma.Backup.create(user, admin.id) do + {:ok, _} <- Pleroma.User.Backup.create(user, admin.id) do ModerationLog.insert_log(%{actor: admin, subject: user, action: "create_backup"}) json(conn, "") diff --git a/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex b/lib/pleroma/web/pleroma_api/controllers/backup_controller.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupController do use Pleroma.Web, :controller alias Pleroma.Web.Plugs.OAuthScopesPlug + alias Pleroma.User.Backup action_fallback(Pleroma.Web.MastodonAPI.FallbackController) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action in [:index, :create]) @@ -14,13 +15,13 @@ defmodule Pleroma.Web.PleromaAPI.BackupController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBackupOperation def index(%{assigns: %{user: user}} = conn, _params) do - backups = Pleroma.Backup.list(user) + backups = Backup.list(user) render(conn, "index.json", backups: backups) end def create(%{assigns: %{user: user}} = conn, _params) do - with {:ok, _} <- Pleroma.Backup.create(user) do - backups = Pleroma.Backup.list(user) + with {:ok, _} <- Backup.create(user) do + backups = Backup.list(user) render(conn, "index.json", backups: backups) end end diff --git a/lib/pleroma/web/pleroma_api/views/backup_view.ex b/lib/pleroma/web/pleroma_api/views/backup_view.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do use Pleroma.Web, :view - alias Pleroma.Backup + alias Pleroma.User.Backup alias Pleroma.Web.CommonAPI.Utils def render("show.json", %{backup: %Backup{} = backup}) do diff --git a/lib/pleroma/workers/backup_worker.ex b/lib/pleroma/workers/backup_worker.ex @@ -6,7 +6,7 @@ defmodule Pleroma.Workers.BackupWorker do use Oban.Worker, queue: :backup, max_attempts: 1 alias Oban.Job - alias Pleroma.Backup + alias Pleroma.User.Backup def process(backup, admin_user_id \\ nil) do %{"op" => "process", "backup_id" => backup.id, "admin_user_id" => admin_user_id} @@ -15,7 +15,7 @@ defmodule Pleroma.Workers.BackupWorker do end def schedule_deletion(backup) do - days = Pleroma.Config.get([Pleroma.Backup, :purge_after_days]) + days = Pleroma.Config.get([Backup, :purge_after_days]) time = 60 * 60 * 24 * days scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time) diff --git a/test/pleroma/backup_test.exs b/test/pleroma/backup_test.exs @@ -1,244 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.BackupTest do - use Oban.Testing, repo: Pleroma.Repo - use Pleroma.DataCase - - import Mock - import Pleroma.Factory - import Swoosh.TestAssertions - - alias Pleroma.Backup - alias Pleroma.Bookmark - alias Pleroma.Tests.ObanHelpers - alias Pleroma.Web.CommonAPI - alias Pleroma.Workers.BackupWorker - - setup do - clear_config([Pleroma.Upload, :uploader]) - clear_config([Pleroma.Backup, :limit_days]) - clear_config([Pleroma.Emails.Mailer, :enabled], true) - end - - test "it requries enabled email" do - Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) - user = insert(:user) - assert {:error, "Backups require enabled email"} == Backup.create(user) - end - - test "it requries user's email" do - user = insert(:user, %{email: nil}) - assert {:error, "Email is required"} == Backup.create(user) - end - - test "it creates a backup record and an Oban job" do - %{id: user_id} = user = insert(:user) - assert {:ok, %Oban.Job{args: args}} = Backup.create(user) - assert_enqueued(worker: BackupWorker, args: args) - - backup = Backup.get(args["backup_id"]) - assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup - end - - test "it return an error if the export limit is over" do - %{id: user_id} = user = insert(:user) - limit_days = Pleroma.Config.get([Pleroma.Backup, :limit_days]) - assert {:ok, %Oban.Job{args: args}} = Backup.create(user) - backup = Backup.get(args["backup_id"]) - assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup - - assert Backup.create(user) == {:error, "Last export was less than #{limit_days} days ago"} - end - - test "it process a backup record" do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - %{id: user_id} = user = insert(:user) - - assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) - assert {:ok, backup} = perform_job(BackupWorker, args) - assert backup.file_size > 0 - assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup - - delete_job_args = %{"op" => "delete", "backup_id" => backup_id} - - assert_enqueued(worker: BackupWorker, args: delete_job_args) - assert {:ok, backup} = perform_job(BackupWorker, delete_job_args) - refute Backup.get(backup_id) - - email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup) - - assert_email_sent( - to: {user.name, user.email}, - html_body: email.html_body - ) - end - - test "it removes outdated backups after creating a fresh one" do - Pleroma.Config.put([Pleroma.Backup, :limit_days], -1) - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - user = insert(:user) - - assert {:ok, job1} = Backup.create(user) - - assert {:ok, %Backup{id: backup1_id}} = ObanHelpers.perform(job1) - assert {:ok, job2} = Backup.create(user) - assert Pleroma.Repo.aggregate(Backup, :count) == 2 - assert {:ok, backup2} = ObanHelpers.perform(job2) - - ObanHelpers.perform_all() - - assert [^backup2] = Pleroma.Repo.all(Backup) - end - - test "it creates a zip archive with user data" do - user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) - - {:ok, %{object: %{data: %{"id" => id1}}} = status1} = - CommonAPI.post(user, %{status: "status1"}) - - {:ok, %{object: %{data: %{"id" => id2}}} = status2} = - CommonAPI.post(user, %{status: "status2"}) - - {:ok, %{object: %{data: %{"id" => id3}}} = status3} = - CommonAPI.post(user, %{status: "status3"}) - - CommonAPI.favorite(user, status1.id) - CommonAPI.favorite(user, status2.id) - - Bookmark.create(user.id, status2.id) - Bookmark.create(user.id, status3.id) - - assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) - assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory]) - assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile) - - assert %{ - "@context" => [ - "https://www.w3.org/ns/activitystreams", - "http://localhost:4001/schemas/litepub-0.1.jsonld", - %{"@language" => "und"} - ], - "bookmarks" => "bookmarks.json", - "followers" => "http://cofe.io/users/cofe/followers", - "following" => "http://cofe.io/users/cofe/following", - "id" => "http://cofe.io/users/cofe", - "inbox" => "http://cofe.io/users/cofe/inbox", - "likes" => "likes.json", - "name" => "Cofe", - "outbox" => "http://cofe.io/users/cofe/outbox", - "preferredUsername" => "cofe", - "publicKey" => %{ - "id" => "http://cofe.io/users/cofe#main-key", - "owner" => "http://cofe.io/users/cofe" - }, - "type" => "Person", - "url" => "http://cofe.io/users/cofe" - } = Jason.decode!(json) - - assert {:ok, {'outbox.json', json}} = :zip.zip_get('outbox.json', zipfile) - - assert %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "outbox.json", - "orderedItems" => [ - %{ - "object" => %{ - "actor" => "http://cofe.io/users/cofe", - "content" => "status1", - "type" => "Note" - }, - "type" => "Create" - }, - %{ - "object" => %{ - "actor" => "http://cofe.io/users/cofe", - "content" => "status2" - } - }, - %{ - "actor" => "http://cofe.io/users/cofe", - "object" => %{ - "content" => "status3" - } - } - ], - "totalItems" => 3, - "type" => "OrderedCollection" - } = Jason.decode!(json) - - assert {:ok, {'likes.json', json}} = :zip.zip_get('likes.json', zipfile) - - assert %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "likes.json", - "orderedItems" => [^id1, ^id2], - "totalItems" => 2, - "type" => "OrderedCollection" - } = Jason.decode!(json) - - assert {:ok, {'bookmarks.json', json}} = :zip.zip_get('bookmarks.json', zipfile) - - assert %{ - "@context" => "https://www.w3.org/ns/activitystreams", - "id" => "bookmarks.json", - "orderedItems" => [^id2, ^id3], - "totalItems" => 2, - "type" => "OrderedCollection" - } = Jason.decode!(json) - - :zip.zip_close(zipfile) - File.rm!(path) - end - - describe "it uploads and deletes a backup archive" do - setup do - clear_config(Pleroma.Uploaders.S3, - bucket: "test_bucket", - public_endpoint: "https://s3.amazonaws.com" - ) - - clear_config([Pleroma.Upload, :uploader]) - - user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) - - {:ok, status1} = CommonAPI.post(user, %{status: "status1"}) - {:ok, status2} = CommonAPI.post(user, %{status: "status2"}) - {:ok, status3} = CommonAPI.post(user, %{status: "status3"}) - CommonAPI.favorite(user, status1.id) - CommonAPI.favorite(user, status2.id) - Bookmark.create(user.id, status2.id) - Bookmark.create(user.id, status3.id) - - assert {:ok, backup} = user |> Backup.new() |> Repo.insert() - assert {:ok, path} = Backup.export(backup) - - [path: path, backup: backup] - end - - test "S3", %{path: path, backup: backup} do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.S3) - - with_mock ExAws, - request: fn - %{http_method: :put} -> {:ok, :ok} - %{http_method: :delete} -> {:ok, %{status_code: 204}} - end do - assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) - assert {:ok, _backup} = Backup.delete(backup) - end - - with_mock ExAws, request: fn %{http_method: :delete} -> {:ok, %{status_code: 204}} end do - end - end - - test "Local", %{path: path, backup: backup} do - Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) - - assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) - assert {:ok, _backup} = Backup.delete(backup) - end - end -end diff --git a/test/pleroma/user/backup_test.exs b/test/pleroma/user/backup_test.exs @@ -0,0 +1,244 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.BackupTest do + use Oban.Testing, repo: Pleroma.Repo + use Pleroma.DataCase + + import Mock + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.User.Backup + alias Pleroma.Bookmark + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Web.CommonAPI + alias Pleroma.Workers.BackupWorker + + setup do + clear_config([Pleroma.Upload, :uploader]) + clear_config([Backup, :limit_days]) + clear_config([Pleroma.Emails.Mailer, :enabled], true) + end + + test "it requries enabled email" do + Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false) + user = insert(:user) + assert {:error, "Backups require enabled email"} == Backup.create(user) + end + + test "it requries user's email" do + user = insert(:user, %{email: nil}) + assert {:error, "Email is required"} == Backup.create(user) + end + + test "it creates a backup record and an Oban job" do + %{id: user_id} = user = insert(:user) + assert {:ok, %Oban.Job{args: args}} = Backup.create(user) + assert_enqueued(worker: BackupWorker, args: args) + + backup = Backup.get(args["backup_id"]) + assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup + end + + test "it return an error if the export limit is over" do + %{id: user_id} = user = insert(:user) + limit_days = Pleroma.Config.get([Backup, :limit_days]) + assert {:ok, %Oban.Job{args: args}} = Backup.create(user) + backup = Backup.get(args["backup_id"]) + assert %Backup{user_id: ^user_id, processed: false, file_size: 0} = backup + + assert Backup.create(user) == {:error, "Last export was less than #{limit_days} days ago"} + end + + test "it process a backup record" do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + %{id: user_id} = user = insert(:user) + + assert {:ok, %Oban.Job{args: %{"backup_id" => backup_id} = args}} = Backup.create(user) + assert {:ok, backup} = perform_job(BackupWorker, args) + assert backup.file_size > 0 + assert %Backup{id: ^backup_id, processed: true, user_id: ^user_id} = backup + + delete_job_args = %{"op" => "delete", "backup_id" => backup_id} + + assert_enqueued(worker: BackupWorker, args: delete_job_args) + assert {:ok, backup} = perform_job(BackupWorker, delete_job_args) + refute Backup.get(backup_id) + + email = Pleroma.Emails.UserEmail.backup_is_ready_email(backup) + + assert_email_sent( + to: {user.name, user.email}, + html_body: email.html_body + ) + end + + test "it removes outdated backups after creating a fresh one" do + Pleroma.Config.put([Backup, :limit_days], -1) + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + user = insert(:user) + + assert {:ok, job1} = Backup.create(user) + + assert {:ok, %Backup{id: backup1_id}} = ObanHelpers.perform(job1) + assert {:ok, job2} = Backup.create(user) + assert Pleroma.Repo.aggregate(Backup, :count) == 2 + assert {:ok, backup2} = ObanHelpers.perform(job2) + + ObanHelpers.perform_all() + + assert [^backup2] = Pleroma.Repo.all(Backup) + end + + test "it creates a zip archive with user data" do + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + {:ok, %{object: %{data: %{"id" => id1}}} = status1} = + CommonAPI.post(user, %{status: "status1"}) + + {:ok, %{object: %{data: %{"id" => id2}}} = status2} = + CommonAPI.post(user, %{status: "status2"}) + + {:ok, %{object: %{data: %{"id" => id3}}} = status3} = + CommonAPI.post(user, %{status: "status3"}) + + CommonAPI.favorite(user, status1.id) + CommonAPI.favorite(user, status2.id) + + Bookmark.create(user.id, status2.id) + Bookmark.create(user.id, status3.id) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + assert {:ok, path} = Backup.export(backup) + assert {:ok, zipfile} = :zip.zip_open(String.to_charlist(path), [:memory]) + assert {:ok, {'actor.json', json}} = :zip.zip_get('actor.json', zipfile) + + assert %{ + "@context" => [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{"@language" => "und"} + ], + "bookmarks" => "bookmarks.json", + "followers" => "http://cofe.io/users/cofe/followers", + "following" => "http://cofe.io/users/cofe/following", + "id" => "http://cofe.io/users/cofe", + "inbox" => "http://cofe.io/users/cofe/inbox", + "likes" => "likes.json", + "name" => "Cofe", + "outbox" => "http://cofe.io/users/cofe/outbox", + "preferredUsername" => "cofe", + "publicKey" => %{ + "id" => "http://cofe.io/users/cofe#main-key", + "owner" => "http://cofe.io/users/cofe" + }, + "type" => "Person", + "url" => "http://cofe.io/users/cofe" + } = Jason.decode!(json) + + assert {:ok, {'outbox.json', json}} = :zip.zip_get('outbox.json', zipfile) + + assert %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "outbox.json", + "orderedItems" => [ + %{ + "object" => %{ + "actor" => "http://cofe.io/users/cofe", + "content" => "status1", + "type" => "Note" + }, + "type" => "Create" + }, + %{ + "object" => %{ + "actor" => "http://cofe.io/users/cofe", + "content" => "status2" + } + }, + %{ + "actor" => "http://cofe.io/users/cofe", + "object" => %{ + "content" => "status3" + } + } + ], + "totalItems" => 3, + "type" => "OrderedCollection" + } = Jason.decode!(json) + + assert {:ok, {'likes.json', json}} = :zip.zip_get('likes.json', zipfile) + + assert %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "likes.json", + "orderedItems" => [^id1, ^id2], + "totalItems" => 2, + "type" => "OrderedCollection" + } = Jason.decode!(json) + + assert {:ok, {'bookmarks.json', json}} = :zip.zip_get('bookmarks.json', zipfile) + + assert %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "bookmarks.json", + "orderedItems" => [^id2, ^id3], + "totalItems" => 2, + "type" => "OrderedCollection" + } = Jason.decode!(json) + + :zip.zip_close(zipfile) + File.rm!(path) + end + + describe "it uploads and deletes a backup archive" do + setup do + clear_config(Pleroma.Uploaders.S3, + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) + + clear_config([Pleroma.Upload, :uploader]) + + user = insert(:user, %{nickname: "cofe", name: "Cofe", ap_id: "http://cofe.io/users/cofe"}) + + {:ok, status1} = CommonAPI.post(user, %{status: "status1"}) + {:ok, status2} = CommonAPI.post(user, %{status: "status2"}) + {:ok, status3} = CommonAPI.post(user, %{status: "status3"}) + CommonAPI.favorite(user, status1.id) + CommonAPI.favorite(user, status2.id) + Bookmark.create(user.id, status2.id) + Bookmark.create(user.id, status3.id) + + assert {:ok, backup} = user |> Backup.new() |> Repo.insert() + assert {:ok, path} = Backup.export(backup) + + [path: path, backup: backup] + end + + test "S3", %{path: path, backup: backup} do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.S3) + + with_mock ExAws, + request: fn + %{http_method: :put} -> {:ok, :ok} + %{http_method: :delete} -> {:ok, %{status_code: 204}} + end do + assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) + assert {:ok, _backup} = Backup.delete(backup) + end + + with_mock ExAws, request: fn %{http_method: :delete} -> {:ok, %{status_code: 204}} end do + end + end + + test "Local", %{path: path, backup: backup} do + Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local) + + assert {:ok, %Pleroma.Upload{}} = Backup.upload(backup, path) + assert {:ok, _backup} = Backup.delete(backup) + end + end +end diff --git a/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs b/test/pleroma/web/admin_api/controllers/admin_api_controller_test.exs @@ -2038,7 +2038,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> post("/api/pleroma/admin/backups", %{nickname: user.nickname}) |> json_response(200) - assert [backup] = Repo.all(Pleroma.Backup) + assert [backup] = Repo.all(Pleroma.User.Backup) ObanHelpers.perform_all() @@ -2079,7 +2079,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> post("/api/pleroma/admin/backups", %{nickname: user.nickname}) |> json_response(200) - assert [_backup] = Repo.all(Pleroma.Backup) + assert [_backup] = Repo.all(Pleroma.User.Backup) assert "" == conn @@ -2088,7 +2088,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do |> post("/api/pleroma/admin/backups", %{nickname: user.nickname}) |> json_response(200) - assert Repo.aggregate(Pleroma.Backup, :count) == 2 + assert Repo.aggregate(Pleroma.User.Backup, :count) == 2 end end end diff --git a/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/backup_controller_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.BackupControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Backup + alias Pleroma.User.Backup alias Pleroma.Web.PleromaAPI.BackupView setup do