Files
mesh-catalog/modules/nats/internal/busarchive/archive.go
T
jschoubben 98ca97fc5b
mesh/check failed: ace: novox/mesh-catalog#442: checks/particulars/go.mod cannot be read to check its mesh-sdk pin: Gitea API /repos/novox/mesh-catalo…
mesh/merge-gate head 98ca97fc5b3f merged into trunk dcf6e86d0236 — pass: builds nats, sent nowhere; merging this needs a person's bus upgrade; 1 wait(s) fo…
mesh/repo-check head 98ca97fc5b3f merged into trunk dcf6e86d0236 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Verify the bus's snapshot with a tool, sharing the snapshot program's archive code (issue 1842)
The upgrade and restore steps told a person to docker exec the snapshot program; no mesh tool
did it. nats_snapshot_verify reads the newest bus.tar of the snapshots directory only.
2026-10-22 14:24:25 +02:00

163 lines
4.2 KiB
Go

package busarchive
import (
"archive/tar"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
// Archive is a snapshot read back: its manifest, and each file beside it unpacked into a scratch
// directory of its own, removed by Close.
type Archive struct {
Manifest Manifest
dir string
files map[string]string
}
// ReadArchive unpacks a snapshot. Only the files the manifest names are kept, and only under names
// that are one stream's directory: an archive is data, and a path in it is never trusted to say where
// on the machine a file goes.
func ReadArchive(r io.Reader, scratch string) (*Archive, error) {
dir, err := os.MkdirTemp(scratch, "mesh-nats-restore-*")
if err != nil {
return nil, err
}
a := &Archive{dir: dir, files: map[string]string{}}
tr := tar.NewReader(r)
first := true
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
a.Close()
return nil, fmt.Errorf("the archive does not read: %w", err)
}
if first {
if hdr.Name != ManifestName {
a.Close()
return nil, fmt.Errorf("the archive starts with %q, not its manifest: it is not a snapshot of the bus", hdr.Name)
}
raw, err := io.ReadAll(io.LimitReader(tr, 64<<20))
if err != nil {
a.Close()
return nil, err
}
if err := json.Unmarshal(raw, &a.Manifest); err != nil {
a.Close()
return nil, fmt.Errorf("its manifest: %w", err)
}
if a.Manifest.Format != 1 {
a.Close()
return nil, fmt.Errorf("its manifest is format %d; this program reads format 1", a.Manifest.Format)
}
first = false
continue
}
if !a.named(hdr.Name) {
a.Close()
return nil, fmt.Errorf("the archive holds %q, which its manifest does not name", hdr.Name)
}
local := filepath.Join(dir, fmt.Sprintf("%04d", len(a.files)))
f, err := os.OpenFile(local, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
a.Close()
return nil, err
}
_, err = io.Copy(f, tr)
f.Close()
if err != nil {
a.Close()
return nil, err
}
a.files[hdr.Name] = local
}
if first {
a.Close()
return nil, errors.New("the archive is empty")
}
for _, s := range a.Manifest.Streams {
if !ValidName(s.Name) || s.Meta != "streams/"+s.Name+"/backup.json" || s.Archive != "streams/"+s.Name+"/stream.tar.s2" {
a.Close()
return nil, fmt.Errorf("the manifest names a stream %q at paths that are not its own", s.Name)
}
}
return a, nil
}
func (a *Archive) named(name string) bool {
for _, s := range a.Manifest.Streams {
if name == s.Meta || name == s.Archive {
return true
}
}
return false
}
// Close removes the unpacked files.
func (a *Archive) Close() { os.RemoveAll(a.dir) }
// Verify holds every file to the manifest's size and checksum, and reads every stream's archive to
// its end.
func (a *Archive) Verify() error {
for _, s := range a.Manifest.Streams {
for _, f := range []struct{ name, sum string }{{s.Meta, s.MetaSHA256}, {s.Archive, s.ArchiveSHA256}} {
local, ok := a.files[f.name]
if !ok {
return fmt.Errorf("%s is named in the manifest and missing from the archive", f.name)
}
got, size, err := sha256Of(local)
if err != nil {
return err
}
if got != f.sum {
return fmt.Errorf("%s does not match the checksum its manifest gives", f.name)
}
if f.name == s.Archive && size != s.ArchiveBytes {
return fmt.Errorf("%s is %d bytes; its manifest says %d", f.name, size, s.ArchiveBytes)
}
}
af, err := os.Open(a.files[s.Archive])
if err != nil {
return err
}
err = ReadsThrough(af)
af.Close()
if err != nil {
return fmt.Errorf("%s does not read: %w", s.Archive, err)
}
}
return nil
}
func sha256Of(path string) (string, int64, error) {
f, err := os.Open(path)
if err != nil {
return "", 0, err
}
defer f.Close()
h := sha256.New()
n, err := io.Copy(h, f)
if err != nil {
return "", 0, err
}
return hex.EncodeToString(h.Sum(nil)), n, nil
}
// Open is one stream's meta and archive.
func (a *Archive) Open(s StreamEntry) (meta []byte, archive *os.File, err error) {
meta, err = os.ReadFile(a.files[s.Meta])
if err != nil {
return nil, nil, err
}
archive, err = os.Open(a.files[s.Archive])
return meta, archive, err
}