diff --git a/commands/cli/parse.go b/commands/cli/parse.go index 193f077412d..463277e3d21 100644 --- a/commands/cli/parse.go +++ b/commands/cli/parse.go @@ -6,6 +6,7 @@ import ( "os" "runtime" "strings" + "syscall" cmds "github.com/ipfs/go-ipfs/commands" files "github.com/ipfs/go-ipfs/commands/files" @@ -47,7 +48,11 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c } req.SetArguments(stringArgs) - file := files.NewSliceFile("", fileArgs) + file, err := files.NewSliceFile("", nil, fileArgs) + if err != nil { + return req, cmd, path, err + } + req.SetFiles(file) err = cmd.CheckArguments(req) @@ -335,8 +340,16 @@ func appendStdinAsString(args []string, stdin *os.File) ([]string, *os.File, err func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]files.File, []string, error) { path := inputs[0] - file, err := os.Open(path) + file, err := os.OpenFile(path, (os.O_RDONLY | syscall.O_NOFOLLOW | syscall.O_NONBLOCK), 0) if err != nil { + errno, err2 := files.Errno(err) + if err2 == nil && errno == syscall.ELOOP { + arg, err := files.NewSymlink(path) + if err != nil { + return nil, nil, err + } + return append(args, arg), inputs[1:], nil + } return nil, nil, err } @@ -367,7 +380,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.NewReaderFile("", stdin, nil) + arg := files.NewReaderFile("", stdin, files.NewDummyRegularFileInfo("stdin")) return append(args, arg), nil } diff --git a/commands/files/file.go b/commands/files/file.go index f1196257d1b..66e916ab2b0 100644 --- a/commands/files/file.go +++ b/commands/files/file.go @@ -25,18 +25,16 @@ type File interface { // and false if the File is a normal file (and therefor supports calling `Read` and `Close`) IsDirectory() bool + // Stat returns an os.FileInfo structure describing the file. If + // there is an error it will be of type *PathError. + Stat() (fi os.FileInfo, err error) + // NextFile returns the next child file available (if the File is a directory). // It will return (nil, io.EOF) if no more files are available. // 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 diff --git a/commands/files/file_test.go b/commands/files/file_test.go index 01b7a9d02fd..7adedda6b37 100644 --- a/commands/files/file_test.go +++ b/commands/files/file_test.go @@ -17,7 +17,10 @@ func TestSliceFiles(t *testing.T) { } buf := make([]byte, 20) - sf := NewSliceFile(name, files) + sf, err := NewSliceFile(name, nil, files) + if err != nil { + t.Error("Failed to create a new SliceFile") + } if !sf.IsDirectory() { t.Error("SliceFile should always be a directory") diff --git a/commands/files/fileinfo.go b/commands/files/fileinfo.go new file mode 100644 index 00000000000..75bdf72733e --- /dev/null +++ b/commands/files/fileinfo.go @@ -0,0 +1,55 @@ +package files + +import ( + "os" + "time" +) + +type fileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func NewDummyRegularFileInfo(name string) (fi os.FileInfo) { + return &fileInfo{ + name: name, + size: 0, + mode: os.ModePerm, + modTime: time.Time{}, + } +} + +func NewDummyDirectoryFileInfo(name string) (fi os.FileInfo) { + return &fileInfo{ + name: name, + size: 0, + mode: os.ModePerm | os.ModeDir, + modTime: time.Time{}, + } +} + +func (fi *fileInfo) Name() string { + return fi.name +} + +func (fi *fileInfo) Size() int64 { + return fi.size +} + +func (fi *fileInfo) Mode() os.FileMode { + return fi.mode +} + +func (fi *fileInfo) ModTime() time.Time { + return fi.modTime +} + +func (fi *fileInfo) IsDir() bool { + return (fi.mode & os.ModeDir) != 0 +} + +func (fi *fileInfo) Sys() interface{} { + return nil +} diff --git a/commands/files/multipartfile.go b/commands/files/multipartfile.go index 844e0afa9c8..a32ae9d8528 100644 --- a/commands/files/multipartfile.go +++ b/commands/files/multipartfile.go @@ -1,10 +1,14 @@ package files import ( + "fmt" "mime" "mime/multipart" "net/http" "net/url" + "os" + "strconv" + "time" ) const ( @@ -22,6 +26,7 @@ type MultipartFile struct { Part *multipart.Part Reader *multipart.Reader Mediatype string + stat os.FileInfo } func NewFileFromPart(part *multipart.Part) (File, error) { @@ -47,6 +52,55 @@ func NewFileFromPart(part *multipart.Part) (File, error) { f.Reader = multipart.NewReader(part, boundary) } + name := f.FileName() + fileInfoString := part.Header.Get("File-Info") + if fileInfoString == "" { + if f.IsDirectory() { + f.stat = NewDummyDirectoryFileInfo(name) + } else { + f.stat = NewDummyRegularFileInfo(name) + } + } else { + version, params, err := mime.ParseMediaType(fileInfoString) + if version != "ipfs/v1" { + return nil, fmt.Errorf( + "unrecognized File-Info version: %s (%s)", version, fileInfoString) + } + sizeString, ok := params["size"] + if !ok { + return nil, fmt.Errorf( + "File-Info missing \"size\" parameter: %s", fileInfoString) + } + size, err := strconv.ParseInt(sizeString, 0, 64) + if err != nil { + return nil, err + } + modeString, ok := params["mode"] + if !ok { + return nil, fmt.Errorf( + "File-Info missing \"mode\" parameter: %s", fileInfoString) + } + mode, err := strconv.ParseUint(modeString, 0, 32) + if err != nil { + return nil, err + } + modTimeString, ok := params["mod-time"] + if !ok { + return nil, fmt.Errorf( + "File-Info missing \"mod-time\" parameter: %s", fileInfoString) + } + modTime, err := time.Parse(time.RFC3339Nano, modTimeString) + if err != nil { + return nil, err + } + f.stat = &fileInfo{ + name: name, + size: size, + mode: os.FileMode(mode), + modTime: modTime, + } + } + return f, nil } @@ -89,3 +143,7 @@ func (f *MultipartFile) Close() error { } return f.Part.Close() } + +func (f *MultipartFile) Stat() (fi os.FileInfo, err error) { + return f.stat, nil +} diff --git a/commands/files/readerfile.go b/commands/files/readerfile.go index 74104288167..996bef2597e 100644 --- a/commands/files/readerfile.go +++ b/commands/files/readerfile.go @@ -3,7 +3,9 @@ package files import ( "errors" "io" + "io/ioutil" "os" + "strings" ) // ReaderFile is a implementation of File created from an `io.Reader`. @@ -18,6 +20,20 @@ func NewReaderFile(filename string, reader io.ReadCloser, stat os.FileInfo) *Rea return &ReaderFile{filename, reader, stat} } +func NewSymlink(path string) (File, error) { + stat, err := os.Lstat(path) + if err != nil { + return nil, err + } + target, err := os.Readlink(path) + if err != nil { + return nil, err + } + reader := strings.NewReader(target) + readCloser := ioutil.NopCloser(reader) + return &ReaderFile{path, readCloser, stat}, nil +} + func (f *ReaderFile) IsDirectory() bool { return false } @@ -31,6 +47,9 @@ func (f *ReaderFile) FileName() string { } func (f *ReaderFile) Read(p []byte) (int, error) { + if f.reader == nil { + return 0, io.EOF + } return f.reader.Read(p) } @@ -38,8 +57,8 @@ func (f *ReaderFile) Close() error { return f.reader.Close() } -func (f *ReaderFile) Stat() os.FileInfo { - return f.stat +func (f *ReaderFile) Stat() (fi os.FileInfo, err error) { + return f.stat, nil } func (f *ReaderFile) Size() (int64, error) { diff --git a/commands/files/serialfile.go b/commands/files/serialfile.go index aeba01fa7ed..75341170ea8 100644 --- a/commands/files/serialfile.go +++ b/commands/files/serialfile.go @@ -82,8 +82,12 @@ func (f *serialFile) NextFile() (File, error) { // open the next file filePath := fp.Join(f.path, stat.Name()) - file, err := os.Open(filePath) + file, err := os.OpenFile(filePath, (os.O_RDONLY | syscall.O_NOFOLLOW | syscall.O_NONBLOCK), 0) if err != nil { + errno, err2 := Errno(err) + if err2 == nil && errno == syscall.ELOOP { + return NewSymlink(filePath) + } return nil, err } f.current = file @@ -115,8 +119,8 @@ func (f *serialFile) Close() error { return nil } -func (f *serialFile) Stat() os.FileInfo { - return f.stat +func (f *serialFile) Stat() (fi os.FileInfo, err error) { + return f.stat, nil } func (f *serialFile) Size() (int64, error) { @@ -148,3 +152,18 @@ func size(stat os.FileInfo, filename string) (int64, error) { } return output, nil } + +func Errno(err error) (syscall.Errno, error) { + if err == nil { + return 0, err + } + err2, ok := err.(*os.PathError) + if !ok { + return 0, err + } + errno, ok := err2.Err.(syscall.Errno) + if !ok { + return 0, err + } + return errno, nil +} diff --git a/commands/files/slicefile.go b/commands/files/slicefile.go index f6e2048120c..4047c11c9f2 100644 --- a/commands/files/slicefile.go +++ b/commands/files/slicefile.go @@ -3,6 +3,7 @@ package files import ( "errors" "io" + "os" ) // SliceFile implements File, and provides simple directory handling. @@ -11,11 +12,22 @@ import ( type SliceFile struct { filename string files []File + stat os.FileInfo n int } -func NewSliceFile(filename string, files []File) *SliceFile { - return &SliceFile{filename, files, 0} +func NewSliceFile(filename string, file *os.File, files []File) (f *SliceFile, err error) { + var stat os.FileInfo + if file == nil { + stat = NewDummyDirectoryFileInfo(filename) + } else { + stat, err = file.Stat() + if err != nil { + return nil, err + } + } + + return &SliceFile{filename, files, stat, 0}, nil } func (f *SliceFile) IsDirectory() bool { @@ -43,6 +55,10 @@ func (f *SliceFile) Close() error { return ErrNotReader } +func (f *SliceFile) Stat() (fi os.FileInfo, err error) { + return f.stat, nil +} + func (f *SliceFile) Peek(n int) File { return f.files[n] } diff --git a/commands/http/client.go b/commands/http/client.go index a34c89d1ae2..2419e0d04c7 100644 --- a/commands/http/client.go +++ b/commands/http/client.go @@ -206,6 +206,11 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error if err == io.EOF { close(outChan) + errorString := httpRes.Trailer.Get("Error") + if errorString != "" { + err = fmt.Errorf(errorString) + res.SetError(err, cmds.ErrNormal) + } return } outChan <- v diff --git a/commands/http/handler.go b/commands/http/handler.go index 774c5ca4a62..0b258bac63d 100644 --- a/commands/http/handler.go +++ b/commands/http/handler.go @@ -176,14 +176,14 @@ func (i internalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // w.WriteString(transferEncodingHeader + ": chunked\r\n") // w.Header().Set(channelHeader, "1") // w.WriteHeader(200) - err = copyChunks(applicationJson, w, out) + err = copyChunks(res, applicationJson, w, out) if err != nil { log.Debug(err) } return } - flushCopy(w, out) + flushCopy(nil, w, out) } func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -193,9 +193,9 @@ func (i Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // flushCopy Copies from an io.Reader to a http.ResponseWriter. // Flushes chunks over HTTP stream as they are read (if supported by transport). -func flushCopy(w http.ResponseWriter, out io.Reader) error { +func flushCopy(res cmds.Response, w http.ResponseWriter, out io.Reader) error { if _, ok := w.(http.Flusher); !ok { - return copyChunks("", w, out) + return copyChunks(res, "", w, out) } io.Copy(&flushResponse{w}, out) @@ -204,7 +204,7 @@ func flushCopy(w http.ResponseWriter, out io.Reader) error { // Copies from an io.Reader to a http.ResponseWriter. // Flushes chunks over HTTP stream as they are read (if supported by transport). -func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error { +func copyChunks(res cmds.Response, contentType string, w http.ResponseWriter, out io.Reader) error { hijacker, ok := w.(http.Hijacker) if !ok { return errors.New("Could not create hijacker") @@ -216,6 +216,9 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error defer conn.Close() writer.WriteString("HTTP/1.1 200 OK\r\n") + if res != nil { + writer.WriteString("Trailer: Error\r\n") + } if contentType != "" { writer.WriteString(contentTypeHeader + ": " + contentType + "\r\n") } @@ -248,7 +251,18 @@ func copyChunks(contentType string, w http.ResponseWriter, out io.Reader) error } } - writer.WriteString("0\r\n\r\n") + writer.WriteString("0\r\n") // eof + + if res != nil { + err := res.Error() + if err != nil { + t := http.Header{} + t.Set("Error", err.Error()) + t.Write(writer) + } + } + + writer.WriteString("\r\n") // end of trailers writer.Flush() return nil diff --git a/commands/http/multifilereader.go b/commands/http/multifilereader.go index 711117896de..7940fb7ed68 100644 --- a/commands/http/multifilereader.go +++ b/commands/http/multifilereader.go @@ -4,10 +4,12 @@ import ( "bytes" "fmt" "io" + "mime" "mime/multipart" "net/textproto" "net/url" "sync" + "time" files "github.com/ipfs/go-ipfs/commands/files" ) @@ -90,7 +92,18 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { header.Set("Content-Type", "application/octet-stream") } - _, err := mfr.mpWriter.CreatePart(header) + stat, err := file.Stat() + if err != nil { + return 0, err + } + + params := map[string]string{ + "mode": fmt.Sprintf("%#o", stat.Mode()), + "size": fmt.Sprintf("%d", stat.Size()), + "mod-time": stat.ModTime().Format(time.RFC3339Nano), + } + header.Set("File-Info", mime.FormatMediaType("ipfs/v1", params)) + _, err = mfr.mpWriter.CreatePart(header) if err != nil { return 0, err } diff --git a/commands/http/multifilereader_test.go b/commands/http/multifilereader_test.go index f1c7aa94dcd..9ee19cc8961 100644 --- a/commands/http/multifilereader_test.go +++ b/commands/http/multifilereader_test.go @@ -12,15 +12,25 @@ import ( func TestOutput(t *testing.T) { text := "Some text! :)" + + sf, err := files.NewSliceFile("boop", nil, []files.File{ + files.NewReaderFile("boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep")), nil), + files.NewReaderFile("boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop")), nil), + }) + if err != nil { + t.Error("Failed to create a new SliceFile") + } + fileset := []files.File{ 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), - }), + sf, files.NewReaderFile("beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil), } - sf := files.NewSliceFile("", fileset) + + sf, err = files.NewSliceFile("", nil, fileset) + if err != nil { + t.Error("Failed to create a new SliceFile") + } buf := make([]byte, 20) // testing output by reading it with the go stdlib "mime/multipart" Reader diff --git a/core/commands/add.go b/core/commands/add.go index 2aad4a3b95a..160b8d4d80f 100644 --- a/core/commands/add.go +++ b/core/commands/add.go @@ -250,6 +250,16 @@ func addFile(n *core.IpfsNode, file files.File, out chan interface{}, progress b return addDir(n, file, out, progress, useTrickle) } + stat, err := file.Stat() + if err != nil { + return nil, err + } + + mode := stat.Mode() + if !mode.IsRegular() { + return nil, fmt.Errorf("`%s` is an unknown type", file.FileName()) + } + // if the progress flag was specified, wrap the file so that we can send // progress updates to the client (over the output channel) var reader io.Reader = file diff --git a/core/coreunix/add.go b/core/coreunix/add.go index c1b9586c7a7..c9bc9898de6 100644 --- a/core/coreunix/add.go +++ b/core/coreunix/add.go @@ -79,8 +79,13 @@ func AddR(n *core.IpfsNode, root string) (key string, err error) { // Returns the path of the added file ("/filename"), the DAG node of // the directory, and and error if any. func AddWrapped(n *core.IpfsNode, r io.Reader, filename string) (string, *merkledag.Node, error) { - file := files.NewReaderFile(filename, ioutil.NopCloser(r), nil) - dir := files.NewSliceFile("", []files.File{file}) + file := files.NewReaderFile( + filename, ioutil.NopCloser(r), files.NewDummyRegularFileInfo(filename)) + dir, err := files.NewSliceFile("", nil, []files.File{file}) + if err != nil { + return "", nil, err + } + dagnode, err := addDir(n, dir) if err != nil { return "", nil, err diff --git a/importer/importer.go b/importer/importer.go index f499b190a4f..cdc5067f7d5 100644 --- a/importer/importer.go +++ b/importer/importer.go @@ -30,6 +30,12 @@ func BuildDagFromFile(fpath string, ds dag.DAGService, mp pin.ManualPinner) (*da return nil, fmt.Errorf("`%s` is a directory", fpath) } + mode := stat.Mode() + + if !mode.IsRegular() { + return nil, fmt.Errorf("`%s` is an unknown type", fpath) + } + f, err := os.Open(fpath) if err != nil { return nil, err diff --git a/test/sharness/t0040-add-and-cat.sh b/test/sharness/t0040-add-and-cat.sh index 03cdb75d4df..d6d03ca1835 100755 --- a/test/sharness/t0040-add-and-cat.sh +++ b/test/sharness/t0040-add-and-cat.sh @@ -264,6 +264,21 @@ test_expect_success "ipfs add -w output looks good" ' test_cmp expected actual ' +test_expect_success "useful error message when adding a named pipe" ' + mkfifo named-pipe + test_expect_code 1 ipfs add named-pipe 2>named-pipe-error + echo "Error: \`named-pipe\` is an unknown type" >named-pipe-error-expected + test_cmp named-pipe-error-expected named-pipe-error +' + +test_expect_success "useful error message when recursively adding a named pipe" ' + mkdir named-pipe-dir + mkfifo named-pipe-dir/named-pipe + test_expect_code 1 ipfs add -r named-pipe-dir 2>named-pipe-dir-error + echo "Error: \`named-pipe-dir/named-pipe\` is an unknown type" >named-pipe-dir-error-expected + test_cmp named-pipe-dir-error-expected named-pipe-dir-error +' + test_kill_ipfs_daemon test_done diff --git a/test/sharness/t0041-add-cat-offline.sh b/test/sharness/t0041-add-cat-offline.sh index cb370eec009..674e65284c9 100755 --- a/test/sharness/t0041-add-cat-offline.sh +++ b/test/sharness/t0041-add-cat-offline.sh @@ -31,4 +31,19 @@ test_expect_success "output looks good" ' test_cmp afile out_2 ' +test_expect_success "useful error message when adding a named pipe" ' + mkfifo named-pipe + test_expect_code 1 ipfs add named-pipe 2>named-pipe-error + echo "Error: \`named-pipe\` is an unknown type" >named-pipe-error-expected + test_cmp named-pipe-error-expected named-pipe-error +' + +test_expect_success "useful error message when recursively adding a named pipe" ' + mkdir named-pipe-dir + mkfifo named-pipe-dir/named-pipe + test_expect_code 1 ipfs add -r named-pipe-dir 2>named-pipe-dir-error + echo "Error: \`named-pipe-dir/named-pipe\` is an unknown type" >named-pipe-dir-error-expected + test_cmp named-pipe-dir-error-expected named-pipe-dir-error +' + test_done