Basic PNG generation in Elixir

ex-png
Marcin Kulik 7 years ago
parent 135720df52
commit e0511053a4

@ -44,6 +44,11 @@ else
config :asciinema, Asciinema.FileStore.Local, path: "uploads/"
end
config :asciinema, :png_generator, Asciinema.PngGenerator.A2png
config :asciinema, Asciinema.PngGenerator.A2png, bin_path: "./a2png/a2png.sh"
config :porcelain, goon_warn_if_missing: false
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"

@ -14,6 +14,7 @@ defmodule Asciinema do
supervisor(Asciinema.Endpoint, []),
# Start your own worker by calling: Asciinema.Worker.start_link(arg1, arg2, arg3)
# worker(Asciinema.Worker, [arg1, arg2, arg3]),
:poolboy.child_spec(:worker, poolboy_config(), []),
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
@ -28,4 +29,11 @@ defmodule Asciinema do
Asciinema.Endpoint.config_change(changed, removed)
:ok
end
defp poolboy_config do
[{:name, {:local, :worker}},
{:worker_module, Asciinema.PngGenerator.A2png},
{:size, 2},
{:max_overflow, 0}]
end
end

@ -1,4 +1,7 @@
defmodule Asciinema.FileStore do
@doc "Serves file at given path in store"
@callback serve_file(conn :: %Plug.Conn{}, path :: String.t, filename :: String.t) :: %Plug.Conn{}
@doc "Opens the given path in store"
@callback open(path :: String.t) :: {:ok, File.io_device} | {:error, File.posix}
end

@ -19,6 +19,10 @@ defmodule Asciinema.FileStore.Local do
|> halt
end
def open(path) do
File.open(base_path() <> path, [:binary, :read])
end
defp config do
Application.get_env(:asciinema, Asciinema.FileStore.Local)
end

@ -1,6 +1,7 @@
defmodule Asciinema.FileStore.S3 do
@behaviour Asciinema.FileStore
import Phoenix.Controller, only: [redirect: 2]
alias ExAws.S3
def serve_file(conn, path, nil) do
do_serve_file(conn, path)
@ -18,6 +19,17 @@ defmodule Asciinema.FileStore.S3 do
|> redirect(external: url)
end
def open(path) do
response = S3.get_object(bucket(), base_path() <> path) |> ExAws.request(region: region())
case response do
{:ok, %{body: body}} ->
File.open(body, [:ram, :binary, :read])
{:error, reason} ->
{:error, reason}
end
end
defp config do
Application.get_env(:asciinema, Asciinema.FileStore.S3)
end

@ -0,0 +1,10 @@
defmodule Asciinema.PngGenerator do
alias Asciinema.Asciicast
@doc "Generates PNG image from asciicast and returns path to it"
@callback generate(asciicast :: %Asciicast{}) :: {:ok, String.t} | {:error, term}
def generate(asciicast) do
Application.get_env(:asciinema, :png_generator).generate(asciicast)
end
end

@ -0,0 +1,55 @@
defmodule Asciinema.PngGenerator.A2png do
@behaviour Asciinema.PngGenerator
use GenServer
alias Asciinema.Asciicast
@result_timeout 30000
@acquire_timeout 5000
def generate(%Asciicast{} = asciicast) do
{:ok, tmp_dir_path} = Briefly.create(directory: true)
:poolboy.transaction(
:worker,
&GenServer.call(&1, {:gen_png, asciicast, tmp_dir_path}, @result_timeout), @acquire_timeout
)
end
# GenServer API
def start_link(_) do
GenServer.start_link(__MODULE__, nil, [])
end
def init(_) do
{:ok, nil}
end
def handle_call({:gen_png, asciicast, tmp_dir_path}, _from, state) do
{:reply, do_gen(asciicast, tmp_dir_path), state}
end
defp do_gen(asciicast, tmp_dir_path) do
path = Asciicast.json_store_path(asciicast)
json_path = Path.join(tmp_dir_path, "tmp.json")
png_path = Path.join(tmp_dir_path, "tmp.png")
snapshot_at = "#{asciicast.duration / 2}"
with {:ok, file} <- file_store().open(path),
{:ok, _} <- :file.copy(file, json_path),
%{status: 0} <- Porcelain.exec(bin_path(), [json_path, png_path, snapshot_at]) do
{:ok, png_path}
else
otherwise ->
otherwise
end
end
def bin_path do
Keyword.get(Application.get_env(:asciinema, __MODULE__), :bin_path)
end
defp file_store do
Application.get_env(:asciinema, :file_store)
end
end

