serialfile.go 4.01 KB
Newer Older
1 2 3
package files

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

29
func NewSerialFile(name, path string, stat os.FileInfo) (File, error) {
30 31 32 33
	switch mode := stat.Mode(); {
	case mode.IsDir() || mode.IsRegular():
		break
	case mode&os.ModeSymlink != 0:
34 35 36 37 38 39
		target, err := os.Readlink(path)
		if err != nil {
			return nil, err
		}

		return NewLinkFile("", path, target, stat), nil
40 41
	default:
		return nil, fmt.Errorf("Unrecognized file type for %s: %s", stat.Name(), mode.String())
42 43 44
	}

	file, err := os.Open(path)
45 46 47 48
	if err != nil {
		return nil, err
	}

49
	return newSerialFile(name, path, file, stat)
50 51
}

52
func newSerialFile(name, path string, file *os.File, stat os.FileInfo) (File, error) {
53 54
	// for non-directories, return a ReaderFile
	if !stat.IsDir() {
55
		return &ReaderFile{name, path, file, stat}, nil
56 57 58 59 60 61 62 63 64 65 66
	}

	// 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
67
	if err := file.Close(); err != nil {
68 69 70 71 72 73
		return nil, err
	}

	// make sure contents are sorted so -- repeatably -- we get the same inputs.
	sort.Sort(sortFIByName(contents))

74
	return &serialFile{name, path, contents, stat, nil}, nil
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
}

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
99
	fileName := fp.Join(f.name, stat.Name())
100
	filePath := fp.Join(f.path, stat.Name())
101 102 103 104 105
	st, err := os.Lstat(filePath)
	if err != nil {
		return nil, err
	}

106 107 108 109
	switch mode := st.Mode(); {
	case mode.IsDir() || mode.IsRegular():
		break
	case mode&os.ModeSymlink != 0:
110 111 112 113 114 115
		f.current = nil
		target, err := os.Readlink(filePath)
		if err != nil {
			return nil, err
		}
		return NewLinkFile(fileName, filePath, target, st), nil
116 117
	default:
		return nil, fmt.Errorf("Unrecognized file type for %s: %s", st.Name(), mode.String())
118 119
	}

120 121 122 123
	file, err := os.Open(filePath)
	if err != nil {
		return nil, err
	}
124

125 126 127 128 129
	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
130
	return newSerialFile(fileName, filePath, file, stat)
131 132 133
}

func (f *serialFile) FileName() string {
134 135 136 137
	return f.name
}

func (f *serialFile) FullPath() string {
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
	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
}
157 158 159 160

func (f *serialFile) Stat() os.FileInfo {
	return f.stat
}
161 162

func (f *serialFile) Size() (int64, error) {
163 164
	if !f.stat.IsDir() {
		return f.stat.Size(), nil
165 166
	}

167 168 169 170
	var du int64
	err := fp.Walk(f.FileName(), func(p string, fi os.FileInfo, err error) error {
		if fi != nil && fi.Mode()&(os.ModeSymlink|os.ModeNamedPipe) == 0 {
			du += fi.Size()
171
		}
172 173 174
		return nil
	})
	return du, err
175
}