Skip to content

Commit

Permalink
Make automatic deletion of unclaimed recordings a 2-step process
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed Dec 18, 2023
1 parent dd27968 commit b705b51
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 64 deletions.
16 changes: 14 additions & 2 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,20 @@ if config_env() in [:prod, :dev] do
config :asciinema, Asciinema.PngGenerator.Rsvg, font_family: rsvg_font_family
end

if gc_days = env.("ASCIICAST_GC_DAYS") do
config :asciinema, :asciicast_gc_days, String.to_integer(gc_days)
if ttls = env.("UNCLAIMED_RECORDING_TTL") do
ttls =
case String.split(ttls, ",", parts: 2) do
[hide_ttl] ->
[hide: String.to_integer(hide_ttl)]

[delete_ttl, delete_ttl] ->
[delete: String.to_integer(delete_ttl)]

[hide_ttl, delete_ttl] ->
[hide: String.to_integer(hide_ttl), delete: String.to_integer(delete_ttl)]
end

config :asciinema, :unclaimed_recording_ttl, ttls
end

if String.downcase("#{env.("CRON")}") in ["0", "false", "no"] do
Expand Down
19 changes: 15 additions & 4 deletions lib/asciinema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,23 @@ defmodule Asciinema do

defdelegate get_live_stream(id_or_owner), to: Streaming

def recording_gc_days do
Application.get_env(:asciinema, :asciicast_gc_days)
def unclaimed_recording_ttl(mode \\ nil)

def unclaimed_recording_ttl(nil) do
unclaimed_recording_ttl(:hide) || unclaimed_recording_ttl(:delete)
end

def unclaimed_recording_ttl(mode) do
Keyword.get(Application.get_env(:asciinema, :unclaimed_recording_ttl, []), mode)
end

def hide_unclaimed_recordings(days) do
t = Timex.shift(Timex.now(), days: -days)
Recordings.hide_unclaimed_asciicasts(Accounts.temporary_users(), t)
end

def archive_unclaimed_recordings(days) do
def delete_unclaimed_recordings(days) do
t = Timex.shift(Timex.now(), days: -days)
Recordings.archive_asciicasts(Accounts.temporary_users(), t)
Recordings.delete_unclaimed_asciicasts(Accounts.temporary_users(), t)
end
end
29 changes: 21 additions & 8 deletions lib/asciinema/gc.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,29 @@ defmodule Asciinema.GC do

@impl Oban.Worker
def perform(_job) do
if days = Asciinema.recording_gc_days() do
count = Asciinema.archive_unclaimed_recordings(days)
hide_unclaimed_recordings(Asciinema.unclaimed_recording_ttl(:hide))
delete_unclaimed_recordings(Asciinema.unclaimed_recording_ttl(:delete))

if count > 0 do
Logger.info("archived #{count} recordings")
end
:ok
end

defp hide_unclaimed_recordings(nil), do: :ok

defp hide_unclaimed_recordings(days) do
count = Asciinema.hide_unclaimed_recordings(days)

if count > 0 do
Logger.info("hid #{count} unclaimed recordings")
end
end

defp delete_unclaimed_recordings(nil), do: :ok

defp delete_unclaimed_recordings(days) do
count = Asciinema.delete_unclaimed_recordings(days)

:ok
else
:discard
if count > 0 do
Logger.info("deleted #{count} unclaimed recordings")
end
end
end
24 changes: 20 additions & 4 deletions lib/asciinema/recordings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,17 @@ defmodule Asciinema.Recordings do
end

def delete_asciicasts(%{asciicasts: _} = owner) do
for a <- Repo.all(Ecto.assoc(owner, :asciicasts)) do
delete_asciicasts(Ecto.assoc(owner, :asciicasts))
end

def delete_asciicasts(%Ecto.Query{} = query) do
asciicasts = Repo.all(query)

for a <- asciicasts do
{:ok, _} = delete_asciicast(a)
end

:ok
length(asciicasts)
end

