Commit 42c9a8a3 authored by Juan Batiz-Benet's avatar Juan Batiz-Benet

Merge pull request #613 from jbenet/progress-bars

Progress Bars
parents ac37a321 8ae2b2aa
......@@ -64,7 +64,7 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c
}
req.SetArguments(stringArgs)
file := &files.SliceFile{"", fileArgs}
file := files.NewSliceFile("", fileArgs)
req.SetFiles(file)
err = cmd.CheckArguments(req)
......@@ -298,7 +298,7 @@ func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recur
}
func appendStdinAsFile(args []files.File, stdin *os.File) ([]files.File, *os.File) {
arg := &files.ReaderFile{"", stdin}
arg := files.NewReaderFile("", stdin, nil)
return append(args, arg), nil
}
......
......@@ -14,7 +14,7 @@ var log = u.Logger("command")
// Function is the type of function that Commands use.
// It reads from the Request, and writes results to the Response.
type Function func(Request) (interface{}, error)
type Function func(Request, Response)
// Marshaler is a function that takes in a Response, and returns an io.Reader
// (or an error on failure)
......@@ -40,18 +40,14 @@ type HelpText struct {
Subcommands string // overrides SUBCOMMANDS section
}
// TODO: check Argument definitions when creating a Command
// (might need to use a Command constructor)
// * make sure any variadic args are at the end
// * make sure there aren't duplicate names
// * make sure optional arguments aren't followed by required arguments
// Command is a runnable command, with input arguments and options (flags).
// It can also have Subcommands, to group units of work into sets.
type Command struct {
Options []Option
Arguments []Argument
PreRun func(req Request) error
Run Function
PostRun Function
Marshalers map[EncodingType]Marshaler
Helptext HelpText
......@@ -99,21 +95,12 @@ func (c *Command) Call(req Request) Response {
return res
}
output, err := cmd.Run(req)
if err != nil {
// if returned error is a commands.Error, use its error code
// otherwise, just default the code to ErrNormal
switch e := err.(type) {
case *Error:
res.SetError(e, e.Code)
case Error:
res.SetError(e, e.Code)
default:
res.SetError(err, ErrNormal)
}
cmd.Run(req, res)
if res.Error() != nil {
return res
}
output := res.Output()
isChan := false
actualType := reflect.TypeOf(output)
if actualType != nil {
......@@ -144,7 +131,6 @@ func (c *Command) Call(req Request) Response {
}
}
res.SetOutput(output)
return res
}
......
......@@ -2,8 +2,8 @@ package commands
import "testing"
func noop(req Request) (interface{}, error) {
return nil, nil
func noop(req Request, res Response) {
return
}
func TestOptionValidation(t *testing.T) {
......
......@@ -3,6 +3,7 @@ package files
import (
"errors"
"io"
"os"
)
var (
......@@ -29,3 +30,22 @@ type File interface {
// If the file is a regular file (not a directory), NextFile will return a non-nil error.
NextFile() (File, error)
}
type StatFile interface {
File
Stat() os.FileInfo
}
type PeekFile interface {
SizeFile
Peek(n int) File
Length() int
}
type SizeFile interface {
File
Size() (int64, error)
}
......@@ -11,13 +11,13 @@ import (
func TestSliceFiles(t *testing.T) {
name := "testname"
files := []File{
&ReaderFile{"file.txt", ioutil.NopCloser(strings.NewReader("Some text!\n"))},
&ReaderFile{"beep.txt", ioutil.NopCloser(strings.NewReader("beep"))},
&ReaderFile{"boop.txt", ioutil.NopCloser(strings.NewReader("boop"))},
NewReaderFile("file.txt", ioutil.NopCloser(strings.NewReader("Some text!\n")), nil),
NewReaderFile("beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil),
NewReaderFile("boop.txt", ioutil.NopCloser(strings.NewReader("boop")), nil),
}
buf := make([]byte, 20)
sf := &SliceFile{name, files}
sf := NewSliceFile(name, files)
if !sf.IsDirectory() {
t.Error("SliceFile should always be a directory")
......@@ -55,7 +55,7 @@ func TestSliceFiles(t *testing.T) {
func TestReaderFiles(t *testing.T) {
message := "beep boop"
rf := &ReaderFile{"file.txt", ioutil.NopCloser(strings.NewReader(message))}
rf := NewReaderFile("file.txt", ioutil.NopCloser(strings.NewReader(message)), nil)
buf := make([]byte, len(message))
if rf.IsDirectory() {
......
package files
import "io"
import (
"errors"
"io"
"os"
)
// ReaderFile is a implementation of File created from an `io.Reader`.
// ReaderFiles are never directories, and can be read from and closed.
type ReaderFile struct {
Filename string
Reader io.ReadCloser
filename string
reader io.ReadCloser
stat os.FileInfo
}
func NewReaderFile(filename string, reader io.ReadCloser, stat os.FileInfo) *ReaderFile {
return &ReaderFile{filename, reader, stat}
}
func (f *ReaderFile) IsDirectory() bool {
......@@ -18,13 +27,24 @@ func (f *ReaderFile) NextFile() (File, error) {
}
func (f *ReaderFile) FileName() string {
return f.Filename
return f.filename
}
func (f *ReaderFile) Read(p []byte) (int, error) {
return f.Reader.Read(p)
return f.reader.Read(p)
}
func (f *ReaderFile) Close() error {
return f.Reader.Close()
return f.reader.Close()
}
func (f *ReaderFile) Stat() os.FileInfo {
return f.stat
}
func (f *ReaderFile) Size() (int64, error) {
if f.stat == nil {
return 0, errors.New("File size unknown")
}
return f.stat.Size(), nil
}
......@@ -20,6 +20,7 @@ func (es sortFIByName) Less(i, j int) bool { return es[i].Name() < es[j].Name()
type serialFile struct {
path string
files []os.FileInfo
stat os.FileInfo
current *os.File
}
......@@ -35,7 +36,7 @@ func NewSerialFile(path string, file *os.File) (File, error) {
func newSerialFile(path string, file *os.File, stat os.FileInfo) (File, error) {
// for non-directories, return a ReaderFile
if !stat.IsDir() {
return &ReaderFile{path, file}, nil
return &ReaderFile{path, file, stat}, nil
}
// for directories, stat all of the contents first, so we know what files to
......@@ -55,7 +56,7 @@ func newSerialFile(path string, file *os.File, stat os.FileInfo) (File, error) {
// make sure contents are sorted so -- repeatably -- we get the same inputs.
sort.Sort(sortFIByName(contents))
return &serialFile{path, contents, nil}, nil
return &serialFile{path, contents, stat, nil}, nil
}
func (f *serialFile) IsDirectory() bool {
......@@ -113,3 +114,37 @@ func (f *serialFile) Close() error {
return nil
}
func (f *serialFile) Stat() os.FileInfo {
return f.stat
}
func (f *serialFile) Size() (int64, error) {
return size(f.stat, f.FileName())
}
func size(stat os.FileInfo, filename string) (int64, error) {
if !stat.IsDir() {
return stat.Size(), nil
}
file, err := os.Open(filename)
if err != nil {
return 0, err
}
files, err := file.Readdir(0)
if err != nil {
return 0, err
}
file.Close()
var output int64
for _, child := range files {
s, err := size(child, fp.Join(filename, child.Name()))
if err != nil {
return 0, err
}
output += s
}
return output, nil
}
package files
import "io"
import (
"errors"
"io"
)
// SliceFile implements File, and provides simple directory handling.
// It contains children files, and is created from a `[]File`.
// SliceFiles are always directories, and can't be read from or closed.
type SliceFile struct {
Filename string
Files []File
filename string
files []File
n int
}
func NewSliceFile(filename string, files []File) *SliceFile {
return &SliceFile{filename, files, 0}
}
func (f *SliceFile) IsDirectory() bool {
......@@ -15,16 +23,16 @@ func (f *SliceFile) IsDirectory() bool {
}
func (f *SliceFile) NextFile() (File, error) {
if len(f.Files) == 0 {
if f.n >= len(f.files) {
return nil, io.EOF
}
file := f.Files[0]
f.Files = f.Files[1:]
file := f.files[f.n]
f.n++
return file, nil
}
func (f *SliceFile) FileName() string {
return f.Filename
return f.filename
}
func (f *SliceFile) Read(p []byte) (int, error) {
......@@ -34,3 +42,30 @@ func (f *SliceFile) Read(p []byte) (int, error) {
func (f *SliceFile) Close() error {
return ErrNotReader
}
func (f *SliceFile) Peek(n int) File {
return f.files[n]
}
func (f *SliceFile) Length() int {
return len(f.files)
}
func (f *SliceFile) Size() (int64, error) {
var size int64
for _, file := range f.files {
sizeFile, ok := file.(SizeFile)
if !ok {
return 0, errors.New("Could not get size of child file")
}
s, err := sizeFile.Size()
if err != nil {
return 0, err
}
size += s
}
return size, nil
}
......@@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
cmds "github.com/jbenet/go-ipfs/commands"
......@@ -137,9 +138,18 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
var err error
res := cmds.NewResponse(req)
contentType := httpRes.Header["Content-Type"][0]
contentType := httpRes.Header.Get(contentTypeHeader)
contentType = strings.Split(contentType, ";")[0]
lengthHeader := httpRes.Header.Get(contentLengthHeader)
if len(lengthHeader) > 0 {
length, err := strconv.ParseUint(lengthHeader, 10, 64)
if err != nil {
return nil, err
}
res.SetLength(length)
}
if len(httpRes.Header.Get(streamHeader)) > 0 {
// if output is a stream, we can just use the body reader
res.SetOutput(httpRes.Body)
......
......@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"strconv"
context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context"
......@@ -92,6 +93,11 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(contentTypeHeader, mime)
}
// set the Content-Length from the response length
if res.Length() > 0 {
w.Header().Set(contentLengthHeader, strconv.FormatUint(res.Length(), 10))
}
// if response contains an error, write an HTTP error status code
if e := res.Error(); e != nil {
if e.Code == cmds.ErrClient {
......
......@@ -13,14 +13,14 @@ import (
func TestOutput(t *testing.T) {
text := "Some text! :)"
fileset := []files.File{
&files.ReaderFile{"file.txt", ioutil.NopCloser(strings.NewReader(text))},
&files.SliceFile{"boop", []files.File{
&files.ReaderFile{"boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep"))},
&files.ReaderFile{"boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop"))},
}},
&files.ReaderFile{"beep.txt", ioutil.NopCloser(strings.NewReader("beep"))},
}
sf := &files.SliceFile{"", fileset}
files.NewReaderFile("file.txt", ioutil.NopCloser(strings.NewReader(text)), nil),
files.NewSliceFile("boop", []files.File{
files.NewReaderFile("boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep")), nil),
files.NewReaderFile("boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop")), nil),
}),
files.NewReaderFile("beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil),
}
sf := files.NewSliceFile("", fileset)
buf := make([]byte, 20)
// testing output by reading it with the go stdlib "mime/multipart" Reader
......
......@@ -3,6 +3,8 @@ package commands
import (
"errors"
"fmt"
"io"
"os"
"reflect"
"strconv"
......@@ -77,6 +79,8 @@ type Request interface {
Context() *Context
SetContext(Context)
Command() *Command
Values() map[string]interface{}
Stdin() io.Reader
ConvertOptions() error
}
......@@ -89,6 +93,8 @@ type request struct {
cmd *Command
ctx Context
optionDefs map[string]Option
values map[string]interface{}
stdin io.Reader
}
// Path returns the command path of this request
......@@ -208,6 +214,14 @@ var converters = map[reflect.Kind]converter{
},
}
func (r *request) Values() map[string]interface{} {
return r.values
}
func (r *request) Stdin() io.Reader {
return r.stdin
}
func (r *request) ConvertOptions() error {
for k, v := range r.options {
opt, ok := r.optionDefs[k]
......@@ -275,7 +289,8 @@ func NewRequest(path []string, opts optMap, args []string, file files.File, cmd
}
ctx := Context{Context: context.TODO()}
req := &request{path, opts, args, file, cmd, ctx, optDefs}
values := make(map[string]interface{})
req := &request{path, opts, args, file, cmd, ctx, optDefs, values, os.Stdin}
err := req.ConvertOptions()
if err != nil {
return nil, err
......
......@@ -6,6 +6,7 @@ import (
"encoding/xml"
"fmt"
"io"
"os"
"strings"
)
......@@ -95,19 +96,30 @@ type Response interface {
SetOutput(interface{})
Output() interface{}
// Sets/Returns the length of the output
SetLength(uint64)
Length() uint64
// Marshal marshals out the response into a buffer. It uses the EncodingType
// on the Request to chose a Marshaler (Codec).
Marshal() (io.Reader, error)
// Gets a io.Reader that reads the marshalled output
Reader() (io.Reader, error)
// Gets Stdout and Stderr, for writing to console without using SetOutput
Stdout() io.Writer
Stderr() io.Writer
}
type response struct {
req Request
err *Error
value interface{}
out io.Reader
req Request
err *Error
value interface{}
out io.Reader
length uint64
stdout io.Writer
stderr io.Writer
}
func (r *response) Request() Request {
......@@ -122,6 +134,14 @@ func (r *response) SetOutput(v interface{}) {
r.value = v
}
func (r *response) Length() uint64 {
return r.length
}
func (r *response) SetLength(l uint64) {
r.length = l
}
func (r *response) Error() *Error {
return r.err
}
......@@ -193,7 +213,19 @@ func (r *response) Reader() (io.Reader, error) {
return r.out, nil
}
func (r *response) Stdout() io.Writer {
return r.stdout
}
func (r *response) Stderr() io.Writer {
return r.stderr
}
// NewResponse returns a response to match given Request
func NewResponse(req Request) Response {
return &response{req: req}
return &response{
req: req,
stdout: os.Stdout,
stderr: os.Stderr,
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment