Skip to content

Commit

Permalink
Add .txt endpoint - /a/<id>.txt
Browse files Browse the repository at this point in the history
  • Loading branch information
ku1ik committed Oct 17, 2023
1 parent f817cd4 commit 5e95167
Show file tree
Hide file tree
Showing 17 changed files with 122 additions and 20 deletions.
8 changes: 8 additions & 0 deletions .env.production.sample
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,11 @@ MAILNAME=localhost
# AWS_SECRET_ACCESS_KEY=
# S3_BUCKET=my-asciinema-bucket
# S3_REGION=us-east-1

### File cache

## Location of local cache directory used for storing .txt versions of the
## recordings, local copy of .cast files when S3 file store is used above,
## and other cached items.
#Default: /var/cache/asciinema
#FILE_CACHE_PATH=
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,5 @@ npm-debug.log
/config/custom.exs
/priv/native
/uploads/*
/cache
/volumes
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ config :sentry,
config :asciinema, :file_store, Asciinema.FileStore.Local
config :asciinema, Asciinema.FileStore.Local, path: "uploads/"

config :asciinema, Asciinema.FileCache, path: "cache/"

config :asciinema, :png_generator, Asciinema.PngGenerator.Rsvg

config :asciinema, Asciinema.PngGenerator.Rsvg,
Expand Down
2 changes: 2 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ config :asciinema, Asciinema.Emails.Mailer,
adapter: Bamboo.SMTPAdapter,
server: "smtp",
port: 25

config :asciinema, Asciinema.FileCache, path: "/var/cache/asciinema"
9 changes: 8 additions & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ if config_env() in [:prod, :dev] do

config :ex_aws, region: {:system, "AWS_REGION"}

file_cache_path = env.("FILE_CACHE_PATH")

if file_cache_path do
config :asciinema, Asciinema.FileCache, path: file_cache_path
end

if env.("S3_BUCKET") do
config :asciinema, :file_store, Asciinema.FileStore.Cached

Expand All @@ -75,7 +81,8 @@ if config_env() in [:prod, :dev] do
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role]

config :asciinema, Asciinema.FileStore.Local, path: "cache/uploads/"
config :asciinema, Asciinema.FileStore.Local,
path: Path.join(file_cache_path || "/var/cache/asciinema", "uploads")
end

if db_pool_size = env.("DB_POOL_SIZE") do
Expand Down
2 changes: 2 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ config :asciinema, Asciinema.Accounts,

config :asciinema, Asciinema.FileStore.Local, path: "uploads/test/"

config :asciinema, Asciinema.FileCache, path: "/tmp/asciinema/"

config :asciinema, :snapshot_updater, Asciinema.Recordings.SnapshotUpdater.Noop

config :asciinema, Oban, testing: :manual
Expand Down
24 changes: 24 additions & 0 deletions lib/asciinema/file_cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Asciinema.FileCache do
def full_path(namespace, path, generator) do
path = full_path(namespace, path)

case File.stat(path) do
{:ok, _} ->
path

{:error, :enoent} ->
content = generator.()
parent_dir = Path.dirname(path)
:ok = File.mkdir_p(parent_dir)
File.write!(path, content)

path
end
end

defp full_path(namespace, path), do: Path.join([base_path(), to_string(namespace), path])

defp base_path do
Keyword.get(Application.get_env(:asciinema, __MODULE__), :path)
end
end
22 changes: 12 additions & 10 deletions lib/asciinema/file_store/local.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Asciinema.FileStore.Local do

@impl true
def put_file(dst_path, src_local_path, _content_type) do
full_dst_path = base_path() <> dst_path
full_dst_path = full_path(dst_path)
parent_dir = Path.dirname(full_dst_path)

with :ok <- File.mkdir_p(parent_dir),
Expand All @@ -20,8 +20,8 @@ defmodule Asciinema.FileStore.Local do

@impl true
def move_file(from_path, to_path) do
full_from_path = base_path() <> from_path
full_to_path = base_path() <> to_path
full_from_path = full_path(from_path)
full_to_path = full_path(to_path)
parent_dir = Path.dirname(full_to_path)
:ok = File.mkdir_p(parent_dir)
File.rename(full_from_path, full_to_path)
Expand All @@ -41,13 +41,13 @@ defmodule Asciinema.FileStore.Local do
defp do_serve_file(conn, path) do
conn
|> put_resp_header("content-type", MIME.from_path(path))
|> send_file(200, base_path() <> path)
|> send_file(200, full_path(path))
|> halt
end

@impl true
def open_file(path) do
File.open(base_path() <> path, [:binary, :read])
File.open(full_path(path), [:binary, :read])
end

@impl true
Expand All @@ -56,19 +56,21 @@ defmodule Asciinema.FileStore.Local do
end

def open_file(path, function) do
File.open(base_path() <> path, [:binary, :read], function)
File.open(full_path(path), [:binary, :read], function)
end

@impl true
def delete_file(path) do
File.rm(base_path() <> path)
File.rm(full_path(path))
end

defp config do
Application.get_env(:asciinema, __MODULE__)
end
defp full_path(path), do: Path.join(base_path(), path)

defp base_path do
Keyword.get(config(), :path)
end

defp config do
Application.get_env(:asciinema, __MODULE__)
end
end
5 changes: 4 additions & 1 deletion lib/asciinema/recordings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Asciinema.Recordings do
require Logger
import Ecto.Query, warn: false
alias Asciinema.{FileStore, Media, Repo, Vt}
alias Asciinema.Recordings.{Asciicast, Output, Paths, SnapshotUpdater}
alias Asciinema.Recordings.{Asciicast, Output, Paths, SnapshotUpdater, Text}
alias Ecto.Changeset

def fetch_asciicast(id) do
Expand Down Expand Up @@ -413,6 +413,9 @@ defmodule Asciinema.Recordings do
end)
end

defdelegate text(asciicast), to: Text
defdelegate text_file_path(asciicast), to: Text

defp frame_before_or_at?({time, _}, secs) do
time <= secs
end
Expand Down
6 changes: 3 additions & 3 deletions lib/asciinema/recordings/paths.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ defmodule Asciinema.Recordings.Paths do
def sharded_path(asciicast, ext \\ nil) do
ext =
case {ext, asciicast.version} do
{nil, 1} -> "json"
{nil, 2} -> "cast"
{nil, 1} -> ".json"
{nil, 2} -> ".cast"
{ext, _} when is_binary(ext) -> ext
end

Expand All @@ -14,6 +14,6 @@ defmodule Asciinema.Recordings.Paths do
|> String.reverse()
|> String.slice(0, 4)

"asciicasts/#{a}/#{b}/#{asciicast.id}.#{ext}"
"asciicasts/#{a}/#{b}/#{asciicast.id}#{ext}"
end
end
22 changes: 22 additions & 0 deletions lib/asciinema/recordings/text.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Asciinema.Recordings.Text do
alias Asciinema.Recordings.{Asciicast, Output, Paths}
alias Asciinema.{FileCache, Vt}

def text(%Asciicast{cols: cols, rows: rows} = asciicast) do
output = Output.stream(asciicast)

Vt.with_vt(cols, rows, [resizable: true, scrollback_limit: nil], fn vt ->
Enum.each(output, fn {_, text} -> Vt.feed(vt, text) end)

Vt.text(vt)
end)
end

def text_file_path(asciicast) do
FileCache.full_path(
:txt,
Paths.sharded_path(asciicast, ".txt"),
fn -> text(asciicast) end
)
end
end
2 changes: 1 addition & 1 deletion lib/asciinema/vt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ defmodule Asciinema.Vt do
@spec dump_screen(reference) :: {:ok, {list(list({binary, map})), {integer, integer} | nil}}
def dump_screen(_vt), do: :erlang.nif_error(:nif_not_loaded)

@spec text(reference) :: list(binary)
@spec text(reference) :: binary
def text(_vt), do: :erlang.nif_error(:nif_not_loaded)
end
12 changes: 12 additions & 0 deletions lib/asciinema_web/controllers/recording_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ defmodule AsciinemaWeb.RecordingController do
|> send_file(200, path)
end

def do_show(conn, "txt", asciicast) do
if asciicast.archived_at do
conn
|> put_status(410)
|> text("This recording has been archived\n")
else
send_download(conn, {:file, Recordings.text_file_path(asciicast)},
filename: "#{asciicast.id}.txt"
)
end
end

def do_show(conn, "svg", asciicast) do
if asciicast.archived_at do
path = Application.app_dir(:asciinema, "priv/static/images/archived.png")
Expand Down
2 changes: 1 addition & 1 deletion lib/asciinema_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule AsciinemaWeb.Router do

pipeline :asciicast do
plug AsciinemaWeb.TrailingFormat
plug :accepts, ["html", "js", "json", "cast", "svg", "png", "gif"]
plug :accepts, ["html", "js", "json", "cast", "txt", "svg", "png", "gif"]
plug :format_specific_plugs
plug :put_secure_browser_headers
end
Expand Down
2 changes: 1 addition & 1 deletion lib/asciinema_web/trailing_format.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule AsciinemaWeb.TrailingFormat do
@known_exts ["js", "json", "cast", "svg", "png", "gif", "xml"]
@known_exts ["js", "json", "cast", "txt", "svg", "png", "gif", "xml"]

def init(opts), do: opts

Expand Down
8 changes: 6 additions & 2 deletions native/vt_nif/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,19 @@ fn dump_screen(env: Env, resource: ResourceArc<VtResource>) -> NifResult<(Atom,
}

#[rustler::nif]
fn text(resource: ResourceArc<VtResource>) -> NifResult<Vec<String>> {
fn text(resource: ResourceArc<VtResource>) -> NifResult<String> {
let vt = convert_err(resource.vt.read(), "rw_lock")?;
let mut text = vt.text();

while !text.is_empty() && text[text.len() - 1].is_empty() {
text.truncate(text.len() - 1);
}

Ok(text)
for line in &mut text.iter_mut() {
line.push('\n');
}

Ok(text.join(""))
}

fn segment_to_term(segment: avt::Segment, env: Env) -> Term {
Expand Down
13 changes: 13 additions & 0 deletions test/controllers/recording_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ defmodule Asciinema.RecordingControllerTest do
assert response(conn, 200)
end

test "TXT", %{conn: conn} do
asciicast = insert(:asciicast) |> with_file()
url = Routes.recording_path(conn, :show, asciicast)

conn_2 = get(conn, url <> ".txt")
assert response(conn_2, 200)
assert response_content_type(conn_2, :txt)

conn_2 = conn |> put_req_header("accept", "text/plain") |> get(url)
assert response(conn_2, 200)
assert response_content_type(conn_2, :txt)
end

@tag :rsvg
test "PNG", %{conn: conn} do
asciicast = insert(:asciicast)
Expand Down

0 comments on commit 5e95167

Please sign in to comment.