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
|
||||
import Ecto.Query, warn: false
|
||||
alias Asciinema.{Repo, Asciicast, FileStore}
|
||||
alias Asciinema.Asciicasts.PosterGenerator
|
||||
|
||||
def create_asciicast(user, %Plug.Upload{path: path, filename: filename} = upload) do
|
||||
asciicast = %Asciicast{user_id: user.id, file: filename}
|
||||
|
||||
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
|
||||
put_file(asciicast, upload)
|
||||
{:ok, asciicast}
|
||||
def get_asciicast!(id) when is_integer(id) do
|
||||
Repo.get!(Asciicast, id)
|
||||
end
|
||||
def get_asciicast!(thing) when is_binary(thing) do
|
||||
q = if String.length(thing) == 25 do
|
||||
from a in Asciicast, where: a.secret_token == ^thing
|
||||
else
|
||||
case Integer.parse(thing) do
|
||||
{id, ""} ->
|
||||
from a in Asciicast, where: a.private == false and a.id == ^id
|
||||
_ ->
|
||||
from a in Asciicast, where: a.id == -1 # TODO fixme
|
||||
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
|
||||
|
||||
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"],
|
||||
duration: attrs["duration"],
|
||||
terminal_columns: attrs["width"],
|
||||
terminal_lines: attrs["height"],
|
||||
terminal_type: get_in(attrs, ["env", "TERM"]),
|
||||
command: attrs["command"],
|
||||
shell: get_in(attrs, ["env", "SHELL"]),
|
||||
title: attrs["title"]}
|
||||
{:ok, attrs}
|
||||
end
|
||||
defp extract_attrs(_attrs) do
|
||||
{:error, :unknown_format}
|
||||
end
|
||||
|
||||
defp put_file(asciicast, %{path: tmp_file_path, content_type: content_type}) do
|
||||
file_store_path = Asciicast.json_store_path(asciicast)
|
||||
defp save_file(asciicast, type, %{path: tmp_file_path, content_type: content_type}) do
|
||||
file_store_path = Asciicast.file_store_path(asciicast, type)
|
||||
:ok = FileStore.put_file(file_store_path, tmp_file_path, content_type)
|
||||
end
|
||||
|
||||
defp generate_poster(asciicast) do
|
||||
PosterGenerator.generate(asciicast)
|
||||
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