forked from evanw/esbuild
-
Notifications
You must be signed in to change notification settings - Fork 1
/
fs.go
222 lines (186 loc) · 4.78 KB
/
fs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package fs
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"sync"
)
type EntryKind uint8
const (
DirEntry EntryKind = 1
FileEntry EntryKind = 2
)
type Entry struct {
Kind EntryKind
Symlink string
}
type FS interface {
// The returned map is immutable and is cached across invocations. Do not
// mutate it.
ReadDirectory(path string) map[string]Entry
ReadFile(path string) (string, bool)
// This is part of the interface because the mock interface used for tests
// should not depend on file system behavior (i.e. different slashes for
// Windows) while the real interface should.
Dir(path string) string
Base(path string) string
Join(parts ...string) string
RelativeToCwd(path string) (string, bool)
}
////////////////////////////////////////////////////////////////////////////////
type mockFS struct {
dirs map[string]map[string]Entry
files map[string]string
}
func MockFS(input map[string]string) FS {
dirs := make(map[string]map[string]Entry)
files := make(map[string]string)
for k, v := range input {
files[k] = v
original := k
// Build the directory map
for {
kDir := path.Dir(k)
dir, ok := dirs[kDir]
if !ok {
dir = make(map[string]Entry)
dirs[kDir] = dir
}
if kDir == k {
break
}
if k == original {
dir[path.Base(k)] = Entry{Kind: FileEntry}
} else {
dir[path.Base(k)] = Entry{Kind: DirEntry}
}
k = kDir
}
}
return &mockFS{dirs, files}
}
func (fs *mockFS) ReadDirectory(path string) map[string]Entry {
return fs.dirs[path]
}
func (fs *mockFS) ReadFile(path string) (string, bool) {
contents, ok := fs.files[path]
return contents, ok
}
func (*mockFS) Dir(p string) string {
return path.Dir(p)
}
func (*mockFS) Base(p string) string {
return path.Base(p)
}
func (*mockFS) Join(parts ...string) string {
return path.Clean(path.Join(parts...))
}
func (*mockFS) RelativeToCwd(path string) (string, bool) {
return "", false
}
////////////////////////////////////////////////////////////////////////////////
type realFS struct {
// Stores the file entries for directories we've listed before
entriesMutex sync.RWMutex
entries map[string]map[string]Entry
// For the current working directory
cwd string
cwdOk bool
}
func RealFS() FS {
cwd, cwdErr := os.Getwd()
return &realFS{
entries: make(map[string]map[string]Entry),
cwd: cwd,
cwdOk: cwdErr == nil,
}
}
func (fs *realFS) ReadDirectory(dir string) map[string]Entry {
// First, check the cache
cached, ok := func() (map[string]Entry, bool) {
fs.entriesMutex.RLock()
defer fs.entriesMutex.RUnlock()
cached, ok := fs.entries[dir]
return cached, ok
}()
// Cache hit: stop now
if ok {
return cached
}
// Cache miss: read the directory entries
names, err := readdir(dir)
entries := make(map[string]Entry)
if err == nil {
for _, name := range names {
entryPath := filepath.Join(dir, name)
// Use "lstat" since we want information about symbolic links
if stat, err := os.Lstat(entryPath); err == nil {
mode := stat.Mode()
symlink := ""
// Follow symlinks now so the cache contains the translation
if (mode & os.ModeSymlink) != 0 {
link, err := os.Readlink(entryPath)
if err != nil {
continue // Skip over this entry
}
symlink = filepath.Clean(filepath.Join(dir, link))
// Re-run "lstat" on the symlink target
stat2, err2 := os.Lstat(symlink)
if err2 != nil {
continue // Skip over this entry
}
mode = stat2.Mode()
if (mode & os.ModeSymlink) != 0 {
continue // Symlink chains are not supported
}
}
// We consider the entry either a directory or a file
if (mode & os.ModeDir) != 0 {
entries[name] = Entry{Kind: DirEntry, Symlink: symlink}
} else {
entries[name] = Entry{Kind: FileEntry, Symlink: symlink}
}
}
}
}
// Update the cache unconditionally. Even if the read failed, we don't want to
// retry again later. The directory is inaccessible so trying again is wasted.
fs.entriesMutex.Lock()
defer fs.entriesMutex.Unlock()
if err != nil {
fs.entries[dir] = nil
return nil
}
fs.entries[dir] = entries
return entries
}
func (fs *realFS) ReadFile(path string) (string, bool) {
buffer, err := ioutil.ReadFile(path)
return string(buffer), err == nil
}
func (*realFS) Dir(p string) string {
return filepath.Dir(p)
}
func (*realFS) Base(p string) string {
return filepath.Base(p)
}
func (*realFS) Join(parts ...string) string {
return filepath.Clean(filepath.Join(parts...))
}
func (fs *realFS) RelativeToCwd(path string) (string, bool) {
if fs.cwdOk {
if rel, err := filepath.Rel(fs.cwd, path); err == nil {
return rel, true
}
}
return "", false
}
func readdir(dirname string) ([]string, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
defer f.Close()
return f.Readdirnames(-1)
}