package sync import ( "crypto/md5" "encoding/hex" "io" "os" "time" ) // NeedsSync returns true when src should be copied to dest. // // Decision logic: // - sizes differ → true (fast path) // - src is newer than dest by more than threshold → true // - sizes equal and mtime within threshold → delegate to MD5Changed func NeedsSync(src, dest FileInfo, threshold time.Duration) bool { if src.Size != dest.Size { return true } diff := src.ModTime.Sub(dest.ModTime) if diff < 0 { diff = -diff } if diff > threshold { return src.ModTime.After(dest.ModTime) } return MD5Changed(src.AbsPath, dest.AbsPath) } // MD5File computes the MD5 digest of the file at path using a streaming read. // Returns the digest as a lowercase hex string. func MD5File(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := md5.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } // MD5Changed returns true when the two files have different MD5 hashes. // Any IO error is treated as "changed" so the file will be re-copied. func MD5Changed(srcPath, destPath string) bool { srcHash, err := MD5File(srcPath) if err != nil { return true } destHash, err := MD5File(destPath) if err != nil { return true } return srcHash != destHash }