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 } }