serialfile.go 3.7 KB
Newer Older
1 2 3 4 5
package files

import (
	"io"
	"os"
gatesvp's avatar
gatesvp committed
6
	fp "path/filepath"
7 8 9 10 11 12 13 14 15 16 17 18 19 20
	"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 {
21
	name    string
22 23
	path    string
	files   []os.FileInfo
24
	stat    os.FileInfo
25 26 27
	current *os.File
}

28
func NewSerialFile(name, path string, file *os.File) (File, error) {
29 30 31 32 33
	stat, err := file.Stat()
	if err != nil {
		return nil, err
	}

34
	return newSerialFile(name, path, file, stat)
35 36
}

37
func newSerialFile(name, path string, file *os.File, stat os.FileInfo) (File, error) {
38 39
	// for non-directories, return a ReaderFile
	if !stat.IsDir() {
40
		return &ReaderFile{name, path, file, stat}, nil
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
	}

	// 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))

60
	return &serialFile{name, path, contents, stat, nil}, nil
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
}

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
85
	fileName := fp.Join(f.name, stat.Name())
86
	filePath := fp.Join(f.path, stat.Name())
87 88 89 90 91 92 93 94 95 96 97 98 99 100
	st, err := os.Lstat(filePath)
	if err != nil {
		return nil, err
	}

	if st.Mode()&os.ModeSymlink != 0 {
		f.current = nil
		target, err := os.Readlink(filePath)
		if err != nil {
			return nil, err
		}
		return NewLinkFile(fileName, filePath, target, st), nil
	}

101 102 103 104
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
105

106 107 108 109 110
	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
111
	return newSerialFile(fileName, filePath, file, stat)
112 113 114
}

func (f *serialFile) FileName() string {
115 116 117 118
	return f.name
}

func (f *serialFile) FullPath() string {
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
	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
}
138 139 140 141

func (f *serialFile) Stat() os.FileInfo {
	return f.stat
}
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

func (f *serialFile) Size() (int64, error) {
	return size(f.stat, f.FileName())
}

func size(stat os.FileInfo, filename string) (int64, error) {
	if !stat.IsDir() {
		return stat.Size(), nil
	}

	file, err := os.Open(filename)
	if err != nil {
		return 0, err
	}
	files, err := file.Readdir(0)
	if err != nil {
		return 0, err
	}
	file.Close()

	var output int64
	for _, child := range files {
		s, err := size(child, fp.Join(filename, child.Name()))
		if err != nil {
			return 0, err
		}
		output += s
	}
	return output, nil
}