Files
bee/audit/internal/app/blackbox_archive.go

184 lines
4.8 KiB
Go

package app
import (
"archive/zip"
"bufio"
"errors"
"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
}
_, copyErr := io.Copy(w, src)
return errors.Join(copyErr, src.Close())
})
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) (retErr 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 func() { retErr = errors.Join(retErr, 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 func() { retErr = errors.Join(retErr, 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()
}