1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Backup do
11 alias Pleroma.Activity
12 alias Pleroma.Bookmark
15 alias Pleroma.Web.ActivityPub.ActivityPub
16 alias Pleroma.Web.ActivityPub.Transmogrifier
17 alias Pleroma.Web.ActivityPub.UserView
20 field(:content_type, :string)
21 field(:file_name, :string)
22 field(:file_size, :integer, default: 0)
23 field(:processed, :boolean, default: false)
25 belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
31 with :ok <- validate_limit(user),
32 {:ok, backup} <- user |> new() |> Repo.insert() do
33 Pleroma.Workers.BackupWorker.enqueue("process", %{"backup_id" => backup.id})
38 rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
39 datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
40 name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
44 content_type: "application/zip",
49 defp validate_limit(user) do
50 case get_last(user.id) do
51 %__MODULE__{inserted_at: inserted_at} ->
52 days = Pleroma.Config.get([Pleroma.Backup, :limit_days])
53 diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
58 {:error, "Last export was less than #{days} days ago"}
66 def get_last(user_id) do
68 |> where(user_id: ^user_id)
69 |> order_by(desc: :id)
74 def remove_outdated(%__MODULE__{id: latest_id, user_id: user_id}) do
76 |> where(user_id: ^user_id)
77 |> where([b], b.id != ^latest_id)
81 def get(id), do: Repo.get(__MODULE__, id)
83 def process(%__MODULE__{} = backup) do
84 with {:ok, zip_file} <- zip(backup),
85 {:ok, %{size: size}} <- File.stat(zip_file),
86 {:ok, _upload} <- upload(backup, zip_file) do
88 |> cast(%{file_size: size, processed: true}, [:file_size, :processed])
93 @files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
94 def zip(%__MODULE__{} = backup) do
95 backup = Repo.preload(backup, :user)
96 name = String.trim_trailing(backup.file_name, ".zip")
97 dir = Path.join(System.tmp_dir!(), name)
99 with :ok <- File.mkdir(dir),
100 :ok <- actor(dir, backup.user),
101 :ok <- statuses(dir, backup.user),
102 :ok <- likes(dir, backup.user),
103 :ok <- bookmarks(dir, backup.user),
104 {:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
105 {:ok, _} <- File.rm_rf(dir) do
106 {:ok, :binary.list_to_bin(zip_path)}
110 def upload(%__MODULE__{} = backup, zip_path) do
111 uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
113 upload = %Pleroma.Upload{
114 name: backup.file_name,
116 content_type: backup.content_type,
117 path: "backups/" <> backup.file_name
120 with {:ok, _} <- Pleroma.Uploaders.Uploader.put_file(uploader, upload),
121 :ok <- File.rm(zip_path) do
126 defp actor(dir, user) do
128 UserView.render("user.json", %{user: user})
129 |> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
131 File.write(dir <> "/actor.json", json)
135 defp write_header(file, name) do
140 "@context": "https://www.w3.org/ns/activitystreams",
141 "id": "#{name}.json",
142 "type": "OrderedCollection",
148 defp write(query, dir, name, fun) do
149 path = dir <> "/#{name}.json"
151 with {:ok, file} <- File.open(path, [:write, :utf8]),
152 :ok <- write_header(file, name) do
153 counter = :counters.new(1, [])
156 |> Pleroma.RepoStreamer.chunk_stream(100)
157 |> Stream.each(fn items ->
158 Enum.each(items, fn i ->
159 with {:ok, str} <- fun.(i),
160 :ok <- IO.write(file, str <> ",\n") do
161 :counters.add(counter, 1, 1)
167 total = :counters.get(counter, 1)
169 with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
175 defp bookmarks(dir, %{id: user_id} = _user) do
177 |> where(user_id: ^user_id)
178 |> join(:inner, [b], activity in assoc(b, :activity))
179 |> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
180 |> write(dir, "bookmarks", fn a -> {:ok, "\"#{a.object}\""} end)
183 defp likes(dir, user) do
185 |> Activity.Queries.by_actor()
186 |> Activity.Queries.by_type("Like")
187 |> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
188 |> write(dir, "likes", fn a -> {:ok, "\"#{a.object}\""} end)
191 defp statuses(dir, user) do
194 |> Map.put(:type, ["Create", "Announce"])
195 |> Map.put(:blocking_user, user)
196 |> Map.put(:muting_user, user)
197 |> Map.put(:reply_filtering_user, user)
198 |> Map.put(:announce_filtering_user, user)
199 |> Map.put(:user, user)
201 [[user.ap_id], User.following(user), Pleroma.List.memberships(user)]
203 |> ActivityPub.fetch_activities_query(opts)
204 |> write(dir, "outbox", fn a ->
205 with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
206 activity |> Map.delete("@context") |> Jason.encode()