Skip to content

Commit 9449c59

Browse files
author
Karrick S. McDermott
committed
HashFromPathname
1 parent 911cd22 commit 9449c59

File tree

8 files changed

+138
-0
lines changed

8 files changed

+138
-0
lines changed

internal/fs/hash.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package fs
2+
3+
import (
4+
"crypto/sha256"
5+
"fmt"
6+
"io"
7+
"os"
8+
"strconv"
9+
10+
"github.com/pkg/errors"
11+
)
12+
13+
// HashFromPathname returns a hash of the specified file or directory, ignoring
14+
// all file system objects named, `vendor` and their descendants. This function
15+
// follows symbolic links.
16+
func HashFromPathname(pathname string) (hash string, err error) {
17+
fi, err := os.Stat(pathname)
18+
if err != nil {
19+
return "", errors.Wrap(err, "could not stat")
20+
}
21+
if fi.IsDir() {
22+
return hashFromDirectory(pathname, fi)
23+
}
24+
return hashFromFile(pathname, fi)
25+
}
26+
27+
func hashFromFile(pathname string, fi os.FileInfo) (hash string, err error) {
28+
fh, err := os.Open(pathname)
29+
if err != nil {
30+
return "", errors.Wrap(err, "could not open")
31+
}
32+
defer func() {
33+
err = errors.Wrap(fh.Close(), "could not close")
34+
}()
35+
36+
h := sha256.New()
37+
_, _ = h.Write([]byte(strconv.FormatInt(fi.Size(), 10)))
38+
39+
if _, err = io.Copy(h, fh); err != nil {
40+
err = errors.Wrap(err, "could not read file")
41+
return
42+
}
43+
44+
hash = fmt.Sprintf("%x", h.Sum(nil))
45+
return
46+
}
47+
48+
func hashFromDirectory(pathname string, fi os.FileInfo) (hash string, err error) {
49+
const maxFileInfos = 32
50+
51+
fh, err := os.Open(pathname)
52+
if err != nil {
53+
return hash, errors.Wrap(err, "could not open")
54+
}
55+
defer func() {
56+
err = errors.Wrap(fh.Close(), "could not close")
57+
}()
58+
59+
h := sha256.New()
60+
61+
// NOTE: Chunk through file system objects to prevent allocating too much
62+
// memory for directories with tens of thousands of child objects.
63+
for {
64+
var children []os.FileInfo
65+
var childHash string
66+
67+
children, err = fh.Readdir(maxFileInfos)
68+
if err != nil {
69+
if err == io.EOF {
70+
err = nil
71+
break
72+
}
73+
return hash, errors.Wrap(err, "could not read directory")
74+
}
75+
for _, child := range children {
76+
switch child.Name() {
77+
case ".", "..", "vendor":
78+
// skip
79+
default:
80+
childPathname := pathname + string(os.PathSeparator) + child.Name()
81+
if childHash, err = HashFromPathname(childPathname); err != nil {
82+
err = errors.Wrap(err, "could not compute hash from pathname")
83+
return
84+
}
85+
_, _ = h.Write([]byte(childPathname))
86+
_, _ = h.Write([]byte(childHash))
87+
}
88+
}
89+
}
90+
91+
hash = fmt.Sprintf("%x", h.Sum(nil))
92+
return
93+
}

internal/fs/hash_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package fs
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestHashFromPathnameWithFile(t *testing.T) {
8+
actual, err := HashFromPathname("testdata/blob")
9+
if err != nil {
10+
t.Fatal(err)
11+
}
12+
expected := "825dc11fe41d8f604ab48a8cd6cecf304005bd82fd0228a6e411e992d4d03a08"
13+
if actual != expected {
14+
t.Errorf("Actual:\n\t%#q\nExpected:\n\t%#q", actual, expected)
15+
}
16+
}
17+
18+
func TestHashFromPathnameWithDirectory(t *testing.T) {
19+
actual, err := HashFromPathname("testdata/recursive")
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
expected := "9b3a1f1f63c0c54860799cc5464a3c380a697a3ec49ca103a62d9c09ad9fedf8"
24+
if actual != expected {
25+
t.Errorf("Actual:\n\t%#q\nExpected:\n\t%#q", actual, expected)
26+
}
27+
}

internal/fs/testdata/blob

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fjdkal;fdjskc
2+
xzc
3+
axc
4+
fdsf
5+
adsf
6+
das
7+
fd

internal/fs/testdata/recursive/blob

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fjdkal;fdjskc
2+
xzc
3+
axc
4+
fdsf
5+
adsf
6+
das
7+
fd

internal/fs/testdata/recursive/emptyFile

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fjdsakl;fd
2+
vcafcds
3+
vca
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
this file ought to be skipped

internal/fs/testdata/recursive/vendor/skip2

Whitespace-only changes.

0 commit comments

Comments
 (0)