diff --git a/file.go b/file.go index c5e8203..ceb60dd 100644 --- a/file.go +++ b/file.go @@ -7,56 +7,46 @@ import ( ) var ( - ErrNotDirectory = errors.New("Couldn't call NextFile(), this isn't a directory") - ErrNotReader = errors.New("This file is a directory, can't use Reader functions") + ErrNotDirectory = errors.New("couldn't call NextFile(), this isn't a directory") + ErrNotReader = errors.New("this file is a directory, can't use Reader functions") + + ErrNotSupported = errors.New("operation not supported") ) // 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()`. +// directories, child files are accessed serially by calling `Files()` +// or `Walk()`. +// +// Read/Seek/Close methods are only valid for files +// Files/Walk methods are only valid for directories type File interface { - // Files implement ReadCloser, but can only be read from or closed if - // they are not directories - io.ReadCloser - - // FileName returns a filename associated with this file - FileName() string + io.Reader + io.Closer + io.Seeker - // FullPath returns the full path used when adding with this file - FullPath() string + // Size returns size of the + Size() (int64, error) // 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`) + // supports calling `Files`/`Walk`) and false if the File is a normal file + // (and therefore supports calling `Read`/`Close`/`Seek`) 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 + // directory). It will return 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) + NextFile() (string, File, error) } -type StatFile interface { - File - - Stat() os.FileInfo -} - -type PeekFile interface { - SizeFile - - Peek(n int) File - Length() int -} - -type SizeFile interface { +// FileInfo exposes information on files in local filesystem +type FileInfo interface { File - Size() (int64, error) -} - -type FileInfo interface { + // AbsPath returns full/real file path. AbsPath() string + + // Stat returns os.Stat of this file Stat() os.FileInfo } diff --git a/file_test.go b/file_test.go index 733a8c8..60d1bc4 100644 --- a/file_test.go +++ b/file_test.go @@ -9,15 +9,14 @@ import ( ) func TestSliceFiles(t *testing.T) { - name := "testname" - files := []File{ - NewReaderFile("file.txt", "file.txt", ioutil.NopCloser(strings.NewReader("Some text!\n")), nil), - NewReaderFile("beep.txt", "beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil), - NewReaderFile("boop.txt", "boop.txt", ioutil.NopCloser(strings.NewReader("boop")), nil), + files := []FileEntry{ + {NewReaderFile(ioutil.NopCloser(strings.NewReader("Some text!\n")), nil), ""}, + {NewReaderFile(ioutil.NopCloser(strings.NewReader("beep")), nil), ""}, + {NewReaderFile(ioutil.NopCloser(strings.NewReader("boop")), nil), ""}, } buf := make([]byte, 20) - sf := NewSliceFile(name, name, files) + sf := NewSliceFile(files) if !sf.IsDirectory() { t.Fatal("SliceFile should always be a directory") @@ -31,7 +30,7 @@ func TestSliceFiles(t *testing.T) { t.Fatal("Shouldn't be able to call `Close` on a SliceFile") } - file, err := sf.NextFile() + _, file, err := sf.NextFile() if file == nil || err != nil { t.Fatal("Expected a file and nil error") } @@ -40,16 +39,16 @@ func TestSliceFiles(t *testing.T) { t.Fatal("NextFile got a file in the wrong order") } - file, err = sf.NextFile() + _, file, err = sf.NextFile() if file == nil || err != nil { t.Fatal("Expected a file and nil error") } - file, err = sf.NextFile() + _, file, err = sf.NextFile() if file == nil || err != nil { t.Fatal("Expected a file and nil error") } - file, err = sf.NextFile() + _, file, err = sf.NextFile() if file != nil || err != io.EOF { t.Fatal("Expected a nil file and io.EOF") } @@ -57,13 +56,13 @@ func TestSliceFiles(t *testing.T) { func TestReaderFiles(t *testing.T) { message := "beep boop" - rf := NewReaderFile("file.txt", "file.txt", ioutil.NopCloser(strings.NewReader(message)), nil) + rf := NewReaderFile(ioutil.NopCloser(strings.NewReader(message)), nil) buf := make([]byte, len(message)) if rf.IsDirectory() { t.Fatal("ReaderFile should never be a directory") } - file, err := rf.NextFile() + _, file, err := rf.NextFile() if file != nil || err != ErrNotDirectory { t.Fatal("Expected a nil file and ErrNotDirectory") } @@ -114,17 +113,17 @@ anotherfile if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err := NewFileFromPart(part) + mpname, mpf, err := newFileFromPart("", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if mpf.IsDirectory() { t.Fatal("Expected file to not be a directory") } - if mpf.FileName() != "name" { + if mpname != "name" { t.Fatal("Expected filename to be \"name\"") } - if file, err := mpf.NextFile(); file != nil || err != ErrNotDirectory { + if _, file, err := mpf.NextFile(); file != nil || err != ErrNotDirectory { t.Fatal("Expected a nil file and ErrNotDirectory") } if n, err := mpf.Read(buf); n != 4 || !(err == io.EOF || err == nil) { @@ -139,14 +138,14 @@ anotherfile if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err = NewFileFromPart(part) + mpname, mpf, err = newFileFromPart("", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if !mpf.IsDirectory() { t.Fatal("Expected file to be a directory") } - if mpf.FileName() != "dir" { + if mpname != "dir" { t.Fatal("Expected filename to be \"dir\"") } if n, err := mpf.Read(buf); n > 0 || err != ErrNotReader { @@ -161,15 +160,15 @@ anotherfile if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err = NewFileFromPart(part) + mpname, mpf, err = newFileFromPart("dir/", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if mpf.IsDirectory() { t.Fatal("Expected file, got directory") } - if mpf.FileName() != "dir/nested" { - t.Fatalf("Expected filename to be \"nested\", got %s", mpf.FileName()) + if mpname != "nested" { + t.Fatalf("Expected filename to be \"nested\", got %s", mpname) } if n, err := mpf.Read(buf); n != 12 || !(err == nil || err == io.EOF) { t.Fatalf("expected to be able to read 12 bytes from file: %s (got %d)", err, n) @@ -183,14 +182,14 @@ anotherfile if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err = NewFileFromPart(part) + mpname, mpf, err = newFileFromPart("dir/", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if mpf.IsDirectory() { t.Fatal("Expected file to be a symlink") } - if mpf.FileName() != "dir/simlynk" { + if mpname != "simlynk" { t.Fatal("Expected filename to be \"dir/simlynk\"") } slink, ok := mpf.(*Symlink) diff --git a/is_hidden.go b/is_hidden.go index b036068..d5bc886 100644 --- a/is_hidden.go +++ b/is_hidden.go @@ -7,9 +7,8 @@ import ( "strings" ) -func IsHidden(f File) bool { - - fName := filepath.Base(f.FileName()) +func IsHidden(name string, f File) bool { + fName := filepath.Base(name) if strings.HasPrefix(fName, ".") && len(fName) > 1 { return true diff --git a/is_hidden_windows.go b/is_hidden_windows.go index 7679433..40f40ae 100644 --- a/is_hidden_windows.go +++ b/is_hidden_windows.go @@ -9,7 +9,7 @@ import ( windows "golang.org/x/sys/windows" ) -func IsHidden(f File) bool { +func IsHidden(name string, f File) bool { fName := filepath.Base(f.FileName()) @@ -17,7 +17,12 @@ func IsHidden(f File) bool { return true } - p, e := windows.UTF16PtrFromString(f.FullPath()) + fi, ok := f.(FileInfo) + if !ok { + return false + } + + p, e := windows.UTF16PtrFromString(fi.AbsPath()) if e != nil { return false } diff --git a/linkfile.go b/linkfile.go index 18466f4..0182d39 100644 --- a/linkfile.go +++ b/linkfile.go @@ -7,7 +7,6 @@ import ( ) type Symlink struct { - name string path string Target string stat os.FileInfo @@ -15,9 +14,8 @@ type Symlink struct { reader io.Reader } -func NewLinkFile(name, path, target string, stat os.FileInfo) File { +func NewLinkFile(path, target string, stat os.FileInfo) File { return &Symlink{ - name: name, path: path, Target: target, stat: stat, @@ -29,22 +27,30 @@ func (lf *Symlink) IsDirectory() bool { return false } -func (lf *Symlink) NextFile() (File, error) { - return nil, io.EOF +func (lf *Symlink) NextFile() (string, File, error) { + return "", nil, ErrNotDirectory } -func (f *Symlink) FileName() string { - return f.name -} +func (lf *Symlink) Close() error { + if c, ok := lf.reader.(io.Closer); ok { + return c.Close() + } -func (f *Symlink) Close() error { return nil } -func (f *Symlink) FullPath() string { - return f.path +func (lf *Symlink) Read(b []byte) (int, error) { + return lf.reader.Read(b) +} + +func (lf *Symlink) Seek(offset int64, whence int) (int64, error) { + if s, ok := lf.reader.(io.Seeker); ok { + return s.Seek(offset, whence) + } + + return 0, ErrNotSupported } -func (f *Symlink) Read(b []byte) (int, error) { - return f.reader.Read(b) +func (lf *Symlink) Size() (int64, error) { + return 0, ErrNotSupported } diff --git a/multifilereader.go b/multifilereader.go index 4833e8d..7863c9d 100644 --- a/multifilereader.go +++ b/multifilereader.go @@ -7,6 +7,7 @@ import ( "mime/multipart" "net/textproto" "net/url" + "path" "sync" ) @@ -15,7 +16,10 @@ import ( type MultiFileReader struct { io.Reader - files []File + // directory stack for NextFile + files []File + path []string + currentFile io.Reader buf bytes.Buffer mpWriter *multipart.Writer @@ -33,6 +37,7 @@ type MultiFileReader struct { func NewMultiFileReader(file File, form bool) *MultiFileReader { mfr := &MultiFileReader{ files: []File{file}, + path: []string{""}, form: form, mutex: &sync.Mutex{}, } @@ -53,6 +58,8 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { // if the current file isn't set, advance to the next file if mfr.currentFile == nil { var file File + var name string + for file == nil { if len(mfr.files) == 0 { mfr.mpWriter.Close() @@ -60,40 +67,43 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { return mfr.buf.Read(buf) } - nextfile, err := mfr.files[len(mfr.files)-1].NextFile() + nextName, nextFile, err := mfr.files[len(mfr.files)-1].NextFile() if err == io.EOF { mfr.files = mfr.files[:len(mfr.files)-1] + mfr.path = mfr.path[:len(mfr.path)-1] continue } else if err != nil { return 0, err } - file = nextfile + file = nextFile + name = nextName } // handle starting a new file part if !mfr.closed { + mfr.currentFile = file + + // write the boundary and headers + header := make(textproto.MIMEHeader) + filename := url.QueryEscape(path.Join(path.Join(mfr.path...), name)) + header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename)) + var contentType string if _, ok := file.(*Symlink); ok { contentType = "application/symlink" } else if file.IsDirectory() { mfr.files = append(mfr.files, file) + mfr.path = append(mfr.path, name) contentType = "application/x-directory" } else { // otherwise, use the file as a reader to read its contents contentType = "application/octet-stream" } - mfr.currentFile = file - - // write the boundary and headers - header := make(textproto.MIMEHeader) - filename := url.QueryEscape(file.FileName()) - header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename)) - header.Set("Content-Type", contentType) - if rf, ok := file.(*ReaderFile); ok { + if rf, ok := file.(FileInfo); ok { header.Set("abspath", rf.AbsPath()) } diff --git a/multifilereader_test.go b/multifilereader_test.go index 3d2c978..18876ef 100644 --- a/multifilereader_test.go +++ b/multifilereader_test.go @@ -8,35 +8,93 @@ import ( "testing" ) -func TestOutput(t *testing.T) { - text := "Some text! :)" - fileset := []File{ - NewReaderFile("file.txt", "file.txt", ioutil.NopCloser(strings.NewReader(text)), nil), - NewSliceFile("boop", "boop", []File{ - NewReaderFile("boop/a.txt", "boop/a.txt", ioutil.NopCloser(strings.NewReader("bleep")), nil), - NewReaderFile("boop/b.txt", "boop/b.txt", ioutil.NopCloser(strings.NewReader("bloop")), nil), - }), - NewReaderFile("beep.txt", "beep.txt", ioutil.NopCloser(strings.NewReader("beep")), nil), - } - sf := NewSliceFile("", "", fileset) - buf := make([]byte, 20) +var text = "Some text! :)" + +func getTestMultiFileReader() *MultiFileReader { + fileset := []FileEntry{ + {NewReaderFile(ioutil.NopCloser(strings.NewReader(text)), nil), "file.txt"}, + {NewSliceFile([]FileEntry{ + {NewReaderFile(ioutil.NopCloser(strings.NewReader("bleep")), nil), "a.txt"}, + {NewReaderFile(ioutil.NopCloser(strings.NewReader("bloop")), nil), "b.txt"}, + }), "boop"}, + {NewReaderFile(ioutil.NopCloser(strings.NewReader("beep")), nil), "beep.txt"}, + } + sf := NewSliceFile(fileset) // testing output by reading it with the go stdlib "mime/multipart" Reader - mfr := NewMultiFileReader(sf, true) + return NewMultiFileReader(sf, true) +} + +func TestMultiFileReaderToMultiFile(t *testing.T) { + mfr := getTestMultiFileReader() mpReader := multipart.NewReader(mfr, mfr.Boundary()) + mf, err := NewFileFromPartReader(mpReader, multipartFormdataType) + if err != nil { + t.Fatal(err) + } + + if !mf.IsDirectory() { + t.Fatal("Expected a directory") + } + + fn, f, err := mf.NextFile() + if fn != "file.txt" || f == nil || err != nil { + t.Fatal("NextFile returned unexpected data") + } + + dn, d, err := mf.NextFile() + if dn != "boop" || d == nil || err != nil { + t.Fatal("NextFile returned unexpected data") + } + + if !d.IsDirectory() { + t.Fatal("Expected a directory") + } + + cfn, cf, err := d.NextFile() + if cfn != "a.txt" || cf == nil || err != nil { + t.Fatal("NextFile returned unexpected data") + } + + cfn, cf, err = d.NextFile() + if cfn != "b.txt" || cf == nil || err != nil { + t.Fatal("NextFile returned unexpected data") + } + + cfn, cf, err = d.NextFile() + if cfn != "" || cf != nil || err != io.EOF { + t.Fatal("NextFile returned unexpected data") + } + + // try to break internal state + cfn, cf, err = d.NextFile() + if cfn != "" || cf != nil || err != io.EOF { + t.Fatal("NextFile returned unexpected data") + } + + fn, f, err = mf.NextFile() + if fn != "beep.txt" || f == nil || err != nil { + t.Fatal("NextFile returned unexpected data") + } +} + +func TestOutput(t *testing.T) { + mfr := getTestMultiFileReader() + mpReader := &peekReader{r: multipart.NewReader(mfr, mfr.Boundary())} + buf := make([]byte, 20) part, err := mpReader.NextPart() if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err := NewFileFromPart(part) + mpname, mpf, err := newFileFromPart("", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if mpf.IsDirectory() { t.Fatal("Expected file to not be a directory") } - if mpf.FileName() != "file.txt" { + if mpname != "file.txt" { t.Fatal("Expected filename to be \"file.txt\"") } if n, err := mpf.Read(buf); n != len(text) || err != nil { @@ -50,14 +108,14 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err = NewFileFromPart(part) + mpname, mpf, err = newFileFromPart("", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } if !mpf.IsDirectory() { t.Fatal("Expected file to be a directory") } - if mpf.FileName() != "boop" { + if mpname != "boop" { t.Fatal("Expected filename to be \"boop\"") } @@ -65,33 +123,33 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - child, err := NewFileFromPart(part) + cname, child, err := newFileFromPart("boop", part, mpReader) if child == nil || err != nil { t.Fatal("Expected to be able to read a child file") } if child.IsDirectory() { t.Fatal("Expected file to not be a directory") } - if child.FileName() != "boop/a.txt" { - t.Fatal("Expected filename to be \"some/file/path\"") + if cname != "a.txt" { + t.Fatal("Expected filename to be \"a.txt\"") } part, err = mpReader.NextPart() if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - child, err = NewFileFromPart(part) + cname, child, err = newFileFromPart("boop", part, mpReader) if child == nil || err != nil { t.Fatal("Expected to be able to read a child file") } if child.IsDirectory() { t.Fatal("Expected file to not be a directory") } - if child.FileName() != "boop/b.txt" { - t.Fatal("Expected filename to be \"some/file/path\"") + if cname != "b.txt" { + t.Fatal("Expected filename to be \"b.txt\"") } - child, err = mpf.NextFile() + cname, child, err = mpf.NextFile() if child != nil || err != io.EOF { t.Fatal("Expected to get (nil, io.EOF)") } @@ -100,10 +158,13 @@ func TestOutput(t *testing.T) { if part == nil || err != nil { t.Fatal("Expected non-nil part, nil error") } - mpf, err = NewFileFromPart(part) + mpname, mpf, err = newFileFromPart("", part, mpReader) if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } + if mpname != "beep.txt" { + t.Fatal("Expected filename to be \"b.txt\"") + } part, err = mpReader.NextPart() if part != nil || err != io.EOF { diff --git a/multipartfile.go b/multipartfile.go index 8a4b0b2..a304163 100644 --- a/multipartfile.go +++ b/multipartfile.go @@ -1,11 +1,13 @@ package files import ( + "errors" "io" "io/ioutil" "mime" "mime/multipart" "net/url" + "path" ) const ( @@ -19,19 +21,36 @@ const ( contentTypeHeader = "Content-Type" ) +var ErrPartOutsideParent = errors.New("file outside parent dir") + // 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 + Reader PartReader Mediatype string } -func NewFileFromPart(part *multipart.Part) (File, error) { +func NewFileFromPartReader(reader *multipart.Reader, mediatype string) (File, error) { + f := &MultipartFile{ + Reader: &peekReader{r: reader}, + Mediatype: mediatype, + } + + return f, nil +} + +func newFileFromPart(parent string, part *multipart.Part, reader PartReader) (string, File, error) { f := &MultipartFile{ - Part: part, + Part: part, + Reader: reader, + } + + dir, base := path.Split(f.fileName()) + if path.Clean(dir) != path.Clean(parent) { + return "", nil, ErrPartOutsideParent } contentType := part.Header.Get(contentTypeHeader) @@ -39,54 +58,64 @@ func NewFileFromPart(part *multipart.Part) (File, error) { case applicationSymlink: out, err := ioutil.ReadAll(part) if err != nil { - return nil, err + return "", nil, err } - return &Symlink{ + return base, &Symlink{ Target: string(out), - name: f.FileName(), }, nil case "": // default to application/octet-stream fallthrough case applicationFile: - return &ReaderFile{ - reader: part, - filename: f.FileName(), - abspath: part.Header.Get("abspath"), - fullpath: f.FullPath(), + return base, &ReaderFile{ + reader: part, + abspath: part.Header.Get("abspath"), }, nil } var err error f.Mediatype, _, err = mime.ParseMediaType(contentType) if err != nil { - return nil, err + return "", nil, err } - return f, nil + return base, f, nil } func (f *MultipartFile) IsDirectory() bool { return f.Mediatype == multipartFormdataType || f.Mediatype == applicationDirectory } -func (f *MultipartFile) NextFile() (File, error) { +func (f *MultipartFile) NextFile() (string, File, error) { if !f.IsDirectory() { - return nil, ErrNotDirectory + return "", nil, ErrNotDirectory + } + if f.Reader == nil { + return "", nil, io.EOF + } + part, err := f.Reader.NextPart() + if err != nil { + return "", nil, err } - if f.Reader != nil { - part, err := f.Reader.NextPart() - if err != nil { - return nil, err - } - return NewFileFromPart(part) + name, cf, err := newFileFromPart(f.fileName(), part, f.Reader) + if err != ErrPartOutsideParent { + return name, cf, err } - return nil, io.EOF + // we read too much, try to fix this + pr, ok := f.Reader.(*peekReader) + if !ok { + return "", nil, errors.New("cannot undo NextPart") + } + + if err := pr.put(part); err != nil { + return "", nil, err + } + return "", nil, io.EOF } -func (f *MultipartFile) FileName() string { +func (f *MultipartFile) fileName() string { if f == nil || f.Part == nil { return "" } @@ -99,10 +128,6 @@ func (f *MultipartFile) FileName() string { return filename } -func (f *MultipartFile) FullPath() string { - return f.FileName() -} - func (f *MultipartFile) Read(p []byte) (int, error) { if f.IsDirectory() { return 0, ErrNotReader @@ -116,3 +141,41 @@ func (f *MultipartFile) Close() error { } return f.Part.Close() } + +func (f *MultipartFile) Seek(offset int64, whence int) (int64, error) { + if f.IsDirectory() { + return 0, ErrNotReader + } + return 0, ErrNotReader +} + +func (f *MultipartFile) Size() (int64, error) { + return 0, ErrNotReader +} + +type PartReader interface { + NextPart() (*multipart.Part, error) +} + +type peekReader struct { + r PartReader + next *multipart.Part +} + +func (pr *peekReader) NextPart() (*multipart.Part, error) { + if pr.next != nil { + p := pr.next + pr.next = nil + return p, nil + } + + return pr.r.NextPart() +} + +func (pr *peekReader) put(p *multipart.Part) error { + if pr.next != nil { + return errors.New("cannot put multiple parts") + } + pr.next = p + return nil +} diff --git a/readerfile.go b/readerfile.go index 8636414..e40032c 100644 --- a/readerfile.go +++ b/readerfile.go @@ -10,40 +10,30 @@ import ( // 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 - fullpath string - abspath string - reader io.ReadCloser - stat os.FileInfo + abspath string + reader io.ReadCloser + stat os.FileInfo } -func NewReaderFile(filename, path string, reader io.ReadCloser, stat os.FileInfo) *ReaderFile { - return &ReaderFile{filename, path, path, reader, stat} +func NewReaderFile(reader io.ReadCloser, stat os.FileInfo) File { + return &ReaderFile{"", reader, stat} } -func NewReaderPathFile(filename, path string, reader io.ReadCloser, stat os.FileInfo) (*ReaderFile, error) { +func NewReaderPathFile(path string, reader io.ReadCloser, stat os.FileInfo) (*ReaderFile, error) { abspath, err := filepath.Abs(path) if err != nil { return nil, err } - return &ReaderFile{filename, path, abspath, reader, stat}, nil + return &ReaderFile{abspath, reader, stat}, nil } 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) FullPath() string { - return f.fullpath +func (f *ReaderFile) NextFile() (string, File, error) { + return "", nil, ErrNotDirectory } func (f *ReaderFile) AbsPath() string { @@ -64,7 +54,15 @@ func (f *ReaderFile) Stat() os.FileInfo { func (f *ReaderFile) Size() (int64, error) { if f.stat == nil { - return 0, errors.New("File size unknown") + return 0, errors.New("file size unknown") } return f.stat.Size(), nil } + +func (f *ReaderFile) Seek(offset int64, whence int) (int64, error) { + if s, ok := f.reader.(io.Seeker); ok { + return s.Seek(offset, whence) + } + + return 0, ErrNotSupported +} diff --git a/serialfile.go b/serialfile.go index 15e6c90..eb41976 100644 --- a/serialfile.go +++ b/serialfile.go @@ -7,29 +7,26 @@ import ( "os" "path/filepath" "strings" - "syscall" ) // 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 { - name string path string files []os.FileInfo stat os.FileInfo - current *File handleHiddenFiles bool } -func NewSerialFile(name, path string, hidden bool, stat os.FileInfo) (File, error) { +func NewSerialFile(path string, hidden bool, stat os.FileInfo) (File, error) { switch mode := stat.Mode(); { case mode.IsRegular(): file, err := os.Open(path) if err != nil { return nil, err } - return NewReaderPathFile(name, path, file, stat) + return NewReaderPathFile(path, file, stat) case mode.IsDir(): // for directories, stat all of the contents first, so we know what files to // open when NextFile() is called @@ -37,15 +34,15 @@ func NewSerialFile(name, path string, hidden bool, stat os.FileInfo) (File, erro if err != nil { return nil, err } - return &serialFile{name, path, contents, stat, nil, hidden}, nil + return &serialFile{path, contents, stat, hidden}, nil case mode&os.ModeSymlink != 0: target, err := os.Readlink(path) if err != nil { return nil, err } - return NewLinkFile(name, path, target, stat), nil + return NewLinkFile(path, target, stat), nil default: - return nil, fmt.Errorf("Unrecognized file type for %s: %s", name, mode.String()) + return nil, fmt.Errorf("unrecognized file type for %s: %s", path, mode.String()) } } @@ -55,23 +52,23 @@ func (f *serialFile) IsDirectory() bool { return true } -func (f *serialFile) NextFile() (File, error) { +func (f *serialFile) NextFile() (string, File, error) { // if a file was opened previously, close it err := f.Close() if err != nil { switch err2 := err.(type) { case *os.PathError: if err2.Err != os.ErrClosed { - return nil, err + return "", nil, err } default: - return nil, err + 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 + return "", nil, io.EOF } stat := f.files[0] @@ -79,7 +76,7 @@ func (f *serialFile) NextFile() (File, error) { for !f.handleHiddenFiles && strings.HasPrefix(stat.Name(), ".") { if len(f.files) == 0 { - return nil, io.EOF + return "", nil, io.EOF } stat = f.files[0] @@ -87,28 +84,17 @@ func (f *serialFile) NextFile() (File, error) { } // open the next file - fileName := filepath.ToSlash(filepath.Join(f.name, stat.Name())) filePath := filepath.ToSlash(filepath.Join(f.path, stat.Name())) // 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 - sf, err := NewSerialFile(fileName, filePath, f.handleHiddenFiles, stat) + sf, err := NewSerialFile(filePath, f.handleHiddenFiles, stat) if err != nil { - return nil, err + return "", nil, err } - f.current = &sf - - return sf, nil -} - -func (f *serialFile) FileName() string { - return f.name -} - -func (f *serialFile) FullPath() string { - return f.path + return stat.Name(), sf, nil } func (f *serialFile) Read(p []byte) (int, error) { @@ -116,18 +102,13 @@ func (f *serialFile) Read(p []byte) (int, error) { } 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 } +func (f *serialFile) Seek(offset int64, whence int) (int64, error) { + return 0, ErrNotReader +} + func (f *serialFile) Stat() os.FileInfo { return f.stat } @@ -138,7 +119,7 @@ func (f *serialFile) Size() (int64, error) { } var du int64 - err := filepath.Walk(f.FullPath(), func(p string, fi os.FileInfo, err error) error { + err := filepath.Walk(f.path, func(p string, fi os.FileInfo, err error) error { if err != nil { return err } diff --git a/slicefile.go b/slicefile.go index 8d18dca..d356465 100644 --- a/slicefile.go +++ b/slicefile.go @@ -1,43 +1,37 @@ package files import ( - "errors" "io" ) +type FileEntry struct { + File File + Name string +} + // 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 - path string - files []File - n int + files []FileEntry + n int } -func NewSliceFile(filename, path string, files []File) *SliceFile { - return &SliceFile{filename, path, files, 0} +func NewSliceFile(files []FileEntry) File { + return &SliceFile{files, 0} } func (f *SliceFile) IsDirectory() bool { return true } -func (f *SliceFile) NextFile() (File, error) { +func (f *SliceFile) NextFile() (string, File, error) { if f.n >= len(f.files) { - return nil, io.EOF + return "", nil, io.EOF } file := f.files[f.n] f.n++ - return file, nil -} - -func (f *SliceFile) FileName() string { - return f.filename -} - -func (f *SliceFile) FullPath() string { - return f.path + return file.Name, file.File, nil } func (f *SliceFile) Read(p []byte) (int, error) { @@ -48,8 +42,8 @@ func (f *SliceFile) Close() error { return ErrNotReader } -func (f *SliceFile) Peek(n int) File { - return f.files[n] +func (f *SliceFile) Seek(offset int64, whence int) (int64, error) { + return 0, ErrNotReader } func (f *SliceFile) Length() int { @@ -60,12 +54,7 @@ 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() + s, err := file.File.Size() if err != nil { return 0, err } diff --git a/webfile.go b/webfile.go index fcf4412..65d1c1e 100644 --- a/webfile.go +++ b/webfile.go @@ -4,7 +4,6 @@ import ( "io" "net/http" "net/url" - "path/filepath" ) // WebFile is an implementation of File which reads it @@ -46,17 +45,6 @@ func (wf *WebFile) Close() error { return wf.body.Close() } -// FullPath returns the "Host+Path" for this WebFile. -func (wf *WebFile) FullPath() string { - return wf.url.Host + wf.url.Path -} - -// FileName returns the last element of the URL -// path for this file. -func (wf *WebFile) FileName() string { - return filepath.Base(wf.url.Path) -} - // IsDirectory returns false. func (wf *WebFile) IsDirectory() bool { return false @@ -66,3 +54,8 @@ func (wf *WebFile) IsDirectory() bool { func (wf *WebFile) NextFile() (File, error) { return nil, ErrNotDirectory } + +// TODO: implement +func (wf *WebFile) Seek(offset int64, whence int) (int64, error) { + return 0, ErrNotSupported +}