Add bounded concurrent scanner
Single-writer controller with a select loop that offers jobs and accepts results simultaneously, so bounded channels cannot deadlock. statx/fstatat relative to the directory fd on Linux/macOS, portable fallback elsewhere. Hard links counted once, symlink loops detected, virtual filesystems skipped, errors classified and surfaced.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
//go:build darwin || freebsd
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ReadDir streams the entries of a directory in chunks, stat-ing each entry
|
||||
// relative to the directory fd.
|
||||
func ReadDir(path string, chunk int, fn ChunkFunc) error {
|
||||
fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC, 0)
|
||||
if err != nil {
|
||||
return &os.PathError{Op: "open", Path: path, Err: err}
|
||||
}
|
||||
f := os.NewFile(uintptr(fd), path)
|
||||
defer f.Close()
|
||||
for {
|
||||
des, err := f.ReadDir(chunk)
|
||||
if len(des) > 0 {
|
||||
out := make([]Meta, len(des))
|
||||
for i, de := range des {
|
||||
var st unix.Stat_t
|
||||
if serr := unix.Fstatat(fd, de.Name(), &st, unix.AT_SYMLINK_NOFOLLOW); serr != nil {
|
||||
out[i] = Meta{Name: de.Name(), Err: &os.PathError{Op: "stat", Path: de.Name(), Err: serr}}
|
||||
continue
|
||||
}
|
||||
out[i] = fromStat(&st)
|
||||
out[i].Name = de.Name()
|
||||
}
|
||||
if ferr := fn(out); ferr != nil {
|
||||
return ferr
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fromStat(st *unix.Stat_t) Meta {
|
||||
return Meta{
|
||||
Mode: modeFromUnix(uint32(st.Mode)),
|
||||
Size: st.Size,
|
||||
Alloc: st.Blocks * 512,
|
||||
AllocKnown: true,
|
||||
MTime: st.Mtim.Nano(),
|
||||
BTime: st.Btim.Nano(),
|
||||
UID: st.Uid,
|
||||
GID: st.Gid,
|
||||
Nlink: uint32(st.Nlink),
|
||||
Dev: uint64(st.Dev),
|
||||
Ino: uint64(st.Ino),
|
||||
HasIno: true,
|
||||
}
|
||||
}
|
||||
|
||||
func statPath(path string, follow bool) (Meta, error) {
|
||||
var st unix.Stat_t
|
||||
var err error
|
||||
if follow {
|
||||
err = unix.Stat(path, &st)
|
||||
} else {
|
||||
err = unix.Lstat(path, &st)
|
||||
}
|
||||
if err != nil {
|
||||
return Meta{}, &os.PathError{Op: "stat", Path: path, Err: err}
|
||||
}
|
||||
m := fromStat(&st)
|
||||
m.Name = baseName(path)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// IsVirtualFS is Linux-specific; elsewhere nothing is skipped.
|
||||
func IsVirtualFS(string) bool { return false }
|
||||
Reference in New Issue
Block a user