Merge pull request #266 from asciinema/ex-upload
New upload endpoint implementationremove-old-upload-endpoint
commit
8d80aeb9b1
@ -1,30 +1,100 @@
|
|||||||
defmodule Asciinema.Asciicasts do
|
defmodule Asciinema.Asciicasts do
|
||||||
|
import Ecto.Query, warn: false
|
||||||
alias Asciinema.{Repo, Asciicast, FileStore}
|
alias Asciinema.{Repo, Asciicast, FileStore}
|
||||||
|
alias Asciinema.Asciicasts.PosterGenerator
|
||||||
|
|
||||||
def create_asciicast(user, %Plug.Upload{path: path, filename: filename} = upload) do
|
def get_asciicast!(id) when is_integer(id) do
|
||||||
asciicast = %Asciicast{user_id: user.id, file: filename}
|
Repo.get!(Asciicast, id)
|
||||||
|
end
|
||||||
with {:ok, json} <- File.read(path),
|
def get_asciicast!(thing) when is_binary(thing) do
|
||||||
{:ok, attrs} <- Poison.decode(json),
|
q = if String.length(thing) == 25 do
|
||||||
{:ok, attrs} <- extract_attrs(attrs),
|
from a in Asciicast, where: a.secret_token == ^thing
|
||||||
changeset = Asciicast.create_changeset(asciicast, attrs),
|
else
|
||||||
{:ok, %Asciicast{} = asciicast} <- Repo.insert(changeset) do
|
case Integer.parse(thing) do
|
||||||
put_file(asciicast, upload)
|
{id, ""} ->
|
||||||
{:ok, asciicast}
|
from a in Asciicast, where: a.private == false and a.id == ^id
|
||||||
|
_ ->
|
||||||
|
from a in Asciicast, where: a.id == -1 # TODO fixme
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Repo.one!(q)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_asciicast(user, params, user_agent \\ nil)
|
||||||
|
|
||||||
|
def create_asciicast(user, %Plug.Upload{path: path, filename: filename} = upload, user_agent) do
|
||||||
|
asciicast = %Asciicast{user_id: user.id,
|
||||||
|
user_agent: user_agent,
|
||||||
|
file: filename,
|
||||||
|
private: user.asciicasts_private_by_default}
|
||||||
|
|
||||||
|
{_, result} = Repo.transaction(fn ->
|
||||||
|
with {:ok, json} <- File.read(path),
|
||||||
|
{:ok, attrs} <- Poison.decode(json),
|
||||||
|
{:ok, attrs} <- extract_attrs(attrs),
|
||||||
|
changeset = Asciicast.create_changeset(asciicast, attrs),
|
||||||
|
{:ok, %Asciicast{} = asciicast} <- Repo.insert(changeset) do
|
||||||
|
save_file(asciicast, :file, upload)
|
||||||
|
generate_poster(asciicast)
|
||||||
|
{:ok, asciicast}
|
||||||
|
else
|
||||||
|
{:error, :invalid} ->
|
||||||
|
{:error, :parse_error}
|
||||||
|
otherwise ->
|
||||||
|
otherwise
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
defp extract_attrs(attrs) do
|
def create_asciicast(user, %{"meta" => attrs,
|
||||||
|
"stdout" => %Plug.Upload{filename: d_filename} = data,
|
||||||
|
"stdout_timing" => %Plug.Upload{filename: t_filename} = timing}, user_agent) do
|
||||||
|
attrs = Map.put(attrs, "version", 0)
|
||||||
|
asciicast = %Asciicast{user_id: user.id,
|
||||||
|
user_agent: unless(attrs["uname"], do: user_agent),
|
||||||
|
stdout_data: d_filename,
|
||||||
|
stdout_timing: t_filename,
|
||||||
|
private: user.asciicasts_private_by_default}
|
||||||
|
|
||||||
|
changeset = Asciicast.create_changeset(asciicast, attrs)
|
||||||
|
{_, result} = Repo.transaction(fn ->
|
||||||
|
with {:ok, %Asciicast{} = asciicast} <- Repo.insert(changeset) do
|
||||||
|
save_file(asciicast, :stdout_data, data)
|
||||||
|
save_file(asciicast, :stdout_timing, timing)
|
||||||
|
generate_poster(asciicast)
|
||||||
|
{:ok, asciicast}
|
||||||
|
else
|
||||||
|
otherwise -> otherwise
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_attrs(%{"version" => 1} = attrs) do
|
||||||
attrs = %{version: attrs["version"],
|
attrs = %{version: attrs["version"],
|
||||||
duration: attrs["duration"],
|
duration: attrs["duration"],
|
||||||
terminal_columns: attrs["width"],
|
terminal_columns: attrs["width"],
|
||||||
terminal_lines: attrs["height"],
|
terminal_lines: attrs["height"],
|
||||||
|
terminal_type: get_in(attrs, ["env", "TERM"]),
|
||||||
|
command: attrs["command"],
|
||||||
|
shell: get_in(attrs, ["env", "SHELL"]),
|
||||||
title: attrs["title"]}
|
title: attrs["title"]}
|
||||||
{:ok, attrs}
|
{:ok, attrs}
|
||||||
end
|
end
|
||||||
|
defp extract_attrs(_attrs) do
|
||||||
|
{:error, :unknown_format}
|
||||||
|
end
|
||||||
|
|
||||||
defp put_file(asciicast, %{path: tmp_file_path, content_type: content_type}) do
|
defp save_file(asciicast, type, %{path: tmp_file_path, content_type: content_type}) do
|
||||||
file_store_path = Asciicast.json_store_path(asciicast)
|
file_store_path = Asciicast.file_store_path(asciicast, type)
|
||||||
:ok = FileStore.put_file(file_store_path, tmp_file_path, content_type)
|
:ok = FileStore.put_file(file_store_path, tmp_file_path, content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp generate_poster(asciicast) do
|
||||||
|
PosterGenerator.generate(asciicast)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
defmodule Asciinema.Asciicasts.PosterGenerator do
|
||||||
|
alias Asciinema.Asciicast
|
||||||
|
|
||||||
|
@doc "Generates poster for given asciicast"
|
||||||
|
@callback generate(asciicast :: %Asciicast{}) :: :ok | {:error, term}
|
||||||
|
|
||||||
|
def generate(asciicast) do
|
||||||
|
Application.get_env(:asciinema, :poster_generator).generate(asciicast)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
defmodule Asciinema.Asciicasts.PosterGenerator.Noop do
|
||||||
|
def generate(_asciicast) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,8 @@
|
|||||||
|
defmodule Asciinema.Asciicasts.PosterGenerator.Sidekiq do
|
||||||
|
alias Asciinema.Asciicast
|
||||||
|
alias Asciinema.SidekiqClient
|
||||||
|
|
||||||
|
def generate(%Asciicast{id: id}) do
|
||||||
|
SidekiqClient.enqueue("AsciicastWorker", [id])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
defmodule Asciinema.SidekiqClient do
|
||||||
|
def enqueue(class, args, queue \\ "default") do
|
||||||
|
job = %{queue: queue,
|
||||||
|
class: class,
|
||||||
|
args: args,
|
||||||
|
enqueued_at: Timex.now |> Timex.to_unix,
|
||||||
|
jid: Crypto.random_token(24),
|
||||||
|
retry: true}
|
||||||
|
|
||||||
|
payload = Poison.encode!(job)
|
||||||
|
{:ok, _} = Redix.command(:redix, ["LPUSH", "queue:#{queue}", payload])
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,58 @@
|
|||||||
|
defmodule Asciinema.Users do
|
||||||
|
import Ecto.Query, warn: false
|
||||||
|
alias Asciinema.{Repo, User, ApiToken}
|
||||||
|
|
||||||
|
def authenticate(api_token) do
|
||||||
|
q = from u in User,
|
||||||
|
join: at in ApiToken,
|
||||||
|
on: at.user_id == u.id,
|
||||||
|
select: {u, at.revoked_at},
|
||||||
|
where: at.token == ^api_token
|
||||||
|
|
||||||
|
case Repo.one(q) do
|
||||||
|
nil ->
|
||||||
|
{:error, :token_not_found}
|
||||||
|
{%User{} = user, nil} ->
|
||||||
|
{:ok, user}
|
||||||
|
{%User{}, _} ->
|
||||||
|
{:error, :token_revoked}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_user_with_api_token(username, api_token) do
|
||||||
|
case authenticate(api_token) do
|
||||||
|
{:ok, %User{} = user} ->
|
||||||
|
user
|
||||||
|
{:error, :token_revoked} ->
|
||||||
|
nil
|
||||||
|
{:error, :token_not_found} ->
|
||||||
|
create_user_with_api_token(username, api_token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_user_with_api_token(username, api_token) do
|
||||||
|
user_changeset = User.temporary_changeset(username)
|
||||||
|
|
||||||
|
{_, result} = Repo.transaction(fn ->
|
||||||
|
with {:ok, %User{} = user} <- Repo.insert(user_changeset),
|
||||||
|
api_token_changeset = ApiToken.create_changeset(user, api_token),
|
||||||
|
{:ok, %ApiToken{}} <- Repo.insert(api_token_changeset) do
|
||||||
|
user
|
||||||
|
else
|
||||||
|
_otherwise -> Repo.rollback(nil)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_api_token!(token) do
|
||||||
|
Repo.get_by!(ApiToken, token: token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_api_token!(api_token) do
|
||||||
|
api_token
|
||||||
|
|> ApiToken.revoke_changeset
|
||||||
|
|> Repo.update!
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": 5,
|
||||||
|
"width": 96,
|
||||||
|
"height": 26,
|
||||||
|
"duration": 11.146430015564,
|
||||||
|
"command": "/bin/bash",
|
||||||
|
"title": "bashing :)",
|
||||||
|
"env": {
|
||||||
|
"TERM": "screen-256color",
|
||||||
|
"SHELL": "/bin/zsh"
|
||||||
|
},
|
||||||
|
"stdout": [
|
||||||
|
[1.234567, "foo bar"],
|
||||||
|
[5.678987, "baz qux"],
|
||||||
|
[3.456789, "żółć jaźń"]
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
defmodule Asciinema.AsciicastsTest do
|
||||||
|
use Asciinema.DataCase
|
||||||
|
alias Asciinema.{Asciicasts, Asciicast}
|
||||||
|
|
||||||
|
describe "create_asciicast/3" do
|
||||||
|
test "json file, v0 format with uname" do
|
||||||
|
user = fixture(:user)
|
||||||
|
params = %{"meta" => %{"command" => "/bin/bash",
|
||||||
|
"duration" => 11.146430015564,
|
||||||
|
"shell" => "/bin/zsh",
|
||||||
|
"terminal_columns" => 96,
|
||||||
|
"terminal_lines" => 26,
|
||||||
|
"terminal_type" => "screen-256color",
|
||||||
|
"title" => "bashing :)",
|
||||||
|
"uname" => "Linux 3.9.9-302.fc19.x86_64 #1 SMP Sat Jul 6 13:41:07 UTC 2013 x86_64"},
|
||||||
|
"stdout" => fixture(:upload, %{path: "0.9.7/stdout",
|
||||||
|
content_type: "application/octet-stream"}),
|
||||||
|
"stdout_timing" => fixture(:upload, %{path: "0.9.7/stdout.time",
|
||||||
|
content_type: "application/octet-stream"})}
|
||||||
|
|
||||||
|
{:ok, asciicast} = Asciicasts.create_asciicast(user, params, "a/user/agent")
|
||||||
|
|
||||||
|
assert %Asciicast{version: 0,
|
||||||
|
file: nil,
|
||||||
|
stdout_data: "stdout",
|
||||||
|
stdout_timing: "stdout.time",
|
||||||
|
command: "/bin/bash",
|
||||||
|
duration: 11.146430015564,
|
||||||
|
shell: "/bin/zsh",
|
||||||
|
terminal_type: "screen-256color",
|
||||||
|
terminal_columns: 96,
|
||||||
|
terminal_lines: 26,
|
||||||
|
title: "bashing :)",
|
||||||
|
uname: "Linux 3.9.9-302.fc19.x86_64 #1 SMP Sat Jul 6 13:41:07 UTC 2013 x86_64",
|
||||||
|
user_agent: nil} = asciicast
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, v0 format without uname" do
|
||||||
|
user = fixture(:user)
|
||||||
|
params = %{"meta" => %{"command" => "/bin/bash",
|
||||||
|
"duration" => 11.146430015564,
|
||||||
|
"shell" => "/bin/zsh",
|
||||||
|
"terminal_columns" => 96,
|
||||||
|
"terminal_lines" => 26,
|
||||||
|
"terminal_type" => "screen-256color",
|
||||||
|
"title" => "bashing :)"},
|
||||||
|
"stdout" => fixture(:upload, %{path: "0.9.8/stdout",
|
||||||
|
content_type: "application/octet-stream"}),
|
||||||
|
"stdout_timing" => fixture(:upload, %{path: "0.9.8/stdout.time",
|
||||||
|
content_type: "application/octet-stream"})}
|
||||||
|
|
||||||
|
{:ok, asciicast} = Asciicasts.create_asciicast(user, params, "a/user/agent")
|
||||||
|
|
||||||
|
assert %Asciicast{version: 0,
|
||||||
|
file: nil,
|
||||||
|
stdout_data: "stdout",
|
||||||
|
stdout_timing: "stdout.time",
|
||||||
|
command: "/bin/bash",
|
||||||
|
duration: 11.146430015564,
|
||||||
|
shell: "/bin/zsh",
|
||||||
|
terminal_type: "screen-256color",
|
||||||
|
terminal_columns: 96,
|
||||||
|
terminal_lines: 26,
|
||||||
|
title: "bashing :)",
|
||||||
|
uname: nil,
|
||||||
|
user_agent: "a/user/agent"} = asciicast
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, v1 format" do
|
||||||
|
user = fixture(:user)
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
|
||||||
|
{:ok, asciicast} = Asciicasts.create_asciicast(user, upload, "a/user/agent")
|
||||||
|
|
||||||
|
assert %Asciicast{version: 1,
|
||||||
|
file: "asciicast.json",
|
||||||
|
stdout_data: nil,
|
||||||
|
stdout_timing: nil,
|
||||||
|
command: "/bin/bash",
|
||||||
|
duration: 11.146430015564,
|
||||||
|
shell: "/bin/zsh",
|
||||||
|
terminal_type: "screen-256color",
|
||||||
|
terminal_columns: 96,
|
||||||
|
terminal_lines: 26,
|
||||||
|
title: "bashing :)",
|
||||||
|
uname: nil,
|
||||||
|
user_agent: "a/user/agent"} = asciicast
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, v1 format (missing required data)" do
|
||||||
|
user = fixture(:user)
|
||||||
|
upload = fixture(:upload, %{path: "1/invalid.json"})
|
||||||
|
|
||||||
|
assert {:error, %Ecto.Changeset{}} = Asciicasts.create_asciicast(user, upload)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, unsupported version number" do
|
||||||
|
user = fixture(:user)
|
||||||
|
upload = fixture(:upload, %{path: "5/asciicast.json"})
|
||||||
|
|
||||||
|
assert {:error, :unknown_format} = Asciicasts.create_asciicast(user, upload)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-json file" do
|
||||||
|
user = fixture(:user)
|
||||||
|
upload = fixture(:upload, %{path: "new-logo-bars.png"})
|
||||||
|
|
||||||
|
assert {:error, :parse_error} = Asciicasts.create_asciicast(user, upload)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,116 @@
|
|||||||
|
defmodule Asciinema.Api.AsciicastControllerTest do
|
||||||
|
use Asciinema.ConnCase
|
||||||
|
alias Asciinema.Users
|
||||||
|
|
||||||
|
setup %{conn: conn} = context do
|
||||||
|
token = Map.get(context, :token, "9da34ff4-9bf7-45d4-aa88-98c933b15a3f")
|
||||||
|
|
||||||
|
conn = if token do
|
||||||
|
put_req_header(conn, "authorization", "Basic " <> Base.encode64("test:" <> token))
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, conn: conn, token: token}
|
||||||
|
end
|
||||||
|
|
||||||
|
@asciicast_url ~r|^http://localhost:4001/a/[a-zA-Z0-9]{25}|
|
||||||
|
|
||||||
|
describe ".create" do
|
||||||
|
@tag token: nil
|
||||||
|
test "separate files (pre-v1 params), v0.9.7 client", %{conn: conn} do
|
||||||
|
asciicast = %{"meta" => fixture(:upload, %{path: "0.9.7/meta.json",
|
||||||
|
content_type: "application/json"}),
|
||||||
|
"stdout" => fixture(:upload, %{path: "0.9.7/stdout",
|
||||||
|
content_type: "application/octet-stream"}),
|
||||||
|
"stdout_timing" => fixture(:upload, %{path: "0.9.7/stdout.time",
|
||||||
|
content_type: "application/octet-stream"})}
|
||||||
|
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => asciicast}
|
||||||
|
assert text_response(conn, 201) =~ @asciicast_url
|
||||||
|
assert List.first(get_resp_header(conn, "location")) =~ @asciicast_url
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag token: nil
|
||||||
|
test "separate files (pre-v1 params), v0.9.8 client", %{conn: conn} do
|
||||||
|
asciicast = %{"meta" => fixture(:upload, %{path: "0.9.8/meta.json",
|
||||||
|
content_type: "application/json"}),
|
||||||
|
"stdout" => fixture(:upload, %{path: "0.9.8/stdout",
|
||||||
|
content_type: "application/octet-stream"}),
|
||||||
|
"stdout_timing" => fixture(:upload, %{path: "0.9.8/stdout.time",
|
||||||
|
content_type: "application/octet-stream"})}
|
||||||
|
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => asciicast}
|
||||||
|
assert text_response(conn, 201) =~ @asciicast_url
|
||||||
|
assert List.first(get_resp_header(conn, "location")) =~ @asciicast_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "separate files (pre-v1 params), v0.9.9 client", %{conn: conn} do
|
||||||
|
asciicast = %{"meta" => fixture(:upload, %{path: "0.9.9/meta.json",
|
||||||
|
content_type: "application/json"}),
|
||||||
|
"stdout" => fixture(:upload, %{path: "0.9.9/stdout",
|
||||||
|
content_type: "application/octet-stream"}),
|
||||||
|
"stdout_timing" => fixture(:upload, %{path: "0.9.9/stdout.time",
|
||||||
|
content_type: "application/octet-stream"})}
|
||||||
|
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => asciicast}
|
||||||
|
assert text_response(conn, 201) =~ @asciicast_url
|
||||||
|
assert List.first(get_resp_header(conn, "location")) =~ @asciicast_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, v1 format", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert text_response(conn, 201) =~ @asciicast_url
|
||||||
|
assert List.first(get_resp_header(conn, "location")) =~ @asciicast_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, v1 format (missing required data)", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "1/invalid.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert %{"errors" => _} = json_response(conn, 422)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "json file, unsupported version number", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "5/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert text_response(conn, 415) =~ ~r|not supported|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-json file", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "new-logo-bars.png"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert text_response(conn, 400) =~ ~r|valid asciicast|
|
||||||
|
end
|
||||||
|
|
||||||
|
test "existing user (API token)", %{conn: conn, token: token} do
|
||||||
|
Users.create_user_with_api_token("test", token)
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert text_response(conn, 201) =~ @asciicast_url
|
||||||
|
assert List.first(get_resp_header(conn, "location")) =~ @asciicast_url
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag token: nil
|
||||||
|
test "no authentication", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert response(conn, 401)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "authentication with revoked token", %{conn: conn, token: token} do
|
||||||
|
Users.get_user_with_api_token("test", token) # force registration of the token
|
||||||
|
token |> Users.get_api_token! |> Users.revoke_api_token!
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert response(conn, 401)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag token: "invalid-lol"
|
||||||
|
test "authentication with invalid token", %{conn: conn} do
|
||||||
|
upload = fixture(:upload, %{path: "1/asciicast.json"})
|
||||||
|
conn = post conn, api_asciicast_path(conn, :create), %{"asciicast" => upload}
|
||||||
|
assert response(conn, 401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,55 @@
|
|||||||
|
defmodule Asciinema.DataCase do
|
||||||
|
@moduledoc """
|
||||||
|
This module defines the setup for tests requiring
|
||||||
|
access to the application's data layer.
|
||||||
|
|
||||||
|
You may define functions here to be used as helpers in
|
||||||
|
your tests.
|
||||||
|
|
||||||
|
Finally, if the test case interacts with the database,
|
||||||
|
it cannot be async. For this reason, every test runs
|
||||||
|
inside a transaction which is reset at the beginning
|
||||||
|
of the test unless the test case is marked as async.
|
||||||
|
"""
|
||||||
|
|
||||||
|
use ExUnit.CaseTemplate
|
||||||
|
|
||||||
|
using do
|
||||||
|
quote do
|
||||||
|
alias Asciinema.Repo
|
||||||
|
|
||||||
|
import Ecto
|
||||||
|
import Ecto.Changeset
|
||||||
|
import Ecto.Query
|
||||||
|
import Asciinema.DataCase
|
||||||
|
|
||||||
|
import Asciinema.Fixtures
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
setup tags do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Asciinema.Repo)
|
||||||
|
|
||||||
|
unless tags[:async] do
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(Asciinema.Repo, {:shared, self()})
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
A helper that transform changeset errors to a map of messages.
|
||||||
|
|
||||||
|
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
|
||||||
|
assert "password is too short" in errors_on(changeset).password
|
||||||
|
assert %{password: ["password is too short"]} = errors_on(changeset)
|
||||||
|
|
||||||
|
"""
|
||||||
|
def errors_on(changeset) do
|
||||||
|
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||||
|
Enum.reduce(opts, message, fn {key, value}, acc ->
|
||||||
|
String.replace(acc, "%{#{key}}", to_string(value))
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,85 @@
|
|||||||
|
defmodule Asciinema.Api.AsciicastController do
|
||||||
|
use Asciinema.Web, :controller
|
||||||
|
import Asciinema.Auth, only: [get_basic_auth: 1, put_basic_auth: 3]
|
||||||
|
alias Asciinema.{Asciicasts, Users, User}
|
||||||
|
|
||||||
|
plug :parse_v0_params
|
||||||
|
plug :authenticate
|
||||||
|
|
||||||
|
def create(conn, %{"asciicast" => %Plug.Upload{} = upload}) do
|
||||||
|
do_create(conn, upload)
|
||||||
|
end
|
||||||
|
def create(conn, %{"asciicast" => %{"meta" => %{},
|
||||||
|
"stdout" => %Plug.Upload{},
|
||||||
|
"stdout_timing" => %Plug.Upload{}} = asciicast_params}) do
|
||||||
|
do_create(conn, asciicast_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_create(conn, params) do
|
||||||
|
user = conn.assigns.current_user
|
||||||
|
user_agent = conn |> get_req_header("user-agent") |> List.first
|
||||||
|
|
||||||
|
case Asciicasts.create_asciicast(user, params, user_agent) do
|
||||||
|
{:ok, asciicast} ->
|
||||||
|
url = asciicast_url(conn, :show, asciicast)
|
||||||
|
conn
|
||||||
|
|> put_status(:created)
|
||||||
|
|> put_resp_header("location", url)
|
||||||
|
|> text(url)
|
||||||
|
{:error, :parse_error} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> text("This doesn't look like a valid asciicast file")
|
||||||
|
{:error, :unknown_format} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unsupported_media_type)
|
||||||
|
|> text("Format not supported")
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> render("error.json", changeset: changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_v0_params(%Plug.Conn{params: %{"asciicast" => %{"meta" => %Plug.Upload{path: meta_path}}}} = conn, _) do
|
||||||
|
with {:ok, json} <- File.read(meta_path),
|
||||||
|
{:ok, attrs} <- Poison.decode(json),
|
||||||
|
{:ok, meta} <- extract_v0_attrs(attrs) do
|
||||||
|
conn
|
||||||
|
|> put_param(["asciicast", "meta"], meta)
|
||||||
|
|> put_basic_auth(attrs["username"], attrs["user_token"])
|
||||||
|
else
|
||||||
|
{:error, :invalid} ->
|
||||||
|
send_resp(conn, 400, "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
defp parse_v0_params(conn, _), do: conn
|
||||||
|
|
||||||
|
defp put_param(%Plug.Conn{params: params} = conn, path, value) do
|
||||||
|
params = put_in(params, path, value)
|
||||||
|
%{conn | params: params}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_v0_attrs(attrs) do
|
||||||
|
attrs = Map.merge(
|
||||||
|
Map.take(attrs, ["command", "duration", "shell", "title", "uname"]),
|
||||||
|
%{"terminal_columns" => get_in(attrs, ["term", "columns"]),
|
||||||
|
"terminal_lines" => get_in(attrs, ["term", "lines"]),
|
||||||
|
"terminal_type" => get_in(attrs, ["term", "type"])}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, attrs}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authenticate(conn, _opts) do
|
||||||
|
with {username, api_token} <- get_basic_auth(conn),
|
||||||
|
%User{} = user <- Users.get_user_with_api_token(username, api_token) do
|
||||||
|
assign(conn, :current_user, user)
|
||||||
|
else
|
||||||
|
_otherwise ->
|
||||||
|
conn
|
||||||
|
|> send_resp(401, "Invalid or revoked recorder token")
|
||||||
|
|> halt()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,28 @@
|
|||||||
|
defmodule Asciinema.ApiToken do
|
||||||
|
use Asciinema.Web, :model
|
||||||
|
alias Asciinema.{ApiToken, User}
|
||||||
|
|
||||||
|
schema "api_tokens" do
|
||||||
|
field :token, :string
|
||||||
|
field :revoked_at, Timex.Ecto.DateTime
|
||||||
|
|
||||||
|
timestamps(inserted_at: :created_at)
|
||||||
|
|
||||||
|
belongs_to :user, Asciinema.User
|
||||||
|
end
|
||||||
|
|
||||||
|
@uuid4 ~r/\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\z/
|
||||||
|
|
||||||
|
def create_changeset(%User{id: user_id}, token) do
|
||||||
|
%ApiToken{user_id: user_id}
|
||||||
|
|> change(%{token: token})
|
||||||
|
|> validate_format(:token, @uuid4)
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_changeset(%ApiToken{revoked_at: nil} = api_token) do
|
||||||
|
change(api_token, %{revoked_at: Timex.now()})
|
||||||
|
end
|
||||||
|
def revoke_changeset(%ApiToken{} = api_token) do
|
||||||
|
change(api_token)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
defmodule Asciinema.Api.AsciicastView do
|
||||||
|
use Asciinema.Web, :view
|
||||||
|
|
||||||
|
def translate_errors(changeset) do
|
||||||
|
Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("error.json", %{changeset: changeset}) do
|
||||||
|
%{errors: translate_errors(changeset)}
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue