mirror of
https://github.com/kopia/kopia.git
synced 2026-03-28 02:53:05 -04:00
136 lines
2.8 KiB
Go
136 lines
2.8 KiB
Go
// 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.Module("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
|
|
|
|
return iocopy.JustCopy(w, r)
|
|
}
|