Add filesystem inventory model
Append-only chunked node arena with stable NodeIDs, intrusive child lists, per-directory category totals and batched ancestor propagation.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrKind classifies scan errors for reporting.
|
||||
type ErrKind uint8
|
||||
|
||||
const (
|
||||
ErrPermission ErrKind = iota
|
||||
ErrVanished
|
||||
ErrIO
|
||||
ErrLoop
|
||||
ErrOther
|
||||
NumErrKinds
|
||||
)
|
||||
|
||||
func (k ErrKind) String() string {
|
||||
return [...]string{"Permission denied", "Vanished during scan", "I/O error", "Symlink loop", "Other error"}[k]
|
||||
}
|
||||
|
||||
// Classify maps an OS error to an ErrKind.
|
||||
func Classify(err error) ErrKind {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
return ErrPermission
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
return ErrVanished
|
||||
case errors.Is(err, syscall.ELOOP):
|
||||
return ErrLoop
|
||||
case errors.Is(err, syscall.EIO):
|
||||
return ErrIO
|
||||
}
|
||||
return ErrOther
|
||||
}
|
||||
|
||||
// ErrorRecord is one recorded scan error.
|
||||
type ErrorRecord struct {
|
||||
Node NodeID // node the error is attached to (the directory for listing failures)
|
||||
Name string // entry name when no node was created (e.g. vanished)
|
||||
Kind ErrKind
|
||||
Msg string
|
||||
}
|
||||
|
||||
// MaxErrorRecords caps the detailed error list; counts are always exact.
|
||||
const MaxErrorRecords = 100_000
|
||||
|
||||
// Stats describes a scan as a whole.
|
||||
type Stats struct {
|
||||
Root string
|
||||
Start, End time.Time
|
||||
Complete bool // scan finished (possibly with errors)
|
||||
Cancelled bool
|
||||
|
||||
Files, Dirs, Symlinks, Others int64
|
||||
|
||||
ErrCounts [NumErrKinds]int64
|
||||
Errors []ErrorRecord
|
||||
Excluded int64
|
||||
SkippedMounts int64
|
||||
VirtualSkipped int64
|
||||
LoopsSkipped int64
|
||||
BrokenLinks int64
|
||||
HardlinkDups int64
|
||||
Unscanned int64 // directories never read because the scan was cancelled
|
||||
|
||||
Excludes []string
|
||||
OneFileSystem bool
|
||||
Follow string
|
||||
Workers int
|
||||
FromSnapshot string
|
||||
}
|
||||
|
||||
// TotalErrors sums all error kinds.
|
||||
func (s *Stats) TotalErrors() int64 {
|
||||
var t int64
|
||||
for _, c := range s.ErrCounts {
|
||||
t += c
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// AddError records an error (the detailed list is capped).
|
||||
func (s *Stats) AddError(r ErrorRecord) {
|
||||
s.ErrCounts[r.Kind]++
|
||||
if len(s.Errors) < MaxErrorRecords {
|
||||
s.Errors = append(s.Errors, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Elapsed is the scan duration so far (or total once finished).
|
||||
func (s *Stats) Elapsed() time.Duration {
|
||||
if !s.End.IsZero() {
|
||||
return s.End.Sub(s.Start)
|
||||
}
|
||||
return time.Since(s.Start)
|
||||
}
|
||||
|
||||
// Incomplete reports whether totals may under-count real usage.
|
||||
func (s *Stats) Incomplete() bool {
|
||||
return !s.Complete || s.Cancelled || s.TotalErrors() > 0 || s.Unscanned > 0
|
||||
}
|
||||
Reference in New Issue
Block a user