forked from rfjakob/cshatag
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcshatag.go
199 lines (182 loc) · 4.89 KB
/
cshatag.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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package main
import (
"bytes"
"crypto/sha256"
"fmt"
"io"
"os"
"runtime"
"strconv"
"strings"
"syscall"
"github.com/pkg/xattr"
)
const xattrSha256 = "user.shatag.sha256"
const xattrTs = "user.shatag.ts"
const zeroSha256 = "0000000000000000000000000000000000000000000000000000000000000000"
var GitVersion = ""
type fileTimestamp struct {
s uint64
ns uint32
}
func (ts *fileTimestamp) prettyPrint() string {
return fmt.Sprintf("%010d.%09d", ts.s, ts.ns)
}
type fileAttr struct {
ts fileTimestamp
sha256 []byte
}
func (a *fileAttr) prettyPrint() string {
return fmt.Sprintf("%s %s", string(a.sha256), a.ts.prettyPrint())
}
// getStoredAttr reads the stored extendend attributes from a file. The file
// should look like this:
//
// $ getfattr -d foo.txt
// user.shatag.sha256="dc9fe2260fd6748b29532be0ca2750a50f9eca82046b15497f127eba6dda90e8"
// user.shatag.ts="1560177334.020775051"
func getStoredAttr(f *os.File) (attr fileAttr, err error) {
attr.sha256 = []byte(zeroSha256)
val, err := xattr.FGet(f, xattrSha256)
if err == nil {
copy(attr.sha256, val)
}
val, err = xattr.FGet(f, xattrTs)
if err == nil {
parts := strings.SplitN(string(val), ".", 2)
attr.ts.s, _ = strconv.ParseUint(parts[0], 10, 64)
if len(parts) > 1 {
ns64, _ := strconv.ParseUint(parts[1], 10, 32)
attr.ts.ns = uint32(ns64)
}
}
return attr, nil
}
// getMtime reads the actual modification time of file "f" from disk.
func getMtime(f *os.File) (ts fileTimestamp) {
fi, err := f.Stat()
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
if !fi.Mode().IsRegular() {
fmt.Printf("Error: %q is not a regular file\n", f.Name())
os.Exit(3)
}
ts.s = uint64(fi.ModTime().Unix())
ts.ns = uint32(fi.ModTime().Nanosecond())
return
}
// getActualAttr reads the actual modification time and hashes the file content.
func getActualAttr(f *os.File) (attr fileAttr, err error) {
attr.sha256 = []byte(zeroSha256)
attr.ts = getMtime(f)
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
fmt.Println(err)
os.Exit(2)
}
// Check if the file was modified while we were computing the hash
ts2 := getMtime(f)
if attr.ts != ts2 {
return attr, syscall.EINPROGRESS
}
attr.sha256 = []byte(fmt.Sprintf("%x", h.Sum(nil)))
return attr, nil
}
// storeAttr stores "attr" into extended attributes.
// Should look like this afterwards:
//
// $ getfattr -d foo.txt
// user.shatag.sha256="dc9fe2260fd6748b29532be0ca2750a50f9eca82046b15497f127eba6dda90e8"
// user.shatag.ts="1560177334.020775051"
func storeAttr(f *os.File, attr fileAttr) (err error) {
if runtime.GOOS == "darwin" {
// SMB or MacOS bug: when working on an SMB mounted filesystem on a Mac, it seems the call
// to `fsetxattr` does not update the xattr but removes it instead. So it takes two runs
// of `cshatag` to update the attribute.
// To work around this issue, we remove the xattr explicitly before setting it again.
// https://github.com/rfjakob/cshatag/issues/8
xattr.FRemove(f, xattrTs)
xattr.FRemove(f, xattrSha256)
}
err = xattr.FSet(f, xattrTs, []byte(attr.ts.prettyPrint()))
if err != nil {
return
}
err = xattr.FSet(f, xattrSha256, attr.sha256)
return
}
// printComparison prints something like this:
//
// stored: faa28bfa6332264571f28b4131b0673f0d55a31a2ccf5c873c435c235647bf76 1560177189.769244818
// actual: dc9fe2260fd6748b29532be0ca2750a50f9eca82046b15497f127eba6dda90e8 1560177334.020775051
func printComparison(stored fileAttr, actual fileAttr) {
fmt.Printf(" stored: %s\n actual: %s\n", stored.prettyPrint(), actual.prettyPrint())
}
var stats struct {
total int
errors int
inprogress int
corrupt int
outdated int
ok int
}
func checkFile(fn string) {
stats.total++
f, err := os.Open(fn)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errors++
return
}
defer f.Close()
stored, _ := getStoredAttr(f)
actual, err := getActualAttr(f)
if err == syscall.EINPROGRESS {
fmt.Printf("<concurrent modification> %s\n", fn)
stats.inprogress++
return
}
if stored.ts == actual.ts {
if bytes.Equal(stored.sha256, actual.sha256) {
fmt.Printf("<ok> %s\n", fn)
stats.ok++
return
}
fmt.Fprintf(os.Stderr, "Error: corrupt file %q\n", fn)
fmt.Printf("<corrupt> %s\n", fn)
printComparison(stored, actual)
stats.corrupt++
return
}
fmt.Printf("<outdated> %s\n", fn)
printComparison(stored, actual)
err = storeAttr(f, actual)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
stats.errors++
return
}
stats.outdated++
}
func main() {
const myname = "cshatag"
if GitVersion == "" {
GitVersion = "(version unknown)"
}
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "%s %s\n", myname, GitVersion)
fmt.Fprintf(os.Stderr, "Usage: %s FILE [FILE ...]\n", myname)
os.Exit(1)
}
for _, fn := range os.Args[1:] {
checkFile(fn)
}
if (stats.ok + stats.outdated) == stats.total {
os.Exit(0)
}
if stats.corrupt > 0 {
os.Exit(5)
}
os.Exit(2)
}