Skip to content
This repository has been archived by the owner on Mar 29, 2023. It is now read-only.

Commit

Permalink
Directory iterators
Browse files Browse the repository at this point in the history
  • Loading branch information
magik6k committed Nov 29, 2018
1 parent 97852d8 commit 2b34e27
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 139 deletions.
75 changes: 61 additions & 14 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,32 +28,79 @@ type File interface {
Size() (int64, error)
}

// Regular represents the regular Unix file
// Regular represents a regular Unix file
type Regular interface {
File

io.Reader
io.Seeker
}

// Directory is a special file which can link to any number of files
// DirEntry exposes information about a directory entry
type DirEntry interface {
// Name returns the base name of this entry, which is the base name of
// the referenced file
Name() string

// File returns the file referenced by this DirEntry
File() File

// Regular is an alias for ent.File().(Regular). If the file isn't a regular
// file, nil value will be returned
Regular() Regular

// Dir is an alias for ent.File().(directory). If the file isn't a directory,
// nil value will be returned
Dir() Directory
}

// DirIterator is a iterator over directory entries.
// See Directory.Entries for more
type DirIterator interface {
// DirEntry holds information about current directory entry.
// Note that after creating new iterator you MUST call Next() at least once
// before accessing these methods. Calling these methods without prior calls
// to Next() and after Next() returned false may result in undefined behavior
DirEntry

// Next advances the iterator to the next file.
Next() bool

// Err may return an error after the previous call to Next() returned `false`.
// If the previous call to Next() returned `true`, Err() is guaranteed to
// return nil
Err() error
}

// 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.
// Entries returns a stateful iterator over directory entries.
//
// Example usage:
//
// it := dir.Entries()
// for it.Next() {
// name := it.Name()
// file := it.File()
// [...]
// }
// if it.Err() != nil {
// return err
// }
//
// Note:
// - Some implementations may only allow reading in order - if a
// child directory is returned, you need to read all it's children
// first before calling NextFile on parent again. Before doing parallel
// reading or reading entire level at once, make sure the implementation
// you are using allows that
// - Returned files may not be sorted
// - Below limitations aren't applicable to all implementations, consult
// your implementations manual before using this interface in a way that
// doesn't meet these constraints
// - Some implementations may only allow reading in order - so if the iterator
// returns a directory you must iterate over it's entries first before
// calling Next again
// - Order is not guaranteed
// - Depending on implementation it may not be safe to iterate multiple
// children in parallel
NextFile() (string, File, error)
// 'branches' in parallel
Entries() (DirIterator, error)
}

// FileInfo exposes information on files in local filesystem
Expand All @@ -63,6 +110,6 @@ type FileInfo interface {
// AbsPath returns full real file path.
AbsPath() string

// Stat returns os.Stat of this file
// Stat returns os.Stat of this file, may be nil for some files
Stat() os.FileInfo
}
37 changes: 18 additions & 19 deletions file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,39 @@ import (
)

func TestSliceFiles(t *testing.T) {
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), ""},
files := []DirEntry{
FileEntry("", NewReaderFile(ioutil.NopCloser(strings.NewReader("Some text!\n")), nil)),
FileEntry("", NewReaderFile(ioutil.NopCloser(strings.NewReader("beep")), nil)),
FileEntry("", NewReaderFile(ioutil.NopCloser(strings.NewReader("boop")), nil)),
}
buf := make([]byte, 20)

sf := NewSliceFile(files)
it, err := sf.Entries()
if err != nil {
t.Fatal(err)
}

