Add snapshot files to list a snapshot's files without restoring it (closes #435)
check / check (push) Successful in 12m11s
check / check (push) Successful in 12m11s
`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. 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
This commit is contained in:
1 parent
5ca0d46932
commit
600f3a9592
8 files changed
+457
-41
No files matched your search
+6
-5
@@ -38,10 +38,11 @@ const (
|
||||
// prune, remote nuke) write the local index or the remote store. They
|
||||
// hold the PID lock so that at most one runs at a time.
|
||||
mutating lockMode = iota
|
||||
// readOnly commands (info, snapshot list, snapshot verify, remote info,
|
||||
// snapshot restore) do not write the local index or the remote store,
|
||||
// so they run without the lock and are never blocked by a running
|
||||
// mutator. restore writes only to the target directory it is given.
|
||||
// readOnly commands (info, snapshot list, snapshot files, snapshot
|
||||
// verify, remote info, snapshot restore) do not write the local index
|
||||
// or the remote store, so they run without the lock and are never
|
||||
// blocked by a running mutator. restore writes only to the target
|
||||
// directory it is given.
|
||||
readOnly
|
||||
)
|
||||
|
||||
@@ -310,7 +311,7 @@ func RunOperation(
|
||||
}
|
||||
|
||||
// runVaultikApp runs the standard single-operation command lifecycle
|
||||
// shared by the snapshot list/purge/remove and remote nuke subcommands:
|
||||
// shared by the snapshot list/files/purge/remove and remote nuke subcommands:
|
||||
// resolve the config, then run op against the Vaultik instance through
|
||||
// RunOperation, reporting a failure prefixed with failMsg on stderr. mode
|
||||
// says whether the command takes the PID lock. jsonOutput marks a command
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package cli //nolint:testpackage // shares runEntry and the backup config helpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// cmdFiles is the word of `snapshot files`, used to build argument
|
||||
// vectors below.
|
||||
const cmdFiles = "files"
|
||||
|
||||
// TestEntrySnapshotFilesListsOnlyThePath backs up two files and runs
|
||||
// `snapshot files` with the snapshot ID and the path of one of them,
|
||||
// which must be the only file listed.
|
||||
//
|
||||
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
||||
func TestEntrySnapshotFilesListsOnlyThePath(t *testing.T) {
|
||||
configPath, _ := writeBackupConfigWithPrivateKey(t)
|
||||
|
||||
// writeBackupConfig puts file.txt in the source directory beside the
|
||||
// config file.
|
||||
sourceDir := filepath.Join(filepath.Dir(configPath), "source")
|
||||
listed := filepath.Join(sourceDir, "file.txt")
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(sourceDir, "other.txt"), []byte("other"), 0o600))
|
||||
|
||||
code, _, stderr := runEntry(t,
|
||||
flagConfig, configPath, cmdSnapshot, cmdCreate)
|
||||
require.Equal(t, 0, code, stderr)
|
||||
|
||||
code, stdout, stderr := runEntry(t, flagConfig, configPath, cmdSnapshot,
|
||||
cmdFiles, onlySnapshotID(t, configPath), listed, flagJSON)
|
||||
require.Equal(t, 0, code, stderr)
|
||||
|
||||
var entries []vaultik.ListedFile
|
||||
|
||||
require.NoError(t, json.Unmarshal([]byte(stdout), &entries))
|
||||
require.Len(t, entries, 1, "only the path given may be listed")
|
||||
assert.Equal(t, listed, entries[0].Path)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func TestCLIEntry(t *testing.T) {
|
||||
} else {
|
||||
// Check snapshot subcommands
|
||||
expectedSubCommands := []string{
|
||||
"create", "list", "purge", "verify", "remove", "restore",
|
||||
"create", "list", "files", "purge", "verify", "remove", "restore",
|
||||
}
|
||||
for _, expected := range expectedSubCommands {
|
||||
found := false
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// filesMaxArgs is the maximum positional argument count of
|
||||
// `snapshot files <snapshot-id> [path]`.
|
||||
const filesMaxArgs = 2
|
||||
|
||||
var (
|
||||
errSnapshotIDRequired = errors.New("snapshot ID required")
|
||||
errWrongArgCount = errors.New("wrong argument count")
|
||||
@@ -52,6 +56,7 @@ func NewSnapshotCommand() *cobra.Command {
|
||||
// Add subcommands
|
||||
cmd.AddCommand(newSnapshotCreateCommand())
|
||||
cmd.AddCommand(newSnapshotListCommand())
|
||||
cmd.AddCommand(newSnapshotFilesCommand())
|
||||
cmd.AddCommand(newSnapshotPurgeCommand())
|
||||
cmd.AddCommand(newSnapshotVerifyCommand())
|
||||
cmd.AddCommand(newSnapshotRemoveCommand())
|
||||
@@ -172,6 +177,44 @@ func newSnapshotListCommand() *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newSnapshotFilesCommand creates the 'snapshot files' subcommand
|
||||
func newSnapshotFilesCommand() *cobra.Command {
|
||||
var jsonOutput bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "files <snapshot-id> [path]",
|
||||
Short: "List the files in a snapshot",
|
||||
Long: `Lists the path, size, mode and modification time of every file in a
|
||||
snapshot, without restoring it. With a path, lists only that path and
|
||||
what is under it; a path that matches nothing is an error.
|
||||
|
||||
The snapshot may be named by its ID or, on a host with no local index,
|
||||
by the remote key that 'snapshot list' prints for a remote-only snapshot
|
||||
(an unambiguous leading part is enough).
|
||||
|
||||
The file list is only in the snapshot's encrypted database, so this
|
||||
downloads and decrypts it, and requires the age private key in the
|
||||
VAULTIK_AGE_SECRET_KEY environment variable, as 'snapshot restore' does.`,
|
||||
Args: cobra.RangeArgs(1, filesMaxArgs),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path := ""
|
||||
if len(args) == filesMaxArgs {
|
||||
path = args[1]
|
||||
}
|
||||
|
||||
return runVaultikApp(cmd, readOnly, jsonOutput,
|
||||
"Failed to list snapshot files",
|
||||
func(v *vaultik.Vaultik) error {
|
||||
return v.ListSnapshotFiles(args[0], path, jsonOutput)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newSnapshotPurgeCommand creates the 'snapshot purge' subcommand
|
||||
func newSnapshotPurgeCommand() *cobra.Command {
|
||||
opts := &vaultik.SnapshotPurgeOptions{}
|
||||
|
||||
Reference in new issue
Block a user