def update_snapshot(%Asciicast{} = asciicast) do
Expand Down Expand Up @@ -525,10 +531,10 @@ defmodule Asciinema.Recordings do
tmp_path
end

def archive_asciicasts(users_query, t) do
def hide_unclaimed_asciicasts(tmp_users_query, t) do
query =
from a in Asciicast,
join: u in ^users_query,
join: u in ^tmp_users_query,
on: a.user_id == u.id,
where: a.archivable and is_nil(a.archived_at) and a.inserted_at < ^t

Expand All @@ -537,6 +543,16 @@ defmodule Asciinema.Recordings do
count
end

def delete_unclaimed_asciicasts(tmp_users_query, t) do
query =
from a in Asciicast,
join: u in ^tmp_users_query,
on: a.user_id == u.id,
where: a.archivable and a.inserted_at < ^t

delete_asciicasts(query)
end

def reassign_asciicasts(src_user_id, dst_user_id) do
q = from(a in Asciicast, where: a.user_id == ^src_user_id)
Repo.update_all(q, set: [user_id: dst_user_id, updated_at: Timex.now()])
Expand Down
26 changes: 4 additions & 22 deletions lib/asciinema_web/controllers/recording_controller.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defmodule AsciinemaWeb.RecordingController do
use AsciinemaWeb, :controller
alias Asciinema.{Recordings, PngGenerator, Accounts}
alias Asciinema.{Recordings, PngGenerator}
alias Asciinema.Recordings.Asciicast
alias AsciinemaWeb.PlayerOpts