_, file, err := sf.NextFile()
if file == nil || err != nil {
t.Fatal("Expected a file and nil error")
if !it.Next() {
t.Fatal("Expected a file")
}
rf, ok := file.(Regular)
if !ok {
rf := it.Regular()
if rf == nil {
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")
}

_, file, err = sf.NextFile()
if file == nil || err != nil {
t.Fatal("Expected a file and nil error")
if !it.Next() {
t.Fatal("Expected a file")
}
_, file, err = sf.NextFile()
if file == nil || err != nil {
t.Fatal("Expected a file and nil error")
if !it.Next() {
t.Fatal("Expected a file")
}

_, file, err = sf.NextFile()
if file != nil || err != io.EOF {
t.Fatal("Expected a nil file and io.EOF")
if it.Next() {
t.Fatal("Wild file appeared!")
}

if err := sf.Close(); err != nil {
Expand Down
46 changes: 27 additions & 19 deletions multifilereader.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type MultiFileReader struct {
io.Reader

// directory stack for NextFile
files []Directory
files []DirIterator
path []string

currentFile File
Expand All @@ -34,16 +34,21 @@ type MultiFileReader struct {
// 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 Directory, form bool) *MultiFileReader {
func NewMultiFileReader(file Directory, form bool) (*MultiFileReader, error) {
it, err := file.Entries()
if err != nil {
return nil, err
}

mfr := &MultiFileReader{
files: []Directory{file},
files: []DirIterator{it},
path: []string{""},
form: form,
mutex: &sync.Mutex{},
}
mfr.mpWriter = multipart.NewWriter(&mfr.buf)

return mfr
return mfr, nil
}

func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) {
Expand All @@ -57,47 +62,50 @@ 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
var entry DirEntry

for file == nil {
for entry == nil {
if len(mfr.files) == 0 {
mfr.mpWriter.Close()
mfr.closed = true
return mfr.buf.Read(buf)
}

nextName, nextFile, err := mfr.files[len(mfr.files)-1].NextFile()
if err == io.EOF {
if !mfr.files[len(mfr.files)-1].Next() {
mfr.files = mfr.files[:len(mfr.files)-1]
mfr.path = mfr.path[:len(mfr.path)-1]
continue
} else if err != nil {
return 0, err
}
if mfr.files[len(mfr.files)-1].Err() != nil {
return 0, mfr.files[len(mfr.files)-1].Err()
}

file = nextFile
name = nextName
entry = mfr.files[len(mfr.files)-1]
}

// handle starting a new file part
if !mfr.closed {

mfr.currentFile = file
mfr.currentFile = entry.File()

// write the boundary and headers
header := make(textproto.MIMEHeader)
filename := url.QueryEscape(path.Join(path.Join(mfr.path...), name))
filename := url.QueryEscape(path.Join(path.Join(mfr.path...), entry.Name()))
header.Set("Content-Disposition", fmt.Sprintf("file; filename=\"%s\"", filename))

var contentType string

switch f := file.(type) {
switch f := entry.File().(type) {
case *Symlink:
contentType = "application/symlink"
case Directory:
mfr.files = append(mfr.files, f)
mfr.path = append(mfr.path, name)
newIt, err := f.Entries()
if err != nil {
return 0, err
}

mfr.files = append(mfr.files, newIt)
mfr.path = append(mfr.path, entry.Name())
contentType = "application/x-directory"
case Regular:
// otherwise, use the file as a reader to read its contents
Expand All @@ -107,7 +115,7 @@ func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) {
}

header.Set("Content-Type", contentType)
if rf, ok := file.(FileInfo); ok {
if rf, ok := entry.File().(FileInfo); ok {
header.Set("abspath", rf.AbsPath())
}

Expand Down
77 changes: 39 additions & 38 deletions multifilereader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,27 @@ import (

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"},
func getTestMultiFileReader(t *testing.T) *MultiFileReader {
fileset := []DirEntry{
FileEntry("file.txt", NewReaderFile(ioutil.NopCloser(strings.NewReader(text)), nil)),
FileEntry("boop", NewSliceFile([]DirEntry{
FileEntry("a.txt", NewReaderFile(ioutil.NopCloser(strings.NewReader("bleep")), nil)),
FileEntry("b.txt", NewReaderFile(ioutil.NopCloser(strings.NewReader("bloop")), nil)),
})),
FileEntry("beep.txt", NewReaderFile(ioutil.NopCloser(strings.NewReader("beep")), nil)),
}
sf := NewSliceFile(fileset)

// testing output by reading it with the go stdlib "mime/multipart" Reader
return NewMultiFileReader(sf, true)
r, err := NewMultiFileReader(sf, true)
if err != nil {
t.Fatal(err)
}
return r
}

func TestMultiFileReaderToMultiFile(t *testing.T) {
mfr := getTestMultiFileReader()
mfr := getTestMultiFileReader(t)
mpReader := multipart.NewReader(mfr, mfr.Boundary())
mf, err := NewFileFromPartReader(mpReader, multipartFormdataType)
if err != nil {
Expand All @@ -37,51 +41,48 @@ func TestMultiFileReaderToMultiFile(t *testing.T) {
if !ok {
t.Fatal("Expected a directory")
}
it, err := md.Entries()
if err != nil {
t.Fatal(err)
}

fn, f, err := md.NextFile()
if fn != "file.txt" || f == nil || err != nil {
t.Fatal("NextFile returned unexpected data")
if !it.Next() || it.Name() != "file.txt" {
t.Fatal("iterator didn't work as expected")
}

dn, d, err := md.NextFile()
if dn != "boop" || d == nil || err != nil {
t.Fatal("NextFile returned unexpected data")
if !it.Next() || it.Name() != "boop" || it.Dir() == nil {
t.Fatal("iterator didn't work as expected")
}

df, ok := d.(Directory)
if !ok {
t.Fatal("Expected a directory")
subIt, err := it.Dir().Entries()
if err != nil {
t.Fatal(err)
}

cfn, cf, err := df.NextFile()
if cfn != "a.txt" || cf == nil || err != nil {
t.Fatal("NextFile returned unexpected data")
if !subIt.Next() || subIt.Name() != "a.txt" || subIt.Dir() != nil {
t.Fatal("iterator didn't work as expected")
}

cfn, cf, err = df.NextFile()
if cfn != "b.txt" || cf == nil || err != nil {
t.Fatal("NextFile returned unexpected data")
if !subIt.Next() || subIt.Name() != "b.txt" || subIt.Dir() != nil {
t.Fatal("iterator didn't work as expected")
}

cfn, cf, err = df.NextFile()
if cfn != "" || cf != nil || err != io.EOF {
t.Fatal("NextFile returned unexpected data")
if subIt.Next() {
t.Fatal("iterator didn't work as expected")
}

// try to break internal state
cfn, cf, err = df.NextFile()
if cfn != "" || cf != nil || err != io.EOF {
t.Fatal("NextFile returned unexpected data")
if subIt.Next() {
t.Fatal("iterator didn't work as expected")
}

fn, f, err = md.NextFile()
if fn != "beep.txt" || f == nil || err != nil {
t.Fatal("NextFile returned unexpected data")
if !it.Next() || it.Name() != "beep.txt" || it.Dir() != nil {
t.Fatal("iterator didn't work as expected")
}
}

func TestOutput(t *testing.T) {
mfr := getTestMultiFileReader()
mfr := getTestMultiFileReader(t)
mpReader := &peekReader{r: multipart.NewReader(mfr, mfr.Boundary())}
buf := make([]byte, 20)

Expand Down Expand Up @@ -153,9 +154,9 @@ func TestOutput(t *testing.T) {
t.Fatal("Expected filename to be \"b.txt\"")
}

cname, child, err = mpd.NextFile()
if child != nil || err != io.EOF {
t.Fatal("Expected to get (nil, io.EOF)")
it, err := mpd.Entries()
if it.Next() {
t.Fatal("Expected to get false")
}

part, err = mpReader.NextPart()
Expand Down
Loading

0 comments on commit 2b34e27

Please sign in to comment.