package volsnap import ( "archive/tar" "compress/gzip" "encoding/json" "fmt" "io" "io/fs" "os" "path" "path/filepath" ) // writeArchive serializes the given host-bind volume directories into a // gzip-compressed tar at dest. Each volume's files live under an integer // subdirectory (its manifest Index); a manifest.json at the archive root makes // the archive self-describing. Returns the manifest describing what was // captured. // // Only regular files and directories are archived. Symlinks and special files // (devices, sockets, fifos) are skipped — this keeps capture safe and avoids // recording links whose targets would be meaningless or escape the volume on a // later restore. A torn snapshot is possible if the app writes during capture; // callers should surface that caveat. func writeArchive(dest string, refs []VolumeRef) ([]SnapshotVolume, error) { // O_EXCL: never clobber an existing file (filenames are unique per call). f, err := os.OpenFile(dest, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err != nil { return nil, fmt.Errorf("create snapshot file: %w", err) } defer f.Close() gz := gzip.NewWriter(f) tw := tar.NewWriter(gz) manifest := make([]SnapshotVolume, 0, len(refs)) for i, ref := range refs { manifest = append(manifest, SnapshotVolume{Index: i, Target: ref.Target, Scope: ref.Scope, Source: ref.Source}) if err := addDir(tw, ref.HostPath, fmt.Sprintf("%d", i)); err != nil { _ = tw.Close() _ = gz.Close() _ = f.Close() os.Remove(dest) return nil, err } } if err := writeManifestEntry(tw, manifest); err != nil { _ = tw.Close() _ = gz.Close() os.Remove(dest) return nil, err } if err := tw.Close(); err != nil { _ = gz.Close() os.Remove(dest) return nil, fmt.Errorf("finalize tar: %w", err) } if err := gz.Close(); err != nil { os.Remove(dest) return nil, fmt.Errorf("finalize gzip: %w", err) } if err := f.Close(); err != nil { os.Remove(dest) return nil, fmt.Errorf("close snapshot file: %w", err) } return manifest, nil } // addDir walks root and writes its regular files and directories into tw under // the given archive prefix. func addDir(tw *tar.Writer, root, prefix string) error { return filepath.WalkDir(root, func(p string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return fmt.Errorf("walk %s: %w", p, walkErr) } // Skip symlinks and special files; archive only dirs and regular files. if d.Type()&fs.ModeSymlink != 0 { return nil } if !d.IsDir() && !d.Type().IsRegular() { return nil } rel, err := filepath.Rel(root, p) if err != nil { return fmt.Errorf("relativize %s: %w", p, err) } name := prefix if rel != "." { name = path.Join(prefix, filepath.ToSlash(rel)) } info, err := d.Info() if err != nil { return fmt.Errorf("stat %s: %w", p, err) } hdr, err := tar.FileInfoHeader(info, "") if err != nil { return fmt.Errorf("tar header %s: %w", p, err) } hdr.Name = name if d.IsDir() { hdr.Name += "/" } if err := tw.WriteHeader(hdr); err != nil { return fmt.Errorf("write tar header %s: %w", name, err) } if d.IsDir() { return nil } src, err := os.Open(p) if err != nil { return fmt.Errorf("open %s: %w", p, err) } defer src.Close() if _, err := io.Copy(tw, src); err != nil { return fmt.Errorf("copy %s: %w", p, err) } return nil }) } func writeManifestEntry(tw *tar.Writer, manifest []SnapshotVolume) error { data, err := json.MarshalIndent(manifest, "", " ") if err != nil { return fmt.Errorf("encode manifest: %w", err) } hdr := &tar.Header{Name: "manifest.json", Mode: 0o600, Size: int64(len(data)), Typeflag: tar.TypeReg} if err := tw.WriteHeader(hdr); err != nil { return fmt.Errorf("write manifest header: %w", err) } if _, err := tw.Write(data); err != nil { return fmt.Errorf("write manifest: %w", err) } return nil }