530 lines
13 KiB
Go
530 lines
13 KiB
Go
|
package fs
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"syscall"
|
||
|
)
|
||
|
|
||
|
type realFS struct {
|
||
|
// Stores the file entries for directories we've listed before
|
||
|
entriesMutex sync.Mutex
|
||
|
entries map[string]entriesOrErr
|
||
|
|
||
|
// If true, do not use the "entries" cache
|
||
|
doNotCacheEntries bool
|
||
|
|
||
|
// This stores data that will end up being returned by "WatchData()"
|
||
|
watchMutex sync.Mutex
|
||
|
watchData map[string]privateWatchData
|
||
|
|
||
|
// When building with WebAssembly, the Go compiler doesn't correctly handle
|
||
|
// platform-specific path behavior. Hack around these bugs by compiling
|
||
|
// support for both Unix and Windows paths into all executables and switch
|
||
|
// between them at run-time instead.
|
||
|
fp goFilepath
|
||
|
}
|
||
|
|
||
|
type entriesOrErr struct {
|
||
|
entries DirEntries
|
||
|
canonicalError error
|
||
|
originalError error
|
||
|
}
|
||
|
|
||
|
type watchState uint8
|
||
|
|
||
|
const (
|
||
|
stateNone watchState = iota
|
||
|
stateDirHasAccessedEntries // Compare "accessedEntries"
|
||
|
stateDirMissing // Compare directory presence
|
||
|
stateFileHasModKey // Compare "modKey"
|
||
|
stateFileNeedModKey // Need to transition to "stateFileHasModKey" or "stateFileUnusableModKey" before "WatchData()" returns
|
||
|
stateFileMissing // Compare file presence
|
||
|
stateFileUnusableModKey // Compare "fileContents"
|
||
|
)
|
||
|
|
||
|
type privateWatchData struct {
|
||
|
accessedEntries *accessedEntries
|
||
|
fileContents string
|
||
|
modKey ModKey
|
||
|
state watchState
|
||
|
}
|
||
|
|
||
|
type RealFSOptions struct {
|
||
|
WantWatchData bool
|
||
|
AbsWorkingDir string
|
||
|
DoNotCache bool
|
||
|
}
|
||
|
|
||
|
func RealFS(options RealFSOptions) (FS, error) {
|
||
|
var fp goFilepath
|
||
|
if CheckIfWindows() {
|
||
|
fp.isWindows = true
|
||
|
fp.pathSeparator = '\\'
|
||
|
} else {
|
||
|
fp.isWindows = false
|
||
|
fp.pathSeparator = '/'
|
||
|
}
|
||
|
|
||
|
// Come up with a default working directory if one was not specified
|
||
|
fp.cwd = options.AbsWorkingDir
|
||
|
if fp.cwd == "" {
|
||
|
if cwd, err := os.Getwd(); err == nil {
|
||
|
fp.cwd = cwd
|
||
|
} else if fp.isWindows {
|
||
|
fp.cwd = "C:\\"
|
||
|
} else {
|
||
|
fp.cwd = "/"
|
||
|
}
|
||
|
} else if !fp.isAbs(fp.cwd) {
|
||
|
return nil, fmt.Errorf("The working directory %q is not an absolute path", fp.cwd)
|
||
|
}
|
||
|
|
||
|
// Resolve symlinks in the current working directory. Symlinks are resolved
|
||
|
// when input file paths are converted to absolute paths because we need to
|
||
|
// recognize an input file as unique even if it has multiple symlinks
|
||
|
// pointing to it. The build will generate relative paths from the current
|
||
|
// working directory to the absolute input file paths for error messages,
|
||
|
// so the current working directory should be processed the same way. Not
|
||
|
// doing this causes test failures with esbuild when run from inside a
|
||
|
// symlinked directory.
|
||
|
//
|
||
|
// This deliberately ignores errors due to e.g. infinite loops. If there is
|
||
|
// an error, we will just use the original working directory and likely
|
||
|
// encounter an error later anyway. And if we don't encounter an error
|
||
|
// later, then the current working directory didn't even matter and the
|
||
|
// error is unimportant.
|
||
|
if path, err := fp.evalSymlinks(fp.cwd); err == nil {
|
||
|
fp.cwd = path
|
||
|
}
|
||
|
|
||
|
// Only allocate memory for watch data if necessary
|
||
|
var watchData map[string]privateWatchData
|
||
|
if options.WantWatchData {
|
||
|
watchData = make(map[string]privateWatchData)
|
||
|
}
|
||
|
|
||
|
return &realFS{
|
||
|
entries: make(map[string]entriesOrErr),
|
||
|
fp: fp,
|
||
|
watchData: watchData,
|
||
|
doNotCacheEntries: options.DoNotCache,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) ReadDirectory(dir string) (entries DirEntries, canonicalError error, originalError error) {
|
||
|
if !fs.doNotCacheEntries {
|
||
|
// First, check the cache
|
||
|
cached, ok := func() (cached entriesOrErr, ok bool) {
|
||
|
fs.entriesMutex.Lock()
|
||
|
defer fs.entriesMutex.Unlock()
|
||
|
cached, ok = fs.entries[dir]
|
||
|
return
|
||
|
}()
|
||
|
if ok {
|
||
|
// Cache hit: stop now
|
||
|
return cached.entries, cached.canonicalError, cached.originalError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Cache miss: read the directory entries
|
||
|
names, canonicalError, originalError := fs.readdir(dir)
|
||
|
entries = DirEntries{dir, make(map[string]*Entry), nil}
|
||
|
|
||
|
// Unwrap to get the underlying error
|
||
|
if pathErr, ok := canonicalError.(*os.PathError); ok {
|
||
|
canonicalError = pathErr.Unwrap()
|
||
|
}
|
||
|
|
||
|
if canonicalError == nil {
|
||
|
for _, name := range names {
|
||
|
// Call "stat" lazily for performance. The "@material-ui/icons" package
|
||
|
// contains a directory with over 11,000 entries in it and running "stat"
|
||
|
// for each entry was a big performance issue for that package.
|
||
|
entries.data[strings.ToLower(name)] = &Entry{
|
||
|
dir: dir,
|
||
|
base: name,
|
||
|
needStat: true,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Store data for watch mode
|
||
|
if fs.watchData != nil {
|
||
|
defer fs.watchMutex.Unlock()
|
||
|
fs.watchMutex.Lock()
|
||
|
state := stateDirHasAccessedEntries
|
||
|
if canonicalError != nil {
|
||
|
state = stateDirMissing
|
||
|
}
|
||
|
entries.accessedEntries = &accessedEntries{wasPresent: make(map[string]bool)}
|
||
|
fs.watchData[dir] = privateWatchData{
|
||
|
accessedEntries: entries.accessedEntries,
|
||
|
state: state,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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.
|
||
|
if canonicalError != nil {
|
||
|
entries.data = nil
|
||
|
}
|
||
|
if !fs.doNotCacheEntries {
|
||
|
fs.entriesMutex.Lock()
|
||
|
defer fs.entriesMutex.Unlock()
|
||
|
fs.entries[dir] = entriesOrErr{
|
||
|
entries: entries,
|
||
|
canonicalError: canonicalError,
|
||
|
originalError: originalError,
|
||
|
}
|
||
|
}
|
||
|
return entries, canonicalError, originalError
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) ReadFile(path string) (contents string, canonicalError error, originalError error) {
|
||
|
BeforeFileOpen()
|
||
|
defer AfterFileClose()
|
||
|
buffer, originalError := ioutil.ReadFile(path)
|
||
|
canonicalError = fs.canonicalizeError(originalError)
|
||
|
|
||
|
// Allocate the string once
|
||
|
fileContents := string(buffer)
|
||
|
|
||
|
// Store data for watch mode
|
||
|
if fs.watchData != nil {
|
||
|
defer fs.watchMutex.Unlock()
|
||
|
fs.watchMutex.Lock()
|
||
|
data, ok := fs.watchData[path]
|
||
|
if canonicalError != nil {
|
||
|
data.state = stateFileMissing
|
||
|
} else if !ok {
|
||
|
data.state = stateFileNeedModKey
|
||
|
}
|
||
|
data.fileContents = fileContents
|
||
|
fs.watchData[path] = data
|
||
|
}
|
||
|
|
||
|
return fileContents, canonicalError, originalError
|
||
|
}
|
||
|
|
||
|
type realOpenedFile struct {
|
||
|
handle *os.File
|
||
|
len int
|
||
|
}
|
||
|
|
||
|
func (f *realOpenedFile) Len() int {
|
||
|
return f.len
|
||
|
}
|
||
|
|
||
|
func (f *realOpenedFile) Read(start int, end int) ([]byte, error) {
|
||
|
bytes := make([]byte, end-start)
|
||
|
remaining := bytes
|
||
|
|
||
|
_, err := f.handle.Seek(int64(start), io.SeekStart)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
for len(remaining) > 0 {
|
||
|
n, err := f.handle.Read(remaining)
|
||
|
if err != nil && n <= 0 {
|
||
|
return nil, err
|
||
|
}
|
||
|
remaining = remaining[n:]
|
||
|
}
|
||
|
|
||
|
return bytes, nil
|
||
|
}
|
||
|
|
||
|
func (f *realOpenedFile) Close() error {
|
||
|
return f.handle.Close()
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) OpenFile(path string) (OpenedFile, error, error) {
|
||
|
BeforeFileOpen()
|
||
|
defer AfterFileClose()
|
||
|
|
||
|
f, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return nil, fs.canonicalizeError(err), err
|
||
|
}
|
||
|
|
||
|
info, err := f.Stat()
|
||
|
if err != nil {
|
||
|
f.Close()
|
||
|
return nil, fs.canonicalizeError(err), err
|
||
|
}
|
||
|
|
||
|
return &realOpenedFile{f, int(info.Size())}, nil, nil
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) ModKey(path string) (ModKey, error) {
|
||
|
BeforeFileOpen()
|
||
|
defer AfterFileClose()
|
||
|
key, err := modKey(path)
|
||
|
|
||
|
// Store data for watch mode
|
||
|
if fs.watchData != nil {
|
||
|
defer fs.watchMutex.Unlock()
|
||
|
fs.watchMutex.Lock()
|
||
|
data, ok := fs.watchData[path]
|
||
|
if !ok {
|
||
|
if err == modKeyUnusable {
|
||
|
data.state = stateFileUnusableModKey
|
||
|
} else if err != nil {
|
||
|
data.state = stateFileMissing
|
||
|
} else {
|
||
|
data.state = stateFileHasModKey
|
||
|
}
|
||
|
} else if data.state == stateFileNeedModKey {
|
||
|
data.state = stateFileHasModKey
|
||
|
}
|
||
|
data.modKey = key
|
||
|
fs.watchData[path] = data
|
||
|
}
|
||
|
|
||
|
return key, err
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) IsAbs(p string) bool {
|
||
|
return fs.fp.isAbs(p)
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Abs(p string) (string, bool) {
|
||
|
abs, err := fs.fp.abs(p)
|
||
|
return abs, err == nil
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Dir(p string) string {
|
||
|
return fs.fp.dir(p)
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Base(p string) string {
|
||
|
return fs.fp.base(p)
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Ext(p string) string {
|
||
|
return fs.fp.ext(p)
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Join(parts ...string) string {
|
||
|
return fs.fp.clean(fs.fp.join(parts))
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Cwd() string {
|
||
|
return fs.fp.cwd
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) Rel(base string, target string) (string, bool) {
|
||
|
if rel, err := fs.fp.rel(base, target); err == nil {
|
||
|
return rel, true
|
||
|
}
|
||
|
return "", false
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) {
|
||
|
BeforeFileOpen()
|
||
|
defer AfterFileClose()
|
||
|
f, originalError := os.Open(dirname)
|
||
|
canonicalError = fs.canonicalizeError(originalError)
|
||
|
|
||
|
// Stop now if there was an error
|
||
|
if canonicalError != nil {
|
||
|
return nil, canonicalError, originalError
|
||
|
}
|
||
|
|
||
|
defer f.Close()
|
||
|
entries, err := f.Readdirnames(-1)
|
||
|
|
||
|
// Unwrap to get the underlying error
|
||
|
if syscallErr, ok := err.(*os.SyscallError); ok {
|
||
|
err = syscallErr.Unwrap()
|
||
|
}
|
||
|
|
||
|
// Don't convert ENOTDIR to ENOENT here. ENOTDIR is a legitimate error
|
||
|
// condition for Readdirnames() on non-Windows platforms.
|
||
|
|
||
|
return entries, canonicalError, originalError
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) canonicalizeError(err error) error {
|
||
|
// Unwrap to get the underlying error
|
||
|
if pathErr, ok := err.(*os.PathError); ok {
|
||
|
err = pathErr.Unwrap()
|
||
|
}
|
||
|
|
||
|
// This has been copied from golang.org/x/sys/windows
|
||
|
const ERROR_INVALID_NAME syscall.Errno = 123
|
||
|
|
||
|
// Windows is much more restrictive than Unix about file names. If a file name
|
||
|
// is invalid, it will return ERROR_INVALID_NAME. Treat this as ENOENT (i.e.
|
||
|
// "the file does not exist") so that the resolver continues trying to resolve
|
||
|
// the path on this failure instead of aborting with an error.
|
||
|
if fs.fp.isWindows && err == ERROR_INVALID_NAME {
|
||
|
err = syscall.ENOENT
|
||
|
}
|
||
|
|
||
|
// Windows returns ENOTDIR here even though nothing we've done yet has asked
|
||
|
// for a directory. This really means ENOENT on Windows. Return ENOENT here
|
||
|
// so callers that check for ENOENT will successfully detect this file as
|
||
|
// missing.
|
||
|
if err == syscall.ENOTDIR {
|
||
|
err = syscall.ENOENT
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) {
|
||
|
entryPath := fs.fp.join([]string{dir, base})
|
||
|
|
||
|
// Use "lstat" since we want information about symbolic links
|
||
|
BeforeFileOpen()
|
||
|
defer AfterFileClose()
|
||
|
stat, err := os.Lstat(entryPath)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
mode := stat.Mode()
|
||
|
|
||
|
// Follow symlinks now so the cache contains the translation
|
||
|
if (mode & os.ModeSymlink) != 0 {
|
||
|
symlink = entryPath
|
||
|
linksWalked := 0
|
||
|
for {
|
||
|
linksWalked++
|
||
|
if linksWalked > 255 {
|
||
|
return // Error: too many links
|
||
|
}
|
||
|
link, err := os.Readlink(symlink)
|
||
|
if err != nil {
|
||
|
return // Skip over this entry
|
||
|
}
|
||
|
if !fs.fp.isAbs(link) {
|
||
|
link = fs.fp.join([]string{dir, link})
|
||
|
}
|
||
|
symlink = fs.fp.clean(link)
|
||
|
|
||
|
// Re-run "lstat" on the symlink target
|
||
|
stat2, err2 := os.Lstat(symlink)
|
||
|
if err2 != nil {
|
||
|
return // Skip over this entry
|
||
|
}
|
||
|
mode = stat2.Mode()
|
||
|
if (mode & os.ModeSymlink) == 0 {
|
||
|
break
|
||
|
}
|
||
|
dir = fs.fp.dir(symlink)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We consider the entry either a directory or a file
|
||
|
if (mode & os.ModeDir) != 0 {
|
||
|
kind = DirEntry
|
||
|
} else {
|
||
|
kind = FileEntry
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (fs *realFS) WatchData() WatchData {
|
||
|
paths := make(map[string]func() string)
|
||
|
|
||
|
for path, data := range fs.watchData {
|
||
|
// Each closure below needs its own copy of these loop variables
|
||
|
path := path
|
||
|
data := data
|
||
|
|
||
|
// Each function should return true if the state has been changed
|
||
|
if data.state == stateFileNeedModKey {
|
||
|
key, err := modKey(path)
|
||
|
if err == modKeyUnusable {
|
||
|
data.state = stateFileUnusableModKey
|
||
|
} else if err != nil {
|
||
|
data.state = stateFileMissing
|
||
|
} else {
|
||
|
data.state = stateFileHasModKey
|
||
|
data.modKey = key
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch data.state {
|
||
|
case stateDirMissing:
|
||
|
paths[path] = func() string {
|
||
|
info, err := os.Stat(path)
|
||
|
if err == nil && info.IsDir() {
|
||
|
return path
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
case stateDirHasAccessedEntries:
|
||
|
paths[path] = func() string {
|
||
|
names, err, _ := fs.readdir(path)
|
||
|
if err != nil {
|
||
|
return path
|
||
|
}
|
||
|
data.accessedEntries.mutex.Lock()
|
||
|
defer data.accessedEntries.mutex.Unlock()
|
||
|
if allEntries := data.accessedEntries.allEntries; allEntries != nil {
|
||
|
// Check all entries
|
||
|
if len(names) != len(allEntries) {
|
||
|
return path
|
||
|
}
|
||
|
sort.Strings(names)
|
||
|
for i, s := range names {
|
||
|
if s != allEntries[i] {
|
||
|
return path
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
// Check individual entries
|
||
|
isPresent := make(map[string]bool, len(names))
|
||
|
for _, name := range names {
|
||
|
isPresent[strings.ToLower(name)] = true
|
||
|
}
|
||
|
for name, wasPresent := range data.accessedEntries.wasPresent {
|
||
|
if wasPresent != isPresent[name] {
|
||
|
return fs.Join(path, name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
case stateFileMissing:
|
||
|
paths[path] = func() string {
|
||
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||
|
return path
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
case stateFileHasModKey:
|
||
|
paths[path] = func() string {
|
||
|
if key, err := modKey(path); err != nil || key != data.modKey {
|
||
|
return path
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
case stateFileUnusableModKey:
|
||
|
paths[path] = func() string {
|
||
|
if buffer, err := ioutil.ReadFile(path); err != nil || string(buffer) != data.fileContents {
|
||
|
return path
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return WatchData{
|
||
|
Paths: paths,
|
||
|
}
|
||
|
}
|