forked from TrueCloudLab/restic
146 lines
3.3 KiB
Go
146 lines
3.3 KiB
Go
package archiver
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/restic/restic/internal/debug"
|
|
"github.com/restic/restic/internal/fs"
|
|
)
|
|
|
|
// Scanner traverses the targets and calls the function Result with cumulated
|
|
// stats concerning the files and folders found. Select is used to decide which
|
|
// items should be included. Error is called when an error occurs.
|
|
type Scanner struct {
|
|
FS fs.FS
|
|
SelectByName SelectByNameFunc
|
|
Select SelectFunc
|
|
Error ErrorFunc
|
|
Result func(item string, s ScanStats)
|
|
}
|
|
|
|
// NewScanner initializes a new Scanner.
|
|
func NewScanner(fs fs.FS) *Scanner {
|
|
return &Scanner{
|
|
FS: fs,
|
|
SelectByName: func(_ string) bool { return true },
|
|
Select: func(_ string, _ os.FileInfo) bool { return true },
|
|
Error: func(_ string, err error) error { return err },
|
|
Result: func(_ string, _ ScanStats) {},
|
|
}
|
|
}
|
|
|
|
// ScanStats collect statistics.
|
|
type ScanStats struct {
|
|
Files, Dirs, Others uint
|
|
Bytes uint64
|
|
}
|
|
|
|
func (s *Scanner) scanTree(ctx context.Context, stats ScanStats, tree Tree) (ScanStats, error) {
|
|
// traverse the path in the file system for all leaf nodes
|
|
if tree.Leaf() {
|
|
abstarget, err := s.FS.Abs(tree.Path)
|
|
if err != nil {
|
|
return ScanStats{}, err
|
|
}
|
|
|
|
stats, err = s.scan(ctx, stats, abstarget)
|
|
if err != nil {
|
|
return ScanStats{}, err
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// otherwise recurse into the nodes in a deterministic order
|
|
for _, name := range tree.NodeNames() {
|
|
var err error
|
|
stats, err = s.scanTree(ctx, stats, tree.Nodes[name])
|
|
if err != nil {
|
|
return ScanStats{}, err
|
|
}
|
|
|
|
if ctx.Err() != nil {
|
|
return stats, nil
|
|
}
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// Scan traverses the targets. The function Result is called for each new item
|
|
// found, the complete result is also returned by Scan.
|
|
func (s *Scanner) Scan(ctx context.Context, targets []string) error {
|
|
debug.Log("start scan for %v", targets)
|
|
|
|
cleanTargets, err := resolveRelativeTargets(s.FS, targets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
debug.Log("clean targets %v", cleanTargets)
|
|
|
|
// we're using the same tree representation as the archiver does
|
|
tree, err := NewTree(s.FS, cleanTargets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stats, err := s.scanTree(ctx, ScanStats{}, *tree)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Result("", stats)
|
|
debug.Log("result: %+v", stats)
|
|
return nil
|
|
}
|
|
|
|
func (s *Scanner) scan(ctx context.Context, stats ScanStats, target string) (ScanStats, error) {
|
|
if ctx.Err() != nil {
|
|
return stats, nil
|
|
}
|
|
|
|
// exclude files by path before running stat to reduce number of lstat calls
|
|
if !s.SelectByName(target) {
|
|
return stats, nil
|
|
}
|
|
|
|
// get file information
|
|
fi, err := s.FS.Lstat(target)
|
|
if err != nil {
|
|
return stats, s.Error(target, err)
|
|
}
|
|
|
|
// run remaining select functions that require file information
|
|
if !s.Select(target, fi) {
|
|
return stats, nil
|
|
}
|
|
|
|
switch {
|
|
case fi.Mode().IsRegular():
|
|
stats.Files++
|
|
stats.Bytes += uint64(fi.Size())
|
|
case fi.Mode().IsDir():
|
|
names, err := fs.Readdirnames(s.FS, target, fs.O_NOFOLLOW)
|
|
if err != nil {
|
|
return stats, s.Error(target, err)
|
|
}
|
|
sort.Strings(names)
|
|
|
|
for _, name := range names {
|
|
stats, err = s.scan(ctx, stats, filepath.Join(target, name))
|
|
if err != nil {
|
|
return stats, err
|
|
}
|
|
}
|
|
stats.Dirs++
|
|
default:
|
|
stats.Others++
|
|
}
|
|
|
|
s.Result(target, stats)
|
|
return stats, nil
|
|
}
|