diff --git a/config/config.exs b/config/config.exs index 29a86cb..9497297 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,6 +28,22 @@ config :phoenix, :template_engines, config :bugsnag, api_key: System.get_env("BUGSNAG_API_KEY") config :bugsnag, release_stage: Mix.env +if System.get_env("S3_BUCKET") do + config :asciinema, :file_store, Asciinema.FileStore.S3 + + config :asciinema, Asciinema.FileStore.S3, + region: System.get_env("S3_REGION"), + bucket: System.get_env("S3_BUCKET"), + path: "uploads/" + + config :ex_aws, + access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role], + secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role] +else + config :asciinema, :file_store, Asciinema.FileStore.Local + config :asciinema, Asciinema.FileStore.Local, path: "uploads/" +end + # 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" diff --git a/config/test.exs b/config/test.exs index c676e3f..6acf29c 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,3 +17,6 @@ config :asciinema, Asciinema.Repo, database: "asciinema_test", hostname: "localhost", pool: Ecto.Adapters.SQL.Sandbox + +config :asciinema, :file_store, Asciinema.FileStore.Local +config :asciinema, Asciinema.FileStore.Local, path: "uploads/test/" diff --git a/docker/nginx/asciinema.conf b/docker/nginx/asciinema.conf index 7b313c7..702f571 100644 --- a/docker/nginx/asciinema.conf +++ b/docker/nginx/asciinema.conf @@ -24,11 +24,11 @@ server { try_files /maintenance.html $uri/index.html $uri.html $uri @phoenix; } - location ~ ^/a/[^.]+\.(json|png)$ { + location ~ ^/a/[^.]+\.png$ { try_files $uri $uri/index.html $uri.html @clj; } - location ~ ^/a/[^.]+\.gif$ { + location ~ ^/a/[^.]+\.(json|gif)$ { try_files $uri $uri/index.html $uri.html @phoenix; } diff --git a/lib/asciinema/file_store.ex b/lib/asciinema/file_store.ex new file mode 100644 index 0000000..87e6bf1 --- /dev/null +++ b/lib/asciinema/file_store.ex @@ -0,0 +1,4 @@ +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{} +end diff --git a/lib/asciinema/file_store/local.ex b/lib/asciinema/file_store/local.ex new file mode 100644 index 0000000..2d68b2b --- /dev/null +++ b/lib/asciinema/file_store/local.ex @@ -0,0 +1,29 @@ +defmodule Asciinema.FileStore.Local do + @behaviour Asciinema.FileStore + import Plug.Conn + alias Plug.MIME + + def serve_file(conn, path, nil) do + do_serve_file(conn, path) + end + def serve_file(conn, path, filename) do + conn + |> put_resp_header("content-disposition", "attachment; filename=#{filename}") + |> do_serve_file(path) + end + + defp do_serve_file(conn, path) do + conn + |> put_resp_header("content-type", MIME.path(path)) + |> send_file(200, base_path() <> path) + |> halt + end + + defp config do + Application.get_env(:asciinema, Asciinema.FileStore.Local) + end + + defp base_path do + Keyword.get(config(), :path) + end +end diff --git a/lib/asciinema/file_store/s3.ex b/lib/asciinema/file_store/s3.ex new file mode 100644 index 0000000..8f84d9e --- /dev/null +++ b/lib/asciinema/file_store/s3.ex @@ -0,0 +1,36 @@ +defmodule Asciinema.FileStore.S3 do + @behaviour Asciinema.FileStore + import Phoenix.Controller, only: [redirect: 2] + + def serve_file(conn, path, nil) do + do_serve_file(conn, path) + end + def serve_file(conn, path, filename) do + do_serve_file(conn, path, ["response-content-disposition": "attachment; filename=#{filename}"]) + end + + defp do_serve_file(conn, path, query_params \\ []) do + {:ok, url} = + ExAws.Config.new(:s3, region: region()) + |> ExAws.S3.presigned_url(:get, bucket(), base_path() <> path, query_params: query_params) + + conn + |> redirect(external: url) + end + + defp config do + Application.get_env(:asciinema, Asciinema.FileStore.S3) + end + + defp region do + Keyword.get(config(), :region) + end + + defp bucket do + Keyword.get(config(), :bucket) + end + + defp base_path do + Keyword.get(config(), :path) + end +end diff --git a/mix.exs b/mix.exs index 47ca76a..20818a4 100644 --- a/mix.exs +++ b/mix.exs @@ -21,6 +21,7 @@ defmodule Asciinema.Mixfile do applications: [ :bugsnag, :cowboy, + :ex_aws, :gettext, :logger, :phoenix, @@ -42,6 +43,7 @@ defmodule Asciinema.Mixfile do defp deps do [ {:cowboy, "~> 1.0"}, + {:ex_aws, "~> 1.0"}, {:gettext, "~> 0.11"}, {:phoenix, "~> 1.2.1"}, {:phoenix_ecto, "~> 3.0"}, diff --git a/mix.lock b/mix.lock index f35ad0d..81e8903 100644 --- a/mix.lock +++ b/mix.lock @@ -7,6 +7,7 @@ "decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "2.0.4", "03fd3b9aa508b1383eb38c00ac389953ed22af53811aa2e504975a3e814a8d97", [:mix], [{:db_connection, "~> 1.0-rc.2", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.11.2", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_aws": {:hex, :ex_aws, "1.1.2", "b78416d0a84efe92c22e5df8ba7ca028d63b2b4228f95871a1ecf10324b6493b", [:mix], [{:configparser_ex, "~> 0.2.1", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.0.6", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/web/controllers/asciicast_file_controller.ex b/web/controllers/asciicast_file_controller.ex new file mode 100644 index 0000000..e593cfe --- /dev/null +++ b/web/controllers/asciicast_file_controller.ex @@ -0,0 +1,23 @@ +defmodule Asciinema.AsciicastFileController do + use Asciinema.Web, :controller + alias Asciinema.{Repo, Asciicast} + + def show(conn, %{"id" => id} = params) do + asciicast = Repo.one!(Asciicast.by_id_or_secret_token(id)) + path = Asciicast.json_store_path(asciicast) + filename = download_filename(asciicast, params) + + file_store().serve_file(conn, path, filename) + end + + defp download_filename(%Asciicast{id: id}, %{"dl" => _}) do + "asciicast-#{id}.json" + end + defp download_filename(_asciicast, _params) do + nil + end + + defp file_store do + Application.get_env(:asciinema, :file_store) + end +end diff --git a/web/models/asciicast.ex b/web/models/asciicast.ex index 86df24c..f9297d5 100644 --- a/web/models/asciicast.ex +++ b/web/models/asciicast.ex @@ -6,6 +6,7 @@ defmodule Asciinema.Asciicast do field :file, :string field :stdout_data, :string field :stdout_timing, :string + field :stdout_frames, :string field :private, :boolean field :secret_token, :string end @@ -22,4 +23,11 @@ defmodule Asciinema.Asciicast do end end end + + def json_store_path(%__MODULE__{id: id, file: file}) when is_binary(file) do + "asciicast/file/#{id}/#{file}" + end + def json_store_path(%__MODULE__{id: id, stdout_frames: stdout_frames}) when is_binary(stdout_frames) do + "asciicast/stdout_frames/#{id}/#{stdout_frames}" + end end diff --git a/web/router.ex b/web/router.ex index a873d0f..679515e 100644 --- a/web/router.ex +++ b/web/router.ex @@ -10,6 +10,17 @@ defmodule Asciinema.Router do plug Asciinema.Auth end + pipeline :asciicast_file do + plug :accepts, ["json"] + end + + scope "/", Asciinema do + pipe_through :asciicast_file + + # rewritten by TrailingFormat from /a/123.json to /a/123/json + get "/a/:id/json", AsciicastFileController, :show + end + pipeline :asciicast_animation do plug :accepts, ["html"] end