Expand Down Expand Up @@ -52,11 +52,10 @@ defmodule AsciinemaWeb.RecordingController do
if asciicast.archived_at do
conn
|> put_status(410)
|> render("archived.html")
|> render("deleted.html", ttl: Asciinema.unclaimed_recording_ttl())
else
conn
|> count_view(asciicast)
|> put_archival_info_flash(asciicast)
|> render(
"show.html",
page_title: AsciinemaWeb.RecordingView.title(asciicast),
Expand Down Expand Up @@ -95,7 +94,7 @@ defmodule AsciinemaWeb.RecordingController do
if asciicast.archived_at do
conn
|> put_status(410)
|> text("This recording has been archived\n")
|> text("This recording has been deleted\n")
else
send_download(conn, {:file, Recordings.text_file_path(asciicast)},
filename: "#{asciicast.id}.txt"
Expand Down Expand Up @@ -201,7 +200,7 @@ defmodule AsciinemaWeb.RecordingController do
if conn.assigns.asciicast.archived_at do
conn
|> put_status(410)
|> render("archived.html")
|> render("deleted.html", ttl: Asciinema.unclaimed_recording_ttl())
else
render(conn, "iframe.html", player_opts: player_opts(params))
end
Expand Down Expand Up @@ -285,23 +284,6 @@ defmodule AsciinemaWeb.RecordingController do
end
end

defp put_archival_info_flash(conn, asciicast) do
with true <- asciicast.archivable,
days when not is_nil(days) <- Asciinema.recording_gc_days(),
%{} = user <- asciicast.user,
true <- Accounts.temporary_user?(user),
true <- Timex.before?(asciicast.inserted_at, Timex.shift(Timex.now(), days: -days)) do
put_flash(
conn,
:error,
{:safe,
"This recording will be archived soon. More details: <a href=\"https://blog.asciinema.org/post/archival/\">blog.asciinema.org/post/archival/</a>"}
)
else
_ -> conn
end
end

defp player_opts(params) do
params
|> Ext.Map.rename(%{"t" => "startAt", "i" => "idleTimeLimit"})
Expand Down
14 changes: 0 additions & 14 deletions lib/asciinema_web/templates/recording/archived.html.eex

This file was deleted.

17 changes: 17 additions & 0 deletions lib/asciinema_web/templates/recording/deleted.html.eex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>This recording has been deleted</h2>

<%= if @ttl do %>
<p>
All unclaimed recordings are automatically deleted <%= @ttl %> days after upload.
</p>

<p>
See <a href="https://docs.asciinema.org/manual/cli/usage/#asciinema-auth">here</a> for more details.
</p>
<% end %>
</div>
</div>
</div>
15 changes: 8 additions & 7 deletions lib/asciinema_web/views/api/recording_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,20 @@ defmodule AsciinemaWeb.Api.RecordingView do
"""

is_tmp_user = Asciinema.Accounts.temporary_user?(conn.assigns.current_user)
gc_days = Asciinema.recording_gc_days()
ttl = Asciinema.unclaimed_recording_ttl()

if is_tmp_user && gc_days do
if is_tmp_user && ttl do
hostname = AsciinemaWeb.instance_hostname()

"""
#{message}
This installation of asciinema recorder hasn't been linked to any #{hostname}
account. All unclaimed recordings (from unknown installations like this one)
are automatically archived #{gc_days} days after upload.
This asciinema CLI hasn't been linked to any #{hostname} account.
If you want to preserve all recordings made on this machine, connect this
installation with #{hostname} account by opening the following link:
Recordings uploaded from unrecognized systems, such as this one, are automatically
deleted #{ttl} days after upload.
If you want to preserve all recordings uploaded from this machine,
authorize this CLI with your #{hostname} account by opening the following link:
#{Routes.connect_url(conn, :show, install_id)}
"""
Expand Down
2 changes: 0 additions & 2 deletions lib/asciinema_web/views/recording_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -525,8 +525,6 @@ defmodule AsciinemaWeb.RecordingView do
"#{Decimal.round(Decimal.from_float(float), 3)}%"
end

def recording_gc_days, do: Asciinema.recording_gc_days()

defp cols(asciicast), do: asciicast.cols_override || asciicast.cols

defp rows(asciicast), do: asciicast.rows_override || asciicast.rows
Expand Down
62 changes: 61 additions & 1 deletion test/asciinema_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule AsciinemaTest do
import Asciinema.Factory
use Asciinema.DataCase
use Oban.Testing, repo: Asciinema.Repo
alias Asciinema.Accounts
alias Asciinema.{Accounts, Recordings}

describe "create_user/1" do
test "succeeds when email not taken" do
Expand Down Expand Up @@ -104,4 +104,64 @@ defmodule AsciinemaTest do
assert :ok = Asciinema.delete_user!(user)
end
end

describe "hide_unclaimed_recordings/1" do
test "sets archived_at on matching asciicasts" do
tmp_user = insert(:temporary_user, email: nil)

asciicast_1 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -2)
)

asciicast_2 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -4)
)

asciicast_3 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -10),
archivable: false
)

assert Asciinema.hide_unclaimed_recordings(3) == 1
assert Recordings.get_asciicast(asciicast_1.id).archived_at == nil
assert Recordings.get_asciicast(asciicast_2.id).archived_at != nil
assert Recordings.get_asciicast(asciicast_3.id).archived_at == nil
end
end

describe "delete_unclaimed_recordings/1" do
test "deletes matching asciicasts" do
tmp_user = insert(:temporary_user, email: nil)

asciicast_1 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -2)
)

asciicast_2 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -4)
)

asciicast_3 =
insert(:asciicast,
user: tmp_user,
inserted_at: Timex.shift(Timex.now(), days: -10),
archivable: false
)

assert Asciinema.delete_unclaimed_recordings(3) == 1
assert Recordings.get_asciicast(asciicast_1.id) != nil
assert Recordings.get_asciicast(asciicast_2.id) == nil
assert Recordings.get_asciicast(asciicast_3.id) != nil
end
end
end
4 changes: 4 additions & 0 deletions test/support/factory.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ defmodule Asciinema.Factory do
}
end

def temporary_user_factory do
%{user_factory() | email: nil}
end

def api_token_factory do
%ApiToken{
user: build(:user),
Expand Down

0 comments on commit b705b51

Please sign in to comment.