package app import ( "archive/zip" "bufio" "io" "io/fs" "os" "path/filepath" ) // blackboxArchiveCompareChunk is the buffer size used when diffing the newly // built local zip against the last one successfully written to removable // media, to find how many leading bytes are unchanged. const blackboxArchiveCompareChunk = 256 * 1024 // buildZipArchive walks root (a locally-staged, fast-storage copy of the // blackbox tree — never the slow removable-media mountpoint) and writes a // single deterministic zip to destPath: same input tree -> byte-identical // output, so two cycles that changed nothing produce identical archives and // patchArchiveOnTarget (below) can skip re-writing the unchanged prefix to // the slow target. Determinism relies on fs.WalkDir's guaranteed lexical // order and each entry's Modified time coming from the source file's mtime // (stable across cycles for files nothing touched). func buildZipArchive(root, destPath string) error { if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { return err } f, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) if err != nil { return err } zw := zip.NewWriter(f) walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.IsDir() { return nil } rel, err := filepath.Rel(root, path) if err != nil { return err } info, err := d.Info() if err != nil { return err } header, err := zip.FileInfoHeader(info) if err != nil { return err } header.Name = filepath.ToSlash(rel) header.Method = zip.Deflate w, err := zw.CreateHeader(header) if err != nil { return err } src, err := os.Open(path) if err != nil { return err } defer src.Close() _, err = io.Copy(w, src) return err }) if walkErr != nil { _ = zw.Close() _ = f.Close() return walkErr } if err := zw.Close(); err != nil { _ = f.Close() return err } return f.Close() } // commonPrefixLen returns how many leading bytes two files share. Used to // find how much of a freshly-rebuilt local zip is identical to the previous // cycle's, so patchArchiveOnTarget only has to write the changed suffix to // removable media. func commonPrefixLen(pathA, pathB string) (int64, error) { fa, err := os.Open(pathA) if err != nil { return 0, err } defer fa.Close() fb, err := os.Open(pathB) if err != nil { return 0, err } defer fb.Close() ra := bufio.NewReaderSize(fa, blackboxArchiveCompareChunk) rb := bufio.NewReaderSize(fb, blackboxArchiveCompareChunk) bufA := make([]byte, blackboxArchiveCompareChunk) bufB := make([]byte, blackboxArchiveCompareChunk) var total int64 for { na, errA := io.ReadFull(ra, bufA) nb, errB := io.ReadFull(rb, bufB) n := na if nb < n { n = nb } for i := 0; i < n; i++ { if bufA[i] != bufB[i] { return total + int64(i), nil } } total += int64(n) if na != nb || isEOFLike(errA) || isEOFLike(errB) { return total, nil } if errA != nil { return total, errA } if errB != nil { return total, errB } } } func isEOFLike(err error) bool { return err == io.EOF || err == io.ErrUnexpectedEOF } // patchArchiveOnTarget makes targetPath (on removable media, possibly // FUSE-mounted with synchronous writes) byte-identical to newLocalZipPath (on // fast local storage), writing only the changed suffix instead of the whole // file. cachedPath is our own local record of what we last wrote to // targetPath; if targetPath's size doesn't match what cachedPath implies // (first run, external tampering, a previous crash mid-write), it falls back // to writing the whole archive rather than risk corrupting it with a wrong // truncate point. func patchArchiveOnTarget(targetPath, newLocalZipPath, cachedPath string) error { newInfo, err := os.Stat(newLocalZipPath) if err != nil { return err } var prefixLen int64 if cachedInfo, err := os.Stat(cachedPath); err == nil { if targetInfo, err := os.Stat(targetPath); err == nil && targetInfo.Size() == cachedInfo.Size() { prefixLen, err = commonPrefixLen(cachedPath, newLocalZipPath) if err != nil { prefixLen = 0 } } } if prefixLen > newInfo.Size() { prefixLen = 0 } target, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0644) if err != nil { return err } defer target.Close() if err := target.Truncate(prefixLen); err != nil { return err } if _, err := target.Seek(prefixLen, io.SeekStart); err != nil { return err } src, err := os.Open(newLocalZipPath) if err != nil { return err } defer src.Close() if _, err := src.Seek(prefixLen, io.SeekStart); err != nil { return err } if _, err := io.Copy(target, src); err != nil { return err } return target.Sync() }