check / check (push) Waiting to run
`vaultik snapshot files <snapshot-id> [path] [--json]` downloads and decrypts the snapshot database, as restore does, and prints mode, size, modification time and path for every file at or under the path. A path that matches nothing is an error. It needs the private key. The mode is in Go's file-mode notation, which the README spells out. The table quotes a path that holds a non-printable character. Named `files` because `ls` is already the alias of `snapshot list`. The missing-key error no longer says "for restore", since this command and deep verify return it too. The README opening now states the private-key rule first. Model: opus-5-5
116 lines
2.6 KiB
Go
116 lines
2.6 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// ListedFile is one entry printed by `snapshot files`. Mode is the Go
|
|
// file mode string, which shows the entry type and the permission bits
|
|
// (for example -rw-r--r-- or drwxr-xr-x).
|
|
type ListedFile struct {
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
Mode string `json:"mode"`
|
|
MTime time.Time `json:"mtime"`
|
|
}
|
|
|
|
// ListSnapshotFiles prints the path, size, mode and modification time of
|
|
// every file the snapshot holds at or under path, or of every file when
|
|
// path is empty. The file list is only in the snapshot's encrypted
|
|
// database, so this downloads and decrypts it as restore does, and needs
|
|
// the private key.
|
|
func (v *Vaultik) ListSnapshotFiles(snapshotID, path string, jsonOutput bool) error {
|
|
identities, err := v.restoreIdentities()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteKey, err := v.resolveSnapshotRemoteKey(snapshotID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
db, tempDir, err := v.downloadSnapshotDB(snapshotID, remoteKey, identities)
|
|
if errors.Is(err, errWrongPrivateKey) {
|
|
return err
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("downloading snapshot database: %w", err)
|
|
}
|
|
|
|
defer v.closeSnapshotDB(db, tempDir)
|
|
|
|
repos := database.NewRepositories(db)
|
|
|
|
var files []*database.File
|
|
|
|
if path == "" {
|
|
files, err = repos.Files.ListAll(v.ctx)
|
|
} else {
|
|
files, err = repos.Files.ListUnderPath(v.ctx, filepath.Clean(path))
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("listing files: %w", err)
|
|
}
|
|
|
|
if path != "" && len(files) == 0 {
|
|
return fmt.Errorf("%w: %s", errPathsNotInSnapshot, path)
|
|
}
|
|
|
|
entries := make([]ListedFile, 0, len(files))
|
|
|
|
for _, file := range files {
|
|
entries = append(entries, ListedFile{
|
|
Path: file.Path.String(),
|
|
Size: file.Size,
|
|
Mode: os.FileMode(file.Mode).String(),
|
|
MTime: file.MTime,
|
|
})
|
|
}
|
|
|
|
if jsonOutput {
|
|
encoder := json.NewEncoder(v.Stdout)
|
|
encoder.SetIndent("", " ")
|
|
|
|
return encoder.Encode(entries)
|
|
}
|
|
|
|
if v.UI.Quiet() {
|
|
return nil
|
|
}
|
|
|
|
return v.printListedFileTable(entries)
|
|
}
|
|
|
|
// printListedFileTable writes the entries as a plain table to stdout.
|
|
func (v *Vaultik) printListedFileTable(entries []ListedFile) error {
|
|
w := tabwriter.NewWriter(v.Stdout, 0, 0, tabPadding, ' ', 0)
|
|
|
|
_, err := fmt.Fprintln(w, "MODE\tSIZE\tMODIFIED\tPATH")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
_, err = fmt.Fprintf(w, "%s\t%d\t%s\t%s\n",
|
|
entry.Mode, entry.Size,
|
|
entry.MTime.Format(time.RFC3339), ui.QuoteUnprintable(entry.Path))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return w.Flush()
|
|
}
|