1package vfsutil
2
3import (
4	"io"
5	"net/http"
6	"os"
7	pathpkg "path"
8	"path/filepath"
9	"sort"
10)
11
12// Walk walks the filesystem rooted at root, calling walkFn for each file or
13// directory in the filesystem, including root. All errors that arise visiting files
14// and directories are filtered by walkFn. The files are walked in lexical
15// order.
16func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error {
17	info, err := Stat(fs, root)
18	if err != nil {
19		return walkFn(root, nil, err)
20	}
21	return walk(fs, root, info, walkFn)
22}
23
24// readDirNames reads the directory named by dirname and returns
25// a sorted list of directory entries.
26func readDirNames(fs http.FileSystem, dirname string) ([]string, error) {
27	fis, err := ReadDir(fs, dirname)
28	if err != nil {
29		return nil, err
30	}
31	names := make([]string, len(fis))
32	for i := range fis {
33		names[i] = fis[i].Name()
34	}
35	sort.Strings(names)
36	return names, nil
37}
38
39// walk recursively descends path, calling walkFn.
40func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
41	err := walkFn(path, info, nil)
42	if err != nil {
43		if info.IsDir() && err == filepath.SkipDir {
44			return nil
45		}
46		return err
47	}
48
49	if !info.IsDir() {
50		return nil
51	}
52
53	names, err := readDirNames(fs, path)
54	if err != nil {
55		return walkFn(path, info, err)
56	}
57
58	for _, name := range names {
59		filename := pathpkg.Join(path, name)
60		fileInfo, err := Stat(fs, filename)
61		if err != nil {
62			if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
63				return err
64			}
65		} else {
66			err = walk(fs, filename, fileInfo, walkFn)
67			if err != nil {
68				if !fileInfo.IsDir() || err != filepath.SkipDir {
69					return err
70				}
71			}
72		}
73	}
74	return nil
75}
76
77// WalkFilesFunc is the type of the function called for each file or directory visited by WalkFiles.
78// It's like filepath.WalkFunc, except it provides an additional ReadSeeker parameter for file being visited.
79type WalkFilesFunc func(path string, info os.FileInfo, rs io.ReadSeeker, err error) error
80
81// WalkFiles walks the filesystem rooted at root, calling walkFn for each file or
82// directory in the filesystem, including root. In addition to FileInfo, it passes an
83// ReadSeeker to walkFn for each file it visits.
84func WalkFiles(fs http.FileSystem, root string, walkFn WalkFilesFunc) error {
85	file, info, err := openStat(fs, root)
86	if err != nil {
87		return walkFn(root, nil, nil, err)
88	}
89	return walkFiles(fs, root, info, file, walkFn)
90}
91
92// walkFiles recursively descends path, calling walkFn.
93// It closes the input file after it's done with it, so the caller shouldn't.
94func walkFiles(fs http.FileSystem, path string, info os.FileInfo, file http.File, walkFn WalkFilesFunc) error {
95	err := walkFn(path, info, file, nil)
96	file.Close()
97	if err != nil {
98		if info.IsDir() && err == filepath.SkipDir {
99			return nil
100		}
101		return err
102	}
103
104	if !info.IsDir() {
105		return nil
106	}
107
108	names, err := readDirNames(fs, path)
109	if err != nil {
110		return walkFn(path, info, nil, err)
111	}
112
113	for _, name := range names {
114		filename := pathpkg.Join(path, name)
115		file, fileInfo, err := openStat(fs, filename)
116		if err != nil {
117			if err := walkFn(filename, nil, nil, err); err != nil && err != filepath.SkipDir {
118				return err
119			}
120		} else {
121			err = walkFiles(fs, filename, fileInfo, file, walkFn)
122			// file is closed by walkFiles, so we don't need to close it here.
123			if err != nil {
124				if !fileInfo.IsDir() || err != filepath.SkipDir {
125					return err
126				}
127			}
128		}
129	}
130	return nil
131}
132
133// openStat performs Open and Stat and returns results, or first error encountered.
134// The caller is responsible for closing the returned file when done.
135func openStat(fs http.FileSystem, name string) (http.File, os.FileInfo, error) {
136	f, err := fs.Open(name)
137	if err != nil {
138		return nil, nil, err
139	}
140	fi, err := f.Stat()
141	if err != nil {
142		f.Close()
143		return nil, nil, err
144	}
145	return f, fi, nil
146}
147