-
Notifications
You must be signed in to change notification settings - Fork 343
/
fshasher.go
139 lines (111 loc) · 2.85 KB
/
fshasher.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
// Package fshasher computes a fingerprint for an FS tree for testing purposes
package fshasher
import (
"archive/tar"
"context"
"io"
"os"
"path"
"time"
"github.com/pkg/errors"
"golang.org/x/crypto/blake2s"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/internal/iocopy"
"github.com/kopia/kopia/repo/logging"
)
var log = logging.GetContextLoggerFunc("kopia/internal/fshasher")
// Hash computes a recursive hash of e using the given hasher h.
func Hash(ctx context.Context, e fs.Entry) ([]byte, error) {
h, err := blake2s.New256(nil)
if err != nil {
return nil, err
}
tw := tar.NewWriter(h)
defer tw.Close() //nolint:errcheck
if err := write(ctx, tw, "", e); err != nil {
return nil, err
}
if err := tw.Flush(); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// nolint:interfacer
func write(ctx context.Context, tw *tar.Writer, fullpath string, e fs.Entry) error {
h, err := header(ctx, fullpath, e)
if err != nil {
return err
}
log(ctx).Debugf("%v %v %v %v %v", e.Mode(), h.ModTime.Format(time.RFC3339), h.Size, h.Name, h.Linkname)
if err := tw.WriteHeader(h); err != nil {
return err
}
switch e := e.(type) {
case fs.Directory:
return writeDirectory(ctx, tw, fullpath, e)
case fs.File:
return writeFile(ctx, tw, e)
case fs.Symlink:
// link target is part of the header
return nil
default: // bare fs.Entry
return nil
}
}
func header(ctx context.Context, fullpath string, e os.FileInfo) (*tar.Header, error) {
var link string
if sl, ok := e.(fs.Symlink); ok {
l, err := sl.Readlink(ctx)
if err != nil {
return nil, err
}
link = l
}
h, err := tar.FileInfoHeader(e, link)
if err != nil {
return nil, err
}
h.Name = fullpath
// clear fields that may cause spurious differences
if e.IsDir() {
// reset times for directories given how ModTime is set in
// snapshot directories
h.ModTime = time.Time{}
}
// when hashing, compare time to within a second resolution because of
// filesystems that don't preserve full timestamp fidelity.
// https://travis-ci.org/github/kopia/kopia/jobs/732592885
h.ModTime = h.ModTime.Truncate(time.Second).UTC()
h.AccessTime = h.ModTime
h.ChangeTime = h.ModTime
if sl, ok := e.(fs.Symlink); ok {
h.Linkname, err = sl.Readlink(ctx)
if err != nil {
return nil, errors.Wrap(err, "error reading link")
}
}
return h, nil
}
func writeDirectory(ctx context.Context, tw *tar.Writer, fullpath string, d fs.Directory) error {
entries, err := d.Readdir(ctx)
if err != nil {
return err
}
for _, e := range entries {
if err := write(ctx, tw, path.Join(fullpath, e.Name()), e); err != nil {
return err
}
}
return nil
}
func writeFile(ctx context.Context, w io.Writer, f fs.File) error {
r, err := f.Open(ctx)
if err != nil {
return err
}
defer r.Close() //nolint:errcheck
if _, err = iocopy.Copy(w, r); err != nil {
return err
}
return nil
}