From 7a579bbf79c8cedfb063e35197982d85a2cf30d1 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 13 Jan 2015 10:16:15 -0800 Subject: [PATCH 1/5] commands: Moved files code into 'commands/files' subpackage --- commands/cli/parse.go | 23 ++-- commands/file.go | 176 ------------------------------ commands/files/file.go | 31 ++++++ commands/{ => files}/file_test.go | 2 +- commands/files/multipartfile.go | 85 +++++++++++++++ commands/files/readerfile.go | 34 ++++++ commands/files/slicefile.go | 36 ++++++ commands/http/multifilereader.go | 6 +- commands/http/parse.go | 5 +- commands/request.go | 13 ++- core/commands/add.go | 5 +- 11 files changed, 215 insertions(+), 201 deletions(-) delete mode 100644 commands/file.go create mode 100644 commands/files/file.go rename commands/{ => files}/file_test.go (99%) create mode 100644 commands/files/multipartfile.go create mode 100644 commands/files/readerfile.go create mode 100644 commands/files/slicefile.go diff --git a/commands/cli/parse.go b/commands/cli/parse.go index ee6b1656d..d0f84de02 100644 --- a/commands/cli/parse.go +++ b/commands/cli/parse.go @@ -11,6 +11,7 @@ import ( "strings" cmds "github.com/jbenet/go-ipfs/commands" + cmdsFiles "github.com/jbenet/go-ipfs/commands/files" u "github.com/jbenet/go-ipfs/util" ) @@ -65,7 +66,7 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c } req.SetArguments(stringArgs) - file := &cmds.SliceFile{"", fileArgs} + file := &cmdsFiles.SliceFile{"", fileArgs} req.SetFiles(file) err = cmd.CheckArguments(req) @@ -139,7 +140,7 @@ func parseOptions(input []string) (map[string]interface{}, []string, error) { return opts, args, nil } -func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmds.File, error) { +func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmdsFiles.File, error) { // ignore stdin on Windows if runtime.GOOS == "windows" { stdin = nil @@ -176,7 +177,7 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi } stringArgs := make([]string, 0, numInputs) - fileArgs := make([]cmds.File, 0, numInputs) + fileArgs := make([]cmdsFiles.File, 0, numInputs) argDefIndex := 0 // the index of the current argument definition for i := 0; i < numInputs; i++ { @@ -263,7 +264,7 @@ func appendStdinAsString(args []string, stdin *os.File) ([]string, *os.File, err return append(args, buf.String()), nil, nil } -func appendFile(args []cmds.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]cmds.File, []string, error) { +func appendFile(args []cmdsFiles.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]cmdsFiles.File, []string, error) { path := inputs[0] file, err := os.Open(path) @@ -297,13 +298,13 @@ func appendFile(args []cmds.File, inputs []string, argDef *cmds.Argument, recurs return append(args, arg), inputs[1:], nil } -func appendStdinAsFile(args []cmds.File, stdin *os.File) ([]cmds.File, *os.File) { - arg := &cmds.ReaderFile{"", stdin} +func appendStdinAsFile(args []cmdsFiles.File, stdin *os.File) ([]cmdsFiles.File, *os.File) { + arg := &cmdsFiles.ReaderFile{"", stdin} return append(args, arg), nil } -// recursively get file or directory contents as a cmds.File -func openPath(file *os.File, path string) (cmds.File, error) { +// recursively get file or directory contents as a cmdsFiles.File +func openPath(file *os.File, path string) (cmdsFiles.File, error) { stat, err := file.Stat() if err != nil { return nil, err @@ -311,7 +312,7 @@ func openPath(file *os.File, path string) (cmds.File, error) { // for non-directories, return a ReaderFile if !stat.IsDir() { - return &cmds.ReaderFile{path, file}, nil + return &cmdsFiles.ReaderFile{path, file}, nil } // for directories, recursively iterate though children then return as a SliceFile @@ -323,7 +324,7 @@ func openPath(file *os.File, path string) (cmds.File, error) { // make sure contents are sorted so -- repeatably -- we get the same inputs. sort.Sort(sortFIByName(contents)) - files := make([]cmds.File, 0, len(contents)) + files := make([]cmdsFiles.File, 0, len(contents)) for _, child := range contents { childPath := fp.Join(path, child.Name()) childFile, err := os.Open(childPath) @@ -339,7 +340,7 @@ func openPath(file *os.File, path string) (cmds.File, error) { files = append(files, f) } - return &cmds.SliceFile{path, files}, nil + return &cmdsFiles.SliceFile{path, files}, nil } // isTerminal returns true if stdin is a Stdin pipe (e.g. `cat file | ipfs`), diff --git a/commands/file.go b/commands/file.go deleted file mode 100644 index 466b1d842..000000000 --- a/commands/file.go +++ /dev/null @@ -1,176 +0,0 @@ -package commands - -import ( - "errors" - "io" - "mime" - "mime/multipart" - "net/http" -) - -const ( - multipartFormdataType = "multipart/form-data" - multipartMixedType = "multipart/mixed" - - contentTypeHeader = "Content-Type" -) - -var ( - ErrNotDirectory = errors.New("Couln't call NextFile(), this isn't a directory") - ErrNotReader = errors.New("This file is a directory, can't use Reader functions") -) - -// File is an interface that provides functionality for handling files/directories -// as values that can be supplied to commands. For directories, child files are -// accessed serially by calling `NextFile()`. -type File interface { - // Files implement ReadCloser, but can only be read from or closed if they are not directories - io.ReadCloser - - // FileName returns a full filename path associated with this file - FileName() string - - // IsDirectory returns true if the File is a directory (and therefore supports calling `NextFile`) - // and false if the File is a normal file (and therefor supports calling `Read` and `Close`) - IsDirectory() bool - - // 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) -} - -// MultipartFile implements File, and is created from a `multipart.Part`. -// It can be either a directory or file (checked by calling `IsDirectory()`). -type MultipartFile struct { - File - - Part *multipart.Part - Reader *multipart.Reader - Mediatype string -} - -func NewFileFromPart(part *multipart.Part) (File, error) { - f := &MultipartFile{ - Part: part, - } - - contentType := part.Header.Get(contentTypeHeader) - - var params map[string]string - var err error - f.Mediatype, params, err = mime.ParseMediaType(contentType) - if err != nil { - return nil, err - } - - if f.IsDirectory() { - boundary, found := params["boundary"] - if !found { - return nil, http.ErrMissingBoundary - } - - f.Reader = multipart.NewReader(part, boundary) - } - - return f, nil -} - -func (f *MultipartFile) IsDirectory() bool { - return f.Mediatype == multipartFormdataType || f.Mediatype == multipartMixedType -} - -func (f *MultipartFile) NextFile() (File, error) { - if !f.IsDirectory() { - return nil, ErrNotDirectory - } - - part, err := f.Reader.NextPart() - if err != nil { - return nil, err - } - - return NewFileFromPart(part) -} - -func (f *MultipartFile) FileName() string { - return f.Part.FileName() -} - -func (f *MultipartFile) Read(p []byte) (int, error) { - if f.IsDirectory() { - return 0, ErrNotReader - } - return f.Part.Read(p) -} - -func (f *MultipartFile) Close() error { - if f.IsDirectory() { - return ErrNotReader - } - return f.Part.Close() -} - -// 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 -} - -func (f *SliceFile) IsDirectory() bool { - return true -} - -func (f *SliceFile) NextFile() (File, error) { - if len(f.Files) == 0 { - return nil, io.EOF - } - file := f.Files[0] - f.Files = f.Files[1:] - return file, nil -} - -func (f *SliceFile) FileName() string { - return f.Filename -} - -func (f *SliceFile) Read(p []byte) (int, error) { - return 0, ErrNotReader -} - -func (f *SliceFile) Close() error { - return ErrNotReader -} - -// 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.Reader -} - -func (f *ReaderFile) IsDirectory() bool { - return false -} - -func (f *ReaderFile) NextFile() (File, error) { - return nil, ErrNotDirectory -} - -func (f *ReaderFile) FileName() string { - return f.Filename -} - -func (f *ReaderFile) Read(p []byte) (int, error) { - return f.Reader.Read(p) -} - -func (f *ReaderFile) Close() error { - closer, ok := f.Reader.(io.Closer) - if !ok { - return nil - } - return closer.Close() -} diff --git a/commands/files/file.go b/commands/files/file.go new file mode 100644 index 000000000..9e9b043a1 --- /dev/null +++ b/commands/files/file.go @@ -0,0 +1,31 @@ +package files + +import ( + "errors" + "io" +) + +var ( + ErrNotDirectory = errors.New("Couln't call NextFile(), this isn't a directory") + ErrNotReader = errors.New("This file is a directory, can't use Reader functions") +) + +// File is an interface that provides functionality for handling files/directories +// as values that can be supplied to commands. For directories, child files are +// accessed serially by calling `NextFile()`. +type File interface { + // Files implement ReadCloser, but can only be read from or closed if they are not directories + io.ReadCloser + + // FileName returns a full filename path associated with this file + FileName() string + + // IsDirectory returns true if the File is a directory (and therefore supports calling `NextFile`) + // and false if the File is a normal file (and therefor supports calling `Read` and `Close`) + IsDirectory() bool + + // 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) +} diff --git a/commands/file_test.go b/commands/files/file_test.go similarity index 99% rename from commands/file_test.go rename to commands/files/file_test.go index 19c8f587b..1ade078b4 100644 --- a/commands/file_test.go +++ b/commands/files/file_test.go @@ -1,4 +1,4 @@ -package commands +package files import ( "io" diff --git a/commands/files/multipartfile.go b/commands/files/multipartfile.go new file mode 100644 index 000000000..4594b859b --- /dev/null +++ b/commands/files/multipartfile.go @@ -0,0 +1,85 @@ +package files + +import ( + "mime" + "mime/multipart" + "net/http" +) + +const ( + multipartFormdataType = "multipart/form-data" + multipartMixedType = "multipart/mixed" + + contentTypeHeader = "Content-Type" +) + +// MultipartFile implements File, and is created from a `multipart.Part`. +// It can be either a directory or file (checked by calling `IsDirectory()`). +type MultipartFile struct { + File + + Part *multipart.Part + Reader *multipart.Reader + Mediatype string +} + +func NewFileFromPart(part *multipart.Part) (File, error) { + f := &MultipartFile{ + Part: part, + } + + contentType := part.Header.Get(contentTypeHeader) + + var params map[string]string + var err error + f.Mediatype, params, err = mime.ParseMediaType(contentType) + if err != nil { + return nil, err + } + + if f.IsDirectory() { + boundary, found := params["boundary"] + if !found { + return nil, http.ErrMissingBoundary + } + + f.Reader = multipart.NewReader(part, boundary) + } + + return f, nil +} + +func (f *MultipartFile) IsDirectory() bool { + return f.Mediatype == multipartFormdataType || f.Mediatype == multipartMixedType +} + +func (f *MultipartFile) NextFile() (File, error) { + if !f.IsDirectory() { + return nil, ErrNotDirectory + } + + part, err := f.Reader.NextPart() + if err != nil { + return nil, err + } + + return NewFileFromPart(part) +} + +func (f *MultipartFile) FileName() string { + return f.Part.FileName() +} + +func (f *MultipartFile) Read(p []byte) (int, error) { + if f.IsDirectory() { + return 0, ErrNotReader + } + return f.Part.Read(p) +} + +func (f *MultipartFile) Close() error { + if f.IsDirectory() { + return ErrNotReader + } + return f.Part.Close() +} diff --git a/commands/files/readerfile.go b/commands/files/readerfile.go new file mode 100644 index 000000000..bb9d338b6 --- /dev/null +++ b/commands/files/readerfile.go @@ -0,0 +1,34 @@ +package files + +import "io" + +// 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.Reader +} + +func (f *ReaderFile) IsDirectory() bool { + return false +} + +func (f *ReaderFile) NextFile() (File, error) { + return nil, ErrNotDirectory +} + +func (f *ReaderFile) FileName() string { + return f.Filename +} + +func (f *ReaderFile) Read(p []byte) (int, error) { + return f.Reader.Read(p) +} + +func (f *ReaderFile) Close() error { + closer, ok := f.Reader.(io.Closer) + if !ok { + return nil + } + return closer.Close() +} diff --git a/commands/files/slicefile.go b/commands/files/slicefile.go new file mode 100644 index 000000000..e1035f2ce --- /dev/null +++ b/commands/files/slicefile.go @@ -0,0 +1,36 @@ +package files + +import "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 +} + +func (f *SliceFile) IsDirectory() bool { + return true +} + +func (f *SliceFile) NextFile() (File, error) { + if len(f.Files) == 0 { + return nil, io.EOF + } + file := f.Files[0] + f.Files = f.Files[1:] + return file, nil +} + +func (f *SliceFile) FileName() string { + return f.Filename +} + +func (f *SliceFile) Read(p []byte) (int, error) { + return 0, ErrNotReader +} + +func (f *SliceFile) Close() error { + return ErrNotReader +} diff --git a/commands/http/multifilereader.go b/commands/http/multifilereader.go index 00e6f18c1..37c26dca3 100644 --- a/commands/http/multifilereader.go +++ b/commands/http/multifilereader.go @@ -8,7 +8,7 @@ import ( "net/textproto" "sync" - cmds "github.com/jbenet/go-ipfs/commands" + files "github.com/jbenet/go-ipfs/commands/files" ) // MultiFileReader reads from a `commands.File` (which can be a directory of files @@ -16,7 +16,7 @@ import ( type MultiFileReader struct { io.Reader - files cmds.File + files files.File currentFile io.Reader buf bytes.Buffer mpWriter *multipart.Writer @@ -31,7 +31,7 @@ type MultiFileReader struct { // NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.File`. // If `form` is set to true, the multipart data will have a Content-Type of 'multipart/form-data', // if `form` is false, the Content-Type will be 'multipart/mixed'. -func NewMultiFileReader(file cmds.File, form bool) *MultiFileReader { +func NewMultiFileReader(file files.File, form bool) *MultiFileReader { mfr := &MultiFileReader{ files: file, form: form, diff --git a/commands/http/parse.go b/commands/http/parse.go index 49c3f052c..b74b1594e 100644 --- a/commands/http/parse.go +++ b/commands/http/parse.go @@ -8,6 +8,7 @@ import ( "strings" cmds "github.com/jbenet/go-ipfs/commands" + files "github.com/jbenet/go-ipfs/commands/files" ) // Parse parses the data in a http.Request and returns a command Request object @@ -94,9 +95,9 @@ func Parse(r *http.Request, root *cmds.Command) (cmds.Request, error) { contentType := r.Header.Get(contentTypeHeader) mediatype, _, _ := mime.ParseMediaType(contentType) - var f *cmds.MultipartFile + var f *files.MultipartFile if mediatype == "multipart/form-data" { - f = &cmds.MultipartFile{Mediatype: mediatype} + f = &files.MultipartFile{Mediatype: mediatype} f.Reader, err = r.MultipartReader() if err != nil { return nil, err diff --git a/commands/request.go b/commands/request.go index 8e23e30eb..ee3747442 100644 --- a/commands/request.go +++ b/commands/request.go @@ -8,6 +8,7 @@ import ( context "github.com/jbenet/go-ipfs/Godeps/_workspace/src/code.google.com/p/go.net/context" + "github.com/jbenet/go-ipfs/commands/files" "github.com/jbenet/go-ipfs/core" "github.com/jbenet/go-ipfs/repo/config" u "github.com/jbenet/go-ipfs/util" @@ -71,8 +72,8 @@ type Request interface { SetOptions(opts map[string]interface{}) error Arguments() []string SetArguments([]string) - Files() File - SetFiles(File) + Files() files.File + SetFiles(files.File) Context() *Context SetContext(Context) Command() *Command @@ -84,7 +85,7 @@ type request struct { path []string options optMap arguments []string - files File + files files.File cmd *Command ctx Context optionDefs map[string]Option @@ -159,11 +160,11 @@ func (r *request) SetArguments(args []string) { r.arguments = args } -func (r *request) Files() File { +func (r *request) Files() files.File { return r.files } -func (r *request) SetFiles(f File) { +func (r *request) SetFiles(f files.File) { r.files = f } @@ -259,7 +260,7 @@ func NewEmptyRequest() (Request, error) { // NewRequest returns a request initialized with given arguments // An non-nil error will be returned if the provided option values are invalid -func NewRequest(path []string, opts optMap, args []string, file File, cmd *Command, optDefs map[string]Option) (Request, error) { +func NewRequest(path []string, opts optMap, args []string, file files.File, cmd *Command, optDefs map[string]Option) (Request, error) { if path == nil { path = make([]string, 0) } diff --git a/core/commands/add.go b/core/commands/add.go index 50cf293bb..5da995f46 100644 --- a/core/commands/add.go +++ b/core/commands/add.go @@ -8,6 +8,7 @@ import ( "path" cmds "github.com/jbenet/go-ipfs/commands" + files "github.com/jbenet/go-ipfs/commands/files" core "github.com/jbenet/go-ipfs/core" importer "github.com/jbenet/go-ipfs/importer" "github.com/jbenet/go-ipfs/importer/chunk" @@ -138,7 +139,7 @@ func addNode(n *core.IpfsNode, node *dag.Node) error { return nil } -func addFile(n *core.IpfsNode, file cmds.File, out chan interface{}) (*dag.Node, error) { +func addFile(n *core.IpfsNode, file files.File, out chan interface{}) (*dag.Node, error) { if file.IsDirectory() { return addDir(n, file, out) } @@ -155,7 +156,7 @@ func addFile(n *core.IpfsNode, file cmds.File, out chan interface{}) (*dag.Node, return dns[len(dns)-1], nil // last dag node is the file. } -func addDir(n *core.IpfsNode, dir cmds.File, out chan interface{}) (*dag.Node, error) { +func addDir(n *core.IpfsNode, dir files.File, out chan interface{}) (*dag.Node, error) { log.Infof("adding directory: %s", dir.FileName()) tree := &dag.Node{Data: ft.FolderPBData()} From 9ba728532f78c0cbd5d431c274f504fc847d2576 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 13 Jan 2015 11:13:29 -0800 Subject: [PATCH 2/5] commands/files: Created SerialFile, which opens directory contents serially --- commands/cli/parse.go | 64 +++---------------- commands/files/serialfile.go | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 56 deletions(-) create mode 100644 commands/files/serialfile.go diff --git a/commands/cli/parse.go b/commands/cli/parse.go index d0f84de02..7e00d385c 100644 --- a/commands/cli/parse.go +++ b/commands/cli/parse.go @@ -5,13 +5,11 @@ import ( "errors" "fmt" "os" - fp "path" "runtime" - "sort" "strings" cmds "github.com/jbenet/go-ipfs/commands" - cmdsFiles "github.com/jbenet/go-ipfs/commands/files" + files "github.com/jbenet/go-ipfs/commands/files" u "github.com/jbenet/go-ipfs/util" ) @@ -66,7 +64,7 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c } req.SetArguments(stringArgs) - file := &cmdsFiles.SliceFile{"", fileArgs} + file := &files.SliceFile{"", fileArgs} req.SetFiles(file) err = cmd.CheckArguments(req) @@ -140,7 +138,7 @@ func parseOptions(input []string) (map[string]interface{}, []string, error) { return opts, args, nil } -func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []cmdsFiles.File, error) { +func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursive bool) ([]string, []files.File, error) { // ignore stdin on Windows if runtime.GOOS == "windows" { stdin = nil @@ -177,7 +175,7 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi } stringArgs := make([]string, 0, numInputs) - fileArgs := make([]cmdsFiles.File, 0, numInputs) + fileArgs := make([]files.File, 0, numInputs) argDefIndex := 0 // the index of the current argument definition for i := 0; i < numInputs; i++ { @@ -264,7 +262,7 @@ func appendStdinAsString(args []string, stdin *os.File) ([]string, *os.File, err return append(args, buf.String()), nil, nil } -func appendFile(args []cmdsFiles.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]cmdsFiles.File, []string, error) { +func appendFile(args []files.File, inputs []string, argDef *cmds.Argument, recursive bool) ([]files.File, []string, error) { path := inputs[0] file, err := os.Open(path) @@ -290,7 +288,7 @@ func appendFile(args []cmdsFiles.File, inputs []string, argDef *cmds.Argument, r } } - arg, err := openPath(file, path) + arg, err := files.NewSerialFile(path, file) if err != nil { return nil, nil, err } @@ -298,51 +296,11 @@ func appendFile(args []cmdsFiles.File, inputs []string, argDef *cmds.Argument, r return append(args, arg), inputs[1:], nil } -func appendStdinAsFile(args []cmdsFiles.File, stdin *os.File) ([]cmdsFiles.File, *os.File) { - arg := &cmdsFiles.ReaderFile{"", stdin} +func appendStdinAsFile(args []files.File, stdin *os.File) ([]files.File, *os.File) { + arg := &files.ReaderFile{"", stdin} return append(args, arg), nil } -// recursively get file or directory contents as a cmdsFiles.File -func openPath(file *os.File, path string) (cmdsFiles.File, error) { - stat, err := file.Stat() - if err != nil { - return nil, err - } - - // for non-directories, return a ReaderFile - if !stat.IsDir() { - return &cmdsFiles.ReaderFile{path, file}, nil - } - - // for directories, recursively iterate though children then return as a SliceFile - contents, err := file.Readdir(0) - if err != nil { - return nil, err - } - - // make sure contents are sorted so -- repeatably -- we get the same inputs. - sort.Sort(sortFIByName(contents)) - - files := make([]cmdsFiles.File, 0, len(contents)) - for _, child := range contents { - childPath := fp.Join(path, child.Name()) - childFile, err := os.Open(childPath) - if err != nil { - return nil, err - } - - f, err := openPath(childFile, childPath) - if err != nil { - return nil, err - } - - files = append(files, f) - } - - return &cmdsFiles.SliceFile{path, files}, nil -} - // isTerminal returns true if stdin is a Stdin pipe (e.g. `cat file | ipfs`), // and false otherwise (e.g. nothing is being piped in, so stdin is // coming from the terminal) @@ -355,9 +313,3 @@ func isTerminal(stdin *os.File) (bool, error) { // if stdin is a CharDevice, return true return ((stat.Mode() & os.ModeCharDevice) != 0), nil } - -type sortFIByName []os.FileInfo - -func (es sortFIByName) Len() int { return len(es) } -func (es sortFIByName) Swap(i, j int) { es[i], es[j] = es[j], es[i] } -func (es sortFIByName) Less(i, j int) bool { return es[i].Name() < es[j].Name() } diff --git a/commands/files/serialfile.go b/commands/files/serialfile.go new file mode 100644 index 000000000..21f3a9bb9 --- /dev/null +++ b/commands/files/serialfile.go @@ -0,0 +1,115 @@ +package files + +import ( + "io" + "os" + fp "path" + "sort" + "syscall" +) + +type sortFIByName []os.FileInfo + +func (es sortFIByName) Len() int { return len(es) } +func (es sortFIByName) Swap(i, j int) { es[i], es[j] = es[j], es[i] } +func (es sortFIByName) Less(i, j int) bool { return es[i].Name() < es[j].Name() } + +// serialFile implements File, and reads from a path on the OS filesystem. +// No more than one file will be opened at a time (directories will advance +// to the next file when NextFile() is called). +type serialFile struct { + path string + files []os.FileInfo + current *os.File +} + +func NewSerialFile(path string, file *os.File) (File, error) { + stat, err := file.Stat() + if err != nil { + return nil, err + } + + return newSerialFile(path, file, stat) +} + +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 + } + + // for directories, stat all of the contents first, so we know what files to + // open when NextFile() is called + contents, err := file.Readdir(0) + if err != nil { + return nil, err + } + + // we no longer need our root directory file (we already statted the contents), + // so close it + err = file.Close() + if err != nil { + return nil, err + } + + // make sure contents are sorted so -- repeatably -- we get the same inputs. + sort.Sort(sortFIByName(contents)) + + return &serialFile{path, contents, nil}, nil +} + +func (f *serialFile) IsDirectory() bool { + // non-directories get created as a ReaderFile, so serialFiles should only + // represent directories + return true +} + +func (f *serialFile) NextFile() (File, error) { + // if a file was opened previously, close it + err := f.Close() + if err != nil { + return nil, err + } + + // if there aren't any files left in the root directory, we're done + if len(f.files) == 0 { + return nil, io.EOF + } + + stat := f.files[0] + f.files = f.files[1:] + + // open the next file + filePath := fp.Join(f.path, stat.Name()) + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + f.current = file + + // recursively call the constructor on the next file + // if it's a regular file, we will open it as a ReaderFile + // if it's a directory, files in it will be opened serially + return newSerialFile(filePath, file, stat) +} + +func (f *serialFile) FileName() string { + return f.path +} + +func (f *serialFile) Read(p []byte) (int, error) { + return 0, ErrNotReader +} + +func (f *serialFile) Close() error { + // close the current file if there is one + if f.current != nil { + err := f.current.Close() + // ignore EINVAL error, the file might have already been closed + if err != nil && err != syscall.EINVAL { + return err + } + } + + return nil +} From a2b3402aa4c930f9c9004df623cb07033b4f0393 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 13 Jan 2015 11:45:31 -0800 Subject: [PATCH 3/5] commands/http: Fixed package import in tests --- commands/http/multifilereader_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/commands/http/multifilereader_test.go b/commands/http/multifilereader_test.go index fc0309e93..4822e8e6b 100644 --- a/commands/http/multifilereader_test.go +++ b/commands/http/multifilereader_test.go @@ -6,20 +6,20 @@ import ( "strings" "testing" - cmds "github.com/jbenet/go-ipfs/commands" + files "github.com/jbenet/go-ipfs/commands/files" ) func TestOutput(t *testing.T) { text := "Some text! :)" - files := []cmds.File{ - &cmds.ReaderFile{"file.txt", strings.NewReader(text)}, - &cmds.SliceFile{"boop", []cmds.File{ - &cmds.ReaderFile{"boop/a.txt", strings.NewReader("bleep")}, - &cmds.ReaderFile{"boop/b.txt", strings.NewReader("bloop")}, + fileset := []files.File{ + &files.ReaderFile{"file.txt", strings.NewReader(text)}, + &files.SliceFile{"boop", []files.File{ + &files.ReaderFile{"boop/a.txt", strings.NewReader("bleep")}, + &files.ReaderFile{"boop/b.txt", strings.NewReader("bloop")}, }}, - &cmds.ReaderFile{"beep.txt", strings.NewReader("beep")}, + &files.ReaderFile{"beep.txt", strings.NewReader("beep")}, } - sf := &cmds.SliceFile{"", files} + sf := &files.SliceFile{"", fileset} buf := make([]byte, 20) // testing output by reading it with the go stdlib "mime/multipart" Reader @@ -30,7 +30,7 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Error("Expected non-nil part, nil error") } - mpf, err := cmds.NewFileFromPart(part) + mpf, err := files.NewFileFromPart(part) if mpf == nil || err != nil { t.Error("Expected non-nil MultipartFile, nil error") } @@ -51,7 +51,7 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Error("Expected non-nil part, nil error") } - mpf, err = cmds.NewFileFromPart(part) + mpf, err = files.NewFileFromPart(part) if mpf == nil || err != nil { t.Error("Expected non-nil MultipartFile, nil error") } @@ -93,7 +93,7 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Error("Expected non-nil part, nil error") } - mpf, err = cmds.NewFileFromPart(part) + mpf, err = files.NewFileFromPart(part) if mpf == nil || err != nil { t.Error("Expected non-nil MultipartFile, nil error") } From f8347f925cbc263a997af246ad105813e7ff29ca Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 13 Jan 2015 22:28:44 -0800 Subject: [PATCH 4/5] commands/files: Made ReaderFile take ReadClosers instead of trying to cast reader to Closer --- commands/files/readerfile.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/commands/files/readerfile.go b/commands/files/readerfile.go index bb9d338b6..af88562fd 100644 --- a/commands/files/readerfile.go +++ b/commands/files/readerfile.go @@ -6,7 +6,7 @@ import "io" // ReaderFiles are never directories, and can be read from and closed. type ReaderFile struct { Filename string - Reader io.Reader + Reader io.ReadCloser } func (f *ReaderFile) IsDirectory() bool { @@ -26,9 +26,5 @@ func (f *ReaderFile) Read(p []byte) (int, error) { } func (f *ReaderFile) Close() error { - closer, ok := f.Reader.(io.Closer) - if !ok { - return nil - } - return closer.Close() + return f.Reader.Close() } From dbb5118d76e6c62529c18e1212741c389e20943c Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Tue, 13 Jan 2015 22:36:04 -0800 Subject: [PATCH 5/5] commands/files: Fixed tests --- commands/files/file_test.go | 9 +++++---- commands/http/multifilereader_test.go | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/commands/files/file_test.go b/commands/files/file_test.go index 1ade078b4..a9499fb78 100644 --- a/commands/files/file_test.go +++ b/commands/files/file_test.go @@ -2,6 +2,7 @@ package files import ( "io" + "io/ioutil" "mime/multipart" "strings" "testing" @@ -10,9 +11,9 @@ import ( func TestSliceFiles(t *testing.T) { name := "testname" files := []File{ - &ReaderFile{"file.txt", strings.NewReader("Some text!\n")}, - &ReaderFile{"beep.txt", strings.NewReader("beep")}, - &ReaderFile{"boop.txt", strings.NewReader("boop")}, + &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"))}, } buf := make([]byte, 20) @@ -54,7 +55,7 @@ func TestSliceFiles(t *testing.T) { func TestReaderFiles(t *testing.T) { message := "beep boop" - rf := &ReaderFile{"file.txt", strings.NewReader(message)} + rf := &ReaderFile{"file.txt", ioutil.NopCloser(strings.NewReader(message))} buf := make([]byte, len(message)) if rf.IsDirectory() { diff --git a/commands/http/multifilereader_test.go b/commands/http/multifilereader_test.go index 4822e8e6b..8d832c9cc 100644 --- a/commands/http/multifilereader_test.go +++ b/commands/http/multifilereader_test.go @@ -2,6 +2,7 @@ package http import ( "io" + "io/ioutil" "mime/multipart" "strings" "testing" @@ -12,12 +13,12 @@ import ( func TestOutput(t *testing.T) { text := "Some text! :)" fileset := []files.File{ - &files.ReaderFile{"file.txt", strings.NewReader(text)}, + &files.ReaderFile{"file.txt", ioutil.NopCloser(strings.NewReader(text))}, &files.SliceFile{"boop", []files.File{ - &files.ReaderFile{"boop/a.txt", strings.NewReader("bleep")}, - &files.ReaderFile{"boop/b.txt", strings.NewReader("bloop")}, + &files.ReaderFile{"boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep"))}, + &files.ReaderFile{"boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop"))}, }}, - &files.ReaderFile{"beep.txt", strings.NewReader("beep")}, + &files.ReaderFile{"beep.txt", ioutil.NopCloser(strings.NewReader("beep"))}, } sf := &files.SliceFile{"", fileset} buf := make([]byte, 20)