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
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.
163 lines
4.2 KiB
Go
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
|
|
}
|