d96e6e3952
Remove change type in favor of explicit change function. Using change function makes it more difficult to unnecessarily add to the change interface. Update test apply functions to use an interface rather than a function type. Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
162 lines
3.2 KiB
Go
162 lines
3.2 KiB
Go
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
type currentPath struct {
|
|
path string
|
|
f os.FileInfo
|
|
fullPath string
|
|
}
|
|
|
|
func pathChange(lower, upper *currentPath) (ChangeKind, string) {
|
|
if lower == nil {
|
|
if upper == nil {
|
|
panic("cannot compare nil paths")
|
|
}
|
|
return ChangeKindAdd, upper.path
|
|
}
|
|
if upper == nil {
|
|
return ChangeKindDelete, lower.path
|
|
}
|
|
// TODO: compare by directory
|
|
|
|
switch i := strings.Compare(lower.path, upper.path); {
|
|
case i < 0:
|
|
// File in lower that is not in upper
|
|
return ChangeKindDelete, lower.path
|
|
case i > 0:
|
|
// File in upper that is not in lower
|
|
return ChangeKindAdd, upper.path
|
|
default:
|
|
return ChangeKindModify, upper.path
|
|
}
|
|
}
|
|
|
|
func sameFile(f1, f2 *currentPath) (bool, error) {
|
|
if os.SameFile(f1.f, f2.f) {
|
|
return true, nil
|
|
}
|
|
|
|
equalStat, err := compareSysStat(f1.f.Sys(), f2.f.Sys())
|
|
if err != nil || !equalStat {
|
|
return equalStat, err
|
|
}
|
|
|
|
if eq, err := compareCapabilities(f1.fullPath, f2.fullPath); err != nil || !eq {
|
|
return eq, err
|
|
}
|
|
|
|
// If not a directory also check size, modtime, and content
|
|
if !f1.f.IsDir() {
|
|
if f1.f.Size() != f2.f.Size() {
|
|
return false, nil
|
|
}
|
|
t1 := f1.f.ModTime()
|
|
t2 := f2.f.ModTime()
|
|
|
|
if t1.Unix() != t2.Unix() {
|
|
return false, nil
|
|
}
|
|
|
|
// If the timestamp may have been truncated in one of the
|
|
// files, check content of file to determine difference
|
|
if t1.Nanosecond() == 0 || t2.Nanosecond() == 0 {
|
|
if f1.f.Size() > 0 {
|
|
eq, err := compareFileContent(f1.fullPath, f2.fullPath)
|
|
if err != nil || !eq {
|
|
return eq, err
|
|
}
|
|
}
|
|
} else if t1.Nanosecond() != t2.Nanosecond() {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
const compareChuckSize = 32 * 1024
|
|
|
|
// compareFileContent compares the content of 2 same sized files
|
|
// by comparing each byte.
|
|
func compareFileContent(p1, p2 string) (bool, error) {
|
|
f1, err := os.Open(p1)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer f1.Close()
|
|
f2, err := os.Open(p2)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer f2.Close()
|
|
|
|
b1 := make([]byte, compareChuckSize)
|
|
b2 := make([]byte, compareChuckSize)
|
|
for {
|
|
n1, err1 := f1.Read(b1)
|
|
if err1 != nil && err1 != io.EOF {
|
|
return false, err1
|
|
}
|
|
n2, err2 := f2.Read(b2)
|
|
if err2 != nil && err2 != io.EOF {
|
|
return false, err2
|
|
}
|
|
if n1 != n2 || !bytes.Equal(b1[:n1], b2[:n2]) {
|
|
return false, nil
|
|
}
|
|
if err1 == io.EOF && err2 == io.EOF {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func pathWalk(ctx context.Context, root string, pathC chan<- *currentPath) error {
|
|
return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Rebase path
|
|
path, err = filepath.Rel(root, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path = filepath.Join(string(os.PathSeparator), path)
|
|
|
|
// Skip root
|
|
if path == string(os.PathSeparator) {
|
|
return nil
|
|
}
|
|
|
|
p := ¤tPath{
|
|
path: path,
|
|
f: f,
|
|
fullPath: filepath.Join(root, path),
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case pathC <- p:
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
func nextPath(ctx context.Context, pathC <-chan *currentPath) (*currentPath, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case p := <-pathC:
|
|
return p, nil
|
|
}
|
|
}
|