-
Notifications
You must be signed in to change notification settings - Fork 0
/
scanner.go
146 lines (122 loc) · 3.32 KB
/
scanner.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
package archiver
import (
"context"
"os"
"path/filepath"
"sort"
"github.com/konidev20/rapi/internal/debug"
"github.com/konidev20/rapi/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(item string) bool { return true },
Select: func(item string, fi os.FileInfo) bool { return true },
Error: func(item string, err error) error { return err },
Result: func(item string, s 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 := 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
}