diff --git a/file.go b/file.go index daa30b8..b2d9712 100644 --- a/file.go +++ b/file.go @@ -7,8 +7,8 @@ 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("file isn't a directory") + ErrNotReader = errors.New("file isn't a regular file") ErrNotSupported = errors.New("operation not supported") ) @@ -20,22 +20,29 @@ var ( // Read/Seek methods are only valid for files // NextFile method is only valid for directories type File interface { - io.Reader io.Closer - io.Seeker - // Size returns size of the + // Size returns size of this file (if this file is a directory, total size of + // all files stored in the tree should be returned). Some implementations may + // choose not to implement this Size() (int64, error) +} + +// Regular represents the regular Unix file +type Regular interface { + File - // IsDirectory returns true if the File is a directory (and therefore - // supports calling `Files`/`Walk`) and false if the File is a normal file - // (and therefore supports calling `Read`/`Close`/`Seek`) - IsDirectory() bool + io.Reader + io.Seeker +} + +// Directory is a special file which can link to any number of files +type Directory interface { + File // NextFile returns the next child file available (if the File is a // 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. + // available. // // Note: // - Some implementations may only allow reading in order - if a diff --git a/file_test.go b/file_test.go index e68540c..9b13082 100644 --- a/file_test.go +++ b/file_test.go @@ -18,23 +18,15 @@ func TestSliceFiles(t *testing.T) { sf := NewSliceFile(files) - if !sf.IsDirectory() { - t.Fatal("SliceFile should always be a directory") - } - - if n, err := sf.Read(buf); n > 0 || err != ErrNotReader { - t.Fatal("Shouldn't be able to read data from a SliceFile") - } - - if err := sf.Close(); err != nil { - t.Fatal("Should be able to call `Close` on a SliceFile") - } - _, file, err := sf.NextFile() if file == nil || err != nil { t.Fatal("Expected a file and nil error") } - read, err := file.Read(buf) + rf, ok := file.(Regular) + if !ok { + t.Fatal("Expected a regular file") + } + read, err := rf.Read(buf) if read != 11 || err != nil { t.Fatal("NextFile got a file in the wrong order") } @@ -52,6 +44,10 @@ func TestSliceFiles(t *testing.T) { if file != nil || err != io.EOF { t.Fatal("Expected a nil file and io.EOF") } + + if err := sf.Close(); err != nil { + t.Fatal("Should be able to call `Close` on a SliceFile") + } } func TestReaderFiles(t *testing.T) { @@ -59,14 +55,6 @@ func TestReaderFiles(t *testing.T) { 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() - if file != nil || err != ErrNotDirectory { - t.Fatal("Expected a nil file and ErrNotDirectory") - } - if n, err := rf.Read(buf); n == 0 || err != nil { t.Fatal("Expected to be able to read") } @@ -117,19 +105,17 @@ anotherfile if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } - if mpf.IsDirectory() { + mf, ok := mpf.(Regular) + if !ok { t.Fatal("Expected file to not be a directory") } if mpname != "name" { t.Fatal("Expected filename to be \"name\"") } - 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) { + if n, err := mf.Read(buf); n != 4 || !(err == io.EOF || err == nil) { t.Fatal("Expected to be able to read 4 bytes", n, err) } - if err := mpf.Close(); err != nil { + if err := mf.Close(); err != nil { t.Fatal("Expected to be able to close file") } @@ -142,16 +128,14 @@ anotherfile if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } - if !mpf.IsDirectory() { + md, ok := mpf.(Directory) + if !ok { t.Fatal("Expected file to be a directory") } if mpname != "dir" { t.Fatal("Expected filename to be \"dir\"") } - if n, err := mpf.Read(buf); n > 0 || err != ErrNotReader { - t.Fatal("Shouldn't be able to call `Read` on a directory") - } - if err := mpf.Close(); err != nil { + if err := md.Close(); err != nil { t.Fatal("Should be able to call `Close` on a directory") } @@ -164,13 +148,14 @@ anotherfile if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } - if mpf.IsDirectory() { - t.Fatal("Expected file, got directory") + mf, ok = mpf.(Regular) + if !ok { + t.Fatal("Expected file to not be a directory") } 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) { + if n, err := mf.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) } if err := mpf.Close(); err != nil { @@ -186,17 +171,14 @@ anotherfile if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } - if mpf.IsDirectory() { - t.Fatal("Expected file to be a symlink") + ms, ok := mpf.(*Symlink) + if !ok { + t.Fatal("Expected file to not be a directory") } if mpname != "simlynk" { t.Fatal("Expected filename to be \"dir/simlynk\"") } - slink, ok := mpf.(*Symlink) - if !ok { - t.Fatalf("expected file to be a symlink") - } - if slink.Target != "anotherfile" { + if ms.Target != "anotherfile" { t.Fatal("expected link to point to anotherfile") } } diff --git a/linkfile.go b/linkfile.go index 0182d39..df334d5 100644 --- a/linkfile.go +++ b/linkfile.go @@ -7,30 +7,20 @@ import ( ) type Symlink struct { - path string Target string stat os.FileInfo reader io.Reader } -func NewLinkFile(path, target string, stat os.FileInfo) File { +func NewLinkFile(target string, stat os.FileInfo) Regular { return &Symlink{ - path: path, Target: target, stat: stat, reader: strings.NewReader(target), } } -func (lf *Symlink) IsDirectory() bool { - return false -} - -func (lf *Symlink) NextFile() (string, File, error) { - return "", nil, ErrNotDirectory -} - func (lf *Symlink) Close() error { if c, ok := lf.reader.(io.Closer); ok { return c.Close() @@ -54,3 +44,5 @@ func (lf *Symlink) Seek(offset int64, whence int) (int64, error) { func (lf *Symlink) Size() (int64, error) { return 0, ErrNotSupported } + +var _ Regular = &Symlink{} diff --git a/multifilereader.go b/multifilereader.go index 85e66e7..b0ce1d9 100644 --- a/multifilereader.go +++ b/multifilereader.go @@ -17,7 +17,7 @@ type MultiFileReader struct { io.Reader // directory stack for NextFile - files []File + files []Directory path []string currentFile File @@ -31,12 +31,12 @@ type MultiFileReader struct { form bool } -// NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.File`. +// NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.Directory`. // 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 File, form bool) *MultiFileReader { +func NewMultiFileReader(file Directory, form bool) *MultiFileReader { mfr := &MultiFileReader{ - files: []File{file}, + files: []Directory{file}, path: []string{""}, form: form, mutex: &sync.Mutex{}, @@ -91,15 +91,19 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename)) var contentType string - if _, ok := file.(*Symlink); ok { + + switch f := file.(type) { + case *Symlink: contentType = "application/symlink" - } else if file.IsDirectory() { - mfr.files = append(mfr.files, file) + case Directory: + mfr.files = append(mfr.files, f) mfr.path = append(mfr.path, name) contentType = "application/x-directory" - } else { + case Regular: // otherwise, use the file as a reader to read its contents contentType = "application/octet-stream" + default: + return 0, ErrNotSupported } header.Set("Content-Type", contentType) @@ -120,16 +124,20 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { } // otherwise, read from file data - written, err = mfr.currentFile.Read(buf) - if err == io.EOF || err == ErrNotReader { - if err := mfr.currentFile.Close(); err != nil { + switch f := mfr.currentFile.(type) { + case Regular: + written, err = f.Read(buf) + if err != io.EOF { return written, err } + } - mfr.currentFile = nil - return written, nil + if err := mfr.currentFile.Close(); err != nil { + return written, err } - return written, err + + mfr.currentFile = nil + return written, nil } // Boundary returns the boundary string to be used to separate files in the multipart data diff --git a/multifilereader_test.go b/multifilereader_test.go index 18876ef..5f0cac5 100644 --- a/multifilereader_test.go +++ b/multifilereader_test.go @@ -33,46 +33,48 @@ func TestMultiFileReaderToMultiFile(t *testing.T) { t.Fatal(err) } - if !mf.IsDirectory() { + md, ok := mf.(Directory) + if !ok { t.Fatal("Expected a directory") } - fn, f, err := mf.NextFile() + fn, f, err := md.NextFile() if fn != "file.txt" || f == nil || err != nil { t.Fatal("NextFile returned unexpected data") } - dn, d, err := mf.NextFile() + dn, d, err := md.NextFile() if dn != "boop" || d == nil || err != nil { t.Fatal("NextFile returned unexpected data") } - if !d.IsDirectory() { + df, ok := d.(Directory) + if !ok { t.Fatal("Expected a directory") } - cfn, cf, err := d.NextFile() + cfn, cf, err := df.NextFile() if cfn != "a.txt" || cf == nil || err != nil { t.Fatal("NextFile returned unexpected data") } - cfn, cf, err = d.NextFile() + cfn, cf, err = df.NextFile() if cfn != "b.txt" || cf == nil || err != nil { t.Fatal("NextFile returned unexpected data") } - cfn, cf, err = d.NextFile() + cfn, cf, err = df.NextFile() if cfn != "" || cf != nil || err != io.EOF { t.Fatal("NextFile returned unexpected data") } // try to break internal state - cfn, cf, err = d.NextFile() + cfn, cf, err = df.NextFile() if cfn != "" || cf != nil || err != io.EOF { t.Fatal("NextFile returned unexpected data") } - fn, f, err = mf.NextFile() + fn, f, err = md.NextFile() if fn != "beep.txt" || f == nil || err != nil { t.Fatal("NextFile returned unexpected data") } @@ -91,13 +93,14 @@ func TestOutput(t *testing.T) { 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") + mpr, ok := mpf.(Regular) + if !ok { + t.Fatal("Expected file to be a regular file") } if mpname != "file.txt" { t.Fatal("Expected filename to be \"file.txt\"") } - if n, err := mpf.Read(buf); n != len(text) || err != nil { + if n, err := mpr.Read(buf); n != len(text) || err != nil { t.Fatal("Expected to read from file", n, err) } if string(buf[:len(text)]) != text { @@ -112,7 +115,8 @@ func TestOutput(t *testing.T) { if mpf == nil || err != nil { t.Fatal("Expected non-nil MultipartFile, nil error") } - if !mpf.IsDirectory() { + mpd, ok := mpf.(Directory) + if !ok { t.Fatal("Expected file to be a directory") } if mpname != "boop" { @@ -127,7 +131,7 @@ func TestOutput(t *testing.T) { if child == nil || err != nil { t.Fatal("Expected to be able to read a child file") } - if child.IsDirectory() { + if _, ok := child.(Regular); !ok { t.Fatal("Expected file to not be a directory") } if cname != "a.txt" { @@ -142,14 +146,14 @@ func TestOutput(t *testing.T) { if child == nil || err != nil { t.Fatal("Expected to be able to read a child file") } - if child.IsDirectory() { + if _, ok := child.(Regular); !ok { t.Fatal("Expected file to not be a directory") } if cname != "b.txt" { t.Fatal("Expected filename to be \"b.txt\"") } - cname, child, err = mpf.NextFile() + cname, child, err = mpd.NextFile() if child != nil || err != io.EOF { t.Fatal("Expected to get (nil, io.EOF)") } diff --git a/multipartfile.go b/multipartfile.go index cc94f17..5c62c2c 100644 --- a/multipartfile.go +++ b/multipartfile.go @@ -34,6 +34,10 @@ type MultipartFile struct { } func NewFileFromPartReader(reader *multipart.Reader, mediatype string) (File, error) { + if !isDirectory(mediatype) { + return nil, ErrNotDirectory + } + f := &MultipartFile{ Reader: &peekReader{r: reader}, Mediatype: mediatype, @@ -61,9 +65,7 @@ func newFileFromPart(parent string, part *multipart.Part, reader PartReader) (st return "", nil, err } - return base, &Symlink{ - Target: string(out), - }, nil + return base, NewLinkFile(string(out), nil), nil case "": // default to application/octet-stream fallthrough case applicationFile: @@ -79,17 +81,21 @@ func newFileFromPart(parent string, part *multipart.Part, reader PartReader) (st return "", nil, err } + if !isDirectory(f.Mediatype) { + return base, &ReaderFile{ + reader: part, + abspath: part.Header.Get("abspath"), + }, nil + } + return base, f, nil } -func (f *MultipartFile) IsDirectory() bool { - return f.Mediatype == multipartFormdataType || f.Mediatype == applicationDirectory +func isDirectory(mediatype string) bool { + return mediatype == multipartFormdataType || mediatype == applicationDirectory } func (f *MultipartFile) NextFile() (string, File, error) { - if !f.IsDirectory() { - return "", nil, ErrNotDirectory - } if f.Reader == nil { return "", nil, io.EOF } @@ -128,24 +134,10 @@ func (f *MultipartFile) fileName() string { return 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 { return f.Part.Close() } -func (f *MultipartFile) Seek(offset int64, whence int) (int64, error) { - if f.IsDirectory() { - return 0, ErrNotReader - } - return 0, ErrNotSupported -} - func (f *MultipartFile) Size() (int64, error) { return 0, ErrNotSupported } @@ -176,3 +168,5 @@ func (pr *peekReader) put(p *multipart.Part) error { pr.next = p return nil } + +var _ Directory = &MultipartFile{} diff --git a/readerfile.go b/readerfile.go index 070ee97..7db7e96 100644 --- a/readerfile.go +++ b/readerfile.go @@ -14,7 +14,7 @@ type ReaderFile struct { stat os.FileInfo } -func NewReaderFile(reader io.ReadCloser, stat os.FileInfo) File { +func NewReaderFile(reader io.ReadCloser, stat os.FileInfo) Regular { return &ReaderFile{"", reader, stat} } @@ -27,14 +27,6 @@ func NewReaderPathFile(path string, reader io.ReadCloser, stat os.FileInfo) (*Re return &ReaderFile{abspath, reader, stat}, nil } -func (f *ReaderFile) IsDirectory() bool { - return false -} - -func (f *ReaderFile) NextFile() (string, File, error) { - return "", nil, ErrNotDirectory -} - func (f *ReaderFile) AbsPath() string { return f.abspath } @@ -65,3 +57,6 @@ func (f *ReaderFile) Seek(offset int64, whence int) (int64, error) { return 0, ErrNotSupported } + +var _ Regular = &ReaderFile{} +var _ FileInfo = &ReaderFile{} diff --git a/serialfile.go b/serialfile.go index 71855d5..666399d 100644 --- a/serialfile.go +++ b/serialfile.go @@ -40,18 +40,12 @@ func NewSerialFile(path string, hidden bool, stat os.FileInfo) (File, error) { if err != nil { return nil, err } - return NewLinkFile(path, target, stat), nil + return NewLinkFile(target, stat), nil default: return nil, fmt.Errorf("unrecognized file type for %s: %s", path, mode.String()) } } -func (f *serialFile) IsDirectory() bool { - // non-directories get created as a ReaderFile, so serialFiles should only - // represent directories - return true -} - func (f *serialFile) NextFile() (string, File, error) { // if a file was opened previously, close it err := f.Close() @@ -97,18 +91,10 @@ func (f *serialFile) NextFile() (string, File, error) { return stat.Name(), sf, nil } -func (f *serialFile) Read(p []byte) (int, error) { - return 0, ErrNotReader -} - func (f *serialFile) Close() error { return nil } -func (f *serialFile) Seek(offset int64, whence int) (int64, error) { - return 0, ErrNotReader -} - func (f *serialFile) Stat() os.FileInfo { return f.stat } @@ -132,3 +118,5 @@ func (f *serialFile) Size() (int64, error) { return du, err } + +var _ Directory = &serialFile{} diff --git a/slicefile.go b/slicefile.go index 5fea5a6..0d78e16 100644 --- a/slicefile.go +++ b/slicefile.go @@ -17,14 +17,10 @@ type SliceFile struct { n int } -func NewSliceFile(files []FileEntry) File { +func NewSliceFile(files []FileEntry) Directory { return &SliceFile{files, 0} } -func (f *SliceFile) IsDirectory() bool { - return true -} - func (f *SliceFile) NextFile() (string, File, error) { if f.n >= len(f.files) { return "", nil, io.EOF @@ -34,18 +30,10 @@ func (f *SliceFile) NextFile() (string, File, error) { return file.Name, file.File, nil } -func (f *SliceFile) Read(p []byte) (int, error) { - return 0, ErrNotReader -} - func (f *SliceFile) Close() error { return nil } -func (f *SliceFile) Seek(offset int64, whence int) (int64, error) { - return 0, ErrNotReader -} - func (f *SliceFile) Length() int { return len(f.files) } @@ -63,3 +51,5 @@ func (f *SliceFile) Size() (int64, error) { return size, nil } + +var _ Directory = &SliceFile{} diff --git a/webfile.go b/webfile.go index 65d1c1e..5638f77 100644 --- a/webfile.go +++ b/webfile.go @@ -1,6 +1,7 @@ package files import ( + "github.com/pkg/errors" "io" "net/http" "net/url" @@ -12,6 +13,7 @@ import ( type WebFile struct { body io.ReadCloser url *url.URL + contentLength int64 } // NewWebFile creates a WebFile with the given URL, which @@ -33,6 +35,7 @@ func (wf *WebFile) Read(b []byte) (int, error) { return 0, err } wf.body = resp.Body + wf.contentLength = resp.ContentLength } return wf.body.Read(b) } @@ -45,17 +48,18 @@ func (wf *WebFile) Close() error { return wf.body.Close() } -// IsDirectory returns false. -func (wf *WebFile) IsDirectory() bool { - return false -} - -// NextFile always returns an ErrNotDirectory error. -func (wf *WebFile) NextFile() (File, error) { - return nil, ErrNotDirectory -} - // TODO: implement func (wf *WebFile) Seek(offset int64, whence int) (int64, error) { return 0, ErrNotSupported } + +func (wf *WebFile) Size() (int64, error) { + if wf.contentLength < 0 { + return -1, errors.New("Content-Length hearer was not set") + } + + return wf.contentLength, nil +} + + +var _ Regular = &WebFile{}