pleroma/lib/pleroma/backup.ex

245 lines
6.6 KiB
Elixir
Raw Normal View History

2020-08-19 13:31:33 +02:00
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
2020-09-02 18:21:33 +02:00
defmodule Pleroma.Backup do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
2020-09-10 18:53:06 +02:00
require Pleroma.Constants
2020-08-19 13:31:33 +02:00
alias Pleroma.Activity
alias Pleroma.Bookmark
2020-09-02 18:21:33 +02:00
alias Pleroma.Repo
2020-08-19 13:31:33 +02:00
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
2020-09-04 19:48:52 +02:00
alias Pleroma.Workers.BackupWorker
2020-08-19 13:31:33 +02:00
2020-09-02 18:21:33 +02:00
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
2020-09-18 20:18:34 +02:00
def create(user, admin_user_id \\ nil) do
2020-09-15 16:07:28 +02:00
with :ok <- validate_email_enabled(),
:ok <- validate_user_email(user),
:ok <- validate_limit(user),
2020-09-02 18:21:33 +02:00
{:ok, backup} <- user |> new() |> Repo.insert() do
2020-09-18 20:18:34 +02:00
BackupWorker.process(backup, admin_user_id)
2020-09-02 18:21:33 +02:00
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
2020-09-04 19:48:52 +02:00
def delete(backup) do
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
with :ok <- uploader.delete_file("backups/" <> backup.file_name) do
Repo.delete(backup)
end
end
2020-09-02 18:21:33 +02:00
defp validate_limit(user) do
case get_last(user.id) do
%__MODULE__{inserted_at: inserted_at} ->
2020-09-04 16:30:39 +02:00
days = Pleroma.Config.get([Pleroma.Backup, :limit_days])
2020-09-02 18:21:33 +02:00
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
if diff > days do
:ok
else
{:error, "Last export was less than #{days} days ago"}
end
nil ->
:ok
end
end
2020-09-15 16:07:28 +02:00
defp validate_email_enabled do
if Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled]) do
:ok
else
{:error, "Backups require enabled email"}
end
end
defp validate_user_email(%User{email: nil}), do: {:error, "Email is required"}
defp validate_user_email(%User{email: email}) when is_binary(email), do: :ok
2020-09-02 18:21:33 +02:00
def get_last(user_id) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> limit(1)
|> Repo.one()
end
2020-09-08 23:04:00 +02:00
def list(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> Repo.all()
end
2020-09-02 19:45:22 +02:00
def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> where([b], b.id != ^latest_id)
2020-09-04 19:48:52 +02:00
|> Repo.all()
|> Enum.each(&BackupWorker.delete/1)
2020-09-02 19:45:22 +02:00
end
def get(id), do: Repo.get(__MODULE__, id)
2020-09-02 18:21:33 +02:00
def process(%__MODULE__{} = backup) do
2020-09-15 16:07:28 +02:00
with {:ok, zip_file} <- export(backup),
2020-09-02 18:21:33 +02:00
{: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
2020-08-19 13:31:33 +02:00
2020-08-24 18:59:57 +02:00
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
2020-09-15 16:07:28 +02:00
def export(%__MODULE__{} = backup) do
2020-09-02 18:21:33 +02:00
backup = Repo.preload(backup, :user)
name = String.trim_trailing(backup.file_name, ".zip")
2020-09-16 21:21:13 +02:00
dir = dir(name)
2020-08-24 18:59:57 +02:00
2020-09-02 18:21:33 +02:00
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
2020-08-31 18:31:21 +02:00
{:ok, :binary.list_to_bin(zip_path)}
end
end
2020-09-16 21:21:13 +02:00
def dir(name) do
dir = Pleroma.Config.get([__MODULE__, :dir]) || System.tmp_dir!()
Path.join(dir, name)
end
2020-09-02 18:21:33 +02:00
def upload(%__MODULE__{} = backup, zip_path) do
2020-08-31 18:31:21 +02:00
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
upload = %Pleroma.Upload{
2020-09-02 18:21:33 +02:00
name: backup.file_name,
2020-08-31 18:31:21 +02:00
tempfile: zip_path,
2020-09-02 18:21:33 +02:00
content_type: backup.content_type,
path: "backups/" <> backup.file_name
2020-08-31 18:31:21 +02:00
}
2020-08-31 21:07:14 +02:00
with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
:ok <- File.rm(zip_path) do
2020-08-31 18:31:21 +02:00
{:ok, upload}
2020-08-19 13:31:33 +02:00
end
end
2020-08-25 16:51:09 +02:00
defp actor(dir, user) do
2020-08-19 13:31:33 +02:00
with {:ok, json} <-
UserView.render("user.json", %{user: user})
|> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
|> Jason.encode() do
File.write(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": [
2020-09-10 18:53:06 +02:00
2020-08-19 13:31:33 +02:00
"""
)
end
defp write(query, dir, name, fun) do
path = 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, str} <- fun.(i),
:ok <- IO.write(file, str <> ",\n") do
acc + 1
else
_ -> acc
end
end)
2020-08-19 13:31:33 +02:00
with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
File.close(file)
end
end
end
2020-08-25 16:51:09 +02:00
defp bookmarks(dir, %{id: user_id} = _user) do
2020-08-19 13:31:33 +02:00
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
2020-08-25 16:51:09 +02:00
defp likes(dir, user) do
2020-08-19 13:31:33 +02:00
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
2020-08-25 16:51:09 +02:00
defp statuses(dir, user) do
2020-08-19 13:31:33 +02:00
opts =
%{}
|> Map.put(:type, ["Create", "Announce"])
2020-09-10 18:53:06 +02:00
|> Map.put(:actor_id, user.ap_id)
2020-08-19 13:31:33 +02:00
2020-09-10 18:53:06 +02:00
[
[Pleroma.Constants.as_public(), user.ap_id],
User.following(user),
Pleroma.List.memberships(user)
]
2020-08-19 13:31:33 +02:00
|> Enum.concat()
|> ActivityPub.fetch_activities_query(opts)
|> write(dir, "outbox", fn a ->
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
activity |> Map.delete("@context") |> Jason.encode()
end
end)
end
end