@ -19,6 +19,7 @@ defmodule Asciinema.Mixfile do
def application do
[mod: {Asciinema, []},
applications: [
:briefly,
:bugsnag,
:cowboy,
:ex_aws,
@ -29,6 +30,8 @@ defmodule Asciinema.Mixfile do
:phoenix_html,
:phoenix_pubsub,
:plug_rails_cookie_session_store,
:poolboy,
:porcelain,
:postgrex,
]]
end
@ -42,6 +45,7 @@ defmodule Asciinema.Mixfile do
# Type `mix help deps` for examples and options.
defp deps do
[
{:briefly, "~> 0.3"},
{:cowboy, "~> 1.0"},
{:ex_aws, "~> 1.0"},
{:gettext, "~> 0.11"},
@ -54,6 +58,8 @@ defmodule Asciinema.Mixfile do
{:plug_rails_cookie_session_store, "~> 0.1"},
{:plugsnag, "~> 1.3.0"},
{:poison, "~> 2.2"},
{:poolboy, "~> 1.5"},
{:porcelain, "~> 2.0"},
{:postgrex, ">= 0.0.0"},
]
end

@ -1,4 +1,5 @@
%{"bugsnag": {:hex, :bugsnag, "1.4.0", "fda8c3f550c93568b6e9ac615b1a9be0c1c4e06c7eb0ffb04a133dfaf1e01327", [:mix], [{:httpoison, "~> 0.9", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
%{"briefly": {:hex, :briefly, "0.3.0", "16e6b76d2070ebc9cbd025fa85cf5dbaf52368c4bd896fb482b5a6b95a540c2f", [:mix], [], "hexpm"},
"bugsnag": {:hex, :bugsnag, "1.4.0", "fda8c3f550c93568b6e9ac615b1a9be0c1c4e06c7eb0ffb04a133dfaf1e01327", [:mix], [{:httpoison, "~> 0.9", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
@ -27,6 +28,7 @@
"plugsnag": {:hex, :plugsnag, "1.3.0", "eb974813360c979993205dcbde9a79fd02e3bd38ebe3870f5089e57a14ebaedb", [:mix], [{:bugsnag, "~> 1.3", [hex: :bugsnag, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.11.2", "139755c1359d3c5c6d6e8b1ea72556d39e2746f61c6ddfb442813c91f53487e8", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.0-rc", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}}

@ -0,0 +1,15 @@
defmodule Asciinema.AsciicastImageController do
use Asciinema.Web, :controller
alias Asciinema.{Repo, Asciicast, PngGenerator}
alias Plug.MIME
def show(conn, %{"id" => id} = _params) do
asciicast = Repo.one!(Asciicast.by_id_or_secret_token(id))
{:ok, png_path} = PngGenerator.generate(asciicast)
conn
|> put_resp_header("content-type", MIME.path(png_path))
|> send_file(200, png_path)
|> halt
end
end

@ -9,6 +9,7 @@ defmodule Asciinema.Asciicast do
field :stdout_frames, :string
field :private, :boolean
field :secret_token, :string
field :duration, :float
end
def by_id_or_secret_token(thing) do

@ -21,6 +21,17 @@ defmodule Asciinema.Router do
get "/a/:id/json", AsciicastFileController, :show
end
pipeline :asciicast_image do
plug :accepts, ["png"]
end
scope "/", Asciinema do
pipe_through :asciicast_image
# rewritten by TrailingFormat from /a/123.png to /a/123/png
get "/a/:id/png", AsciicastImageController, :show
end
pipeline :asciicast_animation do
plug :accepts, ["html"]
end

Loading…
Cancel
Save