Implement asciicast upload

This commit is contained in:
Marcin Kulik
2014-11-02 19:04:19 +01:00
parent bf090e3ae7
commit ece5221bbd
4 changed files with 193 additions and 48 deletions

View File

@@ -1,50 +1,105 @@
package api
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"time"
)
type Frame struct {
Delay float64
Data []byte
}
type Api interface {
CreateAsciicast(*Asciicast) (string, error)
CreateAsciicast([]Frame, time.Duration, int, int, string, string) (string, error)
}
func New(url, token string) *AsciinemaApi {
return &AsciinemaApi{
url: url,
token: token,
http: &HttpClient{},
}
}
type AsciinemaApi struct {
url string
token string
http HTTP
}
func (a *AsciinemaApi) CreateAsciicast(asciicast *Asciicast) (string, error) {
return "/foo", nil
func (a *AsciinemaApi) CreateAsciicast(frames []Frame, duration time.Duration, cols, rows int, command, title string) (string, error) {
files := map[string]io.Reader{
"asciicast[stdout]:stdout": gzippedDataReader(frames),
"asciicast[stdout_timing]:stdout.time": gzippedTimingReader(frames),
"asciicast[meta]:meta.json": metadataReader(duration, cols, rows, command, title),
}
// TODO: set proper user agent
response, err := a.http.PostForm(a.url+"/api/asciicasts", os.Getenv("USER"), a.token, files)
if err != nil {
return "", err
}
defer response.Body.Close()
body := &bytes.Buffer{}
_, err = body.ReadFrom(response.Body)
if err != nil {
return "", err
}
type Asciicast struct {
Command string
Title string
Rows int
Cols int
Shell string
Username string
Term string
Stdout io.Reader
// TODO: handle non-200 statuses
return body.String(), nil
}
func NewAsciicast(command, title string, rows, cols int, stdout io.Reader) *Asciicast {
return &Asciicast{
Command: command,
Title: title,
Rows: rows,
Cols: cols,
Shell: os.Getenv("SHELL"),
Username: os.Getenv("USER"),
Term: os.Getenv("TERM"),
Stdout: stdout,
func gzippedDataReader(frames []Frame) io.Reader {
data := &bytes.Buffer{}
w := gzip.NewWriter(data)
for _, frame := range frames {
w.Write(frame.Data)
}
w.Close()
return data
}
func gzippedTimingReader(frames []Frame) io.Reader {
timing := &bytes.Buffer{}
w := gzip.NewWriter(timing)
for _, frame := range frames {
w.Write([]byte(fmt.Sprintf("%f %d\n", frame.Delay, len(frame.Data))))
}
w.Close()
return timing
}
func metadataReader(duration time.Duration, cols, rows int, command, title string) io.Reader {
metadata := map[string]interface{}{
"duration": duration.Seconds(),
"title": title,
"command": command,
"shell": os.Getenv("SHELL"),
"term": map[string]interface{}{
"type": os.Getenv("TERM"),
"columns": cols,
"lines": rows,
},
}
buf := &bytes.Buffer{}
encoder := json.NewEncoder(buf)
encoder.Encode(metadata)
return buf
}

64
api/http.go Normal file
View File

@@ -0,0 +1,64 @@
package api
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"strings"
)
type HTTP interface {
PostForm(string, string, string, map[string]io.Reader) (*http.Response, error)
}
type HttpClient struct{}
func (c *HttpClient) PostForm(url, username, password string, files map[string]io.Reader) (*http.Response, error) {
body, contentType, err := c.multiPartBody(url, files)
if err != nil {
return nil, err
}
client := &http.Client{}
req, err := http.NewRequest("POST", url, body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.SetBasicAuth(username, password)
return client.Do(req)
}
func (c *HttpClient) multiPartBody(url string, files map[string]io.Reader) (io.Reader, string, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if files != nil {
for name, reader := range files {
items := strings.Split(name, ":")
fieldname := items[0]
filename := items[1]
part, err := writer.CreateFormFile(fieldname, filename)
if err != nil {
return nil, "", err
}
_, err = io.Copy(part, reader)
if err != nil {
return nil, "", err
}
}
}
err := writer.Close()
if err != nil {
return nil, "", err
}
return body, writer.FormDataContentType(), nil
}

View File

@@ -1,11 +1,10 @@
package commands
import (
"bytes"
"flag"
"fmt"
"io"
"os"
"time"
"github.com/asciinema/asciinema-cli/api"
"github.com/asciinema/asciinema-cli/cli"
@@ -61,15 +60,17 @@ func (c *RecordCommand) Execute(args []string) error {
}
util.Printf("Asciicast recording started.")
util.Printf("Hit ctrl-d or type \"exit\" to finish.")
util.Printf(`Hit ctrl-d or type "exit" to finish.`)
stdout := &StdoutStream{}
stdout := NewStream()
err := c.Terminal.Record(c.Command, stdout)
if err != nil {
return err
}
stdout.Close()
util.Printf("Asciicast recording finished.")
if !c.NoConfirm {
@@ -78,9 +79,8 @@ func (c *RecordCommand) Execute(args []string) error {
}
rows, cols, _ = c.Terminal.Size()
asciicast := api.NewAsciicast(c.Command, c.Title, rows, cols, stdout.Reader())
url, err := c.Api.CreateAsciicast(asciicast)
url, err := c.Api.CreateAsciicast(stdout.Frames, stdout.Duration(), cols, rows, c.Command, c.Title)
if err != nil {
return err
}
@@ -102,15 +102,36 @@ func defaultRecCommand(recCommand string) string {
return recCommand
}
type StdoutStream struct {
data []byte
type Stream struct {
Frames []api.Frame
startTime time.Time
lastWriteTime time.Time
}
func (s *StdoutStream) Write(p []byte) (int, error) {
s.data = append(s.data, p...)
func NewStream() *Stream {
now := time.Now()
return &Stream{
startTime: now,
lastWriteTime: now,
}
}
func (s *Stream) Write(p []byte) (int, error) {
now := time.Now()
frame := api.Frame{}
frame.Delay = now.Sub(s.lastWriteTime).Seconds()
frame.Data = make([]byte, len(p))
copy(frame.Data, p)
s.Frames = append(s.Frames, frame)
s.lastWriteTime = now
return len(p), nil
}
func (s *StdoutStream) Reader() io.Reader {
return bytes.NewReader(s.data)
func (s *Stream) Close() {
s.lastWriteTime = time.Now()
}
func (s *Stream) Duration() time.Duration {
return s.lastWriteTime.Sub(s.startTime)
}

View File

@@ -1,10 +1,10 @@
package commands_test
import (
"bytes"
"errors"
"io"
"testing"
"time"
"github.com/asciinema/asciinema-cli/api"
"github.com/asciinema/asciinema-cli/commands"
@@ -24,6 +24,8 @@ func (t *testTerminal) Record(command string, stdoutCopy io.Writer) error {
}
stdoutCopy.Write([]byte("hello"))
stdoutCopy.Write([]byte("world"))
return nil
}
@@ -32,28 +34,31 @@ type testApi struct {
t *testing.T
}
func (a *testApi) CreateAsciicast(asciicast *api.Asciicast) (string, error) {
if asciicast.Command != "ls" {
func (a *testApi) CreateAsciicast(frames []api.Frame, duration time.Duration, cols, rows int, command, title string) (string, error) {
if command != "ls" {
a.t.Errorf("expected command to be set on asciicast")
}
if asciicast.Title != "listing" {
if title != "listing" {
a.t.Errorf("expected title to be set on asciicast")
}
if asciicast.Rows != 15 {
if rows != 15 {
a.t.Errorf("expected rows to be set on asciicast")
}
if asciicast.Cols != 40 {
if cols != 40 {
a.t.Errorf("expected cols to be set on asciicast")
}
buf := new(bytes.Buffer)
buf.ReadFrom(asciicast.Stdout)
stdout := buf.String()
stdout := string(frames[0].Data)
if stdout != "hello" {
a.t.Errorf("expected recorded stdout to be set on asciicast")
a.t.Errorf(`expected frame data "%v", got "%v"`, "hello", stdout)
}
stdout = string(frames[1].Data)
if stdout != "world" {
a.t.Errorf(`expected frame data "%v", got "%v"`, "world", stdout)
}
if a.err != nil {