Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
120 lines
3.0 KiB
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()
|
|
}
|