Files
nmon/internal/backup/archive.go

120 lines
3.0 KiB
Go

package backup
import (
"archive/tar"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"
"path"
"path/filepath"
"sort"
"time"
)
// archiveWriter writes tar entries and remembers each one's size and hash for the manifest.
type archiveWriter struct {
tw *tar.Writer
mtime time.Time
files []FileEntry
}
func newArchiveWriter(w io.Writer, mtime time.Time) *archiveWriter {
return &archiveWriter{tw: tar.NewWriter(w), mtime: mtime}
}
func (a *archiveWriter) header(name string, size int64) *tar.Header {
return &tar.Header{Name: name, Mode: 0o600, Size: size, ModTime: a.mtime, Typeflag: tar.TypeReg}
}
// addFile copies the regular file at src into the archive as name.
func (a *archiveWriter) addFile(name, src string) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer func() { _ = f.Close() }() // read-only handle
st, err := f.Stat()
if err != nil {
return err
}
if err := a.tw.WriteHeader(a.header(name, st.Size())); err != nil {
return err
}
h := sha256.New()
n, err := io.Copy(io.MultiWriter(a.tw, h), f)
if err != nil {
return fmt.Errorf("archive %s: %w", name, err)
}
if n != st.Size() {
return fmt.Errorf("archive %s: file changed while being read (%d of %d bytes)", name, n, st.Size())
}
a.files = append(a.files, FileEntry{Name: name, Size: n, SHA256: hex.EncodeToString(h.Sum(nil))})
return nil
}
// addTree adds every regular file under dir as prefix/<relative path>, in sorted order. Symbolic links are
// followed: a VictoriaMetrics snapshot is a directory of links into the live data directories.
func (a *archiveWriter) addTree(prefix, dir string) error {
rels, err := listFiles(dir, "", 0)
if err != nil {
return err
}
sort.Strings(rels)
for _, rel := range rels {
if err := a.addFile(path.Join(prefix, rel), filepath.Join(dir, filepath.FromSlash(rel))); err != nil {
return err
}
}
return nil
}
const maxTreeDepth = 16
// listFiles returns the slash-separated paths of the regular files below dir/rel, following links.
func listFiles(dir, rel string, depth int) ([]string, error) {
if depth > maxTreeDepth {
return nil, fmt.Errorf("%s: directory tree too deep (link loop?)", filepath.Join(dir, rel))
}
ents, err := os.ReadDir(filepath.Join(dir, filepath.FromSlash(rel)))
if err != nil {
return nil, err
}
var out []string
for _, e := range ents {
child := path.Join(rel, e.Name())
st, err := os.Stat(filepath.Join(dir, filepath.FromSlash(child)))
if err != nil {
return nil, err
}
switch {
case st.IsDir():
sub, err := listFiles(dir, child, depth+1)
if err != nil {
return nil, err
}
out = append(out, sub...)
case st.Mode().IsRegular():
out = append(out, child)
}
}
return out, nil
}
// finish writes the manifest as the final entry and closes the tar.
func (a *archiveWriter) finish(m *Manifest) error {
m.Files = a.files
b, err := m.encode()
if err != nil {
return err
}
if err := a.tw.WriteHeader(a.header(manifestName, int64(len(b)))); err != nil {
return err
}
if _, err := a.tw.Write(b); err != nil {
return err
}
return a.tw.Close()
}