restic/cmd/restic/cmd_ls.go

263 lines
6.7 KiB
Go
Raw Normal View History

2014-10-05 12:44:59 +00:00
package main
import (
"context"
"encoding/json"
"os"
"strings"
"time"
2014-10-05 12:44:59 +00:00
2016-09-17 10:36:05 +00:00
"github.com/spf13/cobra"
"github.com/restic/restic/internal/backend"
2017-07-23 12:21:03 +00:00
"github.com/restic/restic/internal/errors"
2018-08-11 21:25:22 +00:00
"github.com/restic/restic/internal/fs"
2017-07-24 15:42:25 +00:00
"github.com/restic/restic/internal/restic"
2018-06-09 21:31:31 +00:00
"github.com/restic/restic/internal/walker"
2014-10-05 12:44:59 +00:00
)
2016-09-17 10:36:05 +00:00
var cmdLs = &cobra.Command{
Use: "ls [flags] snapshotID [dir...]",
Short: "List files in a snapshot",
2016-09-17 10:36:05 +00:00
Long: `
2018-08-11 04:10:02 +00:00
The "ls" command lists files and directories in a snapshot.
The special snapshot ID "latest" can be used to list files and
directories of the latest snapshot in the repository. The
--host flag can be used in conjunction to select the latest
snapshot originating from a certain host only.
File listings can optionally be filtered by directories. Any
positional arguments after the snapshot ID are interpreted as
2018-08-11 21:25:22 +00:00
absolute directory paths, and only files inside those directories
will be listed. If the --recursive flag is used, then the filter
2018-08-11 04:10:02 +00:00
will allow traversing into matching directories' subfolders.
2018-08-11 21:25:22 +00:00
Any directory paths specified must be absolute (starting with
a path separator); paths use the forward slash '/' as separator.
EXIT STATUS
===========
Exit status is 0 if the command was successful, and non-zero if there was any error.
2016-09-17 10:36:05 +00:00
`,
DisableAutoGenTag: true,
2016-09-17 10:36:05 +00:00
RunE: func(cmd *cobra.Command, args []string) error {
2022-10-02 21:24:37 +00:00
return runLs(cmd.Context(), lsOptions, globalOptions, args)
2016-09-17 10:36:05 +00:00
},
}
2014-12-07 15:30:52 +00:00
2017-01-14 03:19:47 +00:00
// LsOptions collects all options for the ls command.
type LsOptions struct {
ListLong bool
restic.SnapshotFilter
Recursive bool
2017-01-14 03:19:47 +00:00
}
var lsOptions LsOptions
2016-09-17 10:36:05 +00:00
2014-11-30 21:39:58 +00:00
func init() {
2016-09-17 10:36:05 +00:00
cmdRoot.AddCommand(cmdLs)
2017-01-14 03:19:47 +00:00
flags := cmdLs.Flags()
initSingleSnapshotFilter(flags, &lsOptions.SnapshotFilter)
2017-01-14 03:19:47 +00:00
flags.BoolVarP(&lsOptions.ListLong, "long", "l", false, "use a long listing format showing size and mode")
flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
2014-11-30 21:39:58 +00:00
}
type lsSnapshot struct {
*restic.Snapshot
ID *restic.ID `json:"id"`
ShortID string `json:"short_id"`
StructType string `json:"struct_type"` // "snapshot"
}
// Print node in our custom JSON format, followed by a newline.
func lsNodeJSON(enc *json.Encoder, path string, node *restic.Node) error {
n := &struct {
Name string `json:"name"`
Type string `json:"type"`
Path string `json:"path"`
UID uint32 `json:"uid"`
GID uint32 `json:"gid"`
Size *uint64 `json:"size,omitempty"`
Mode os.FileMode `json:"mode,omitempty"`
Permissions string `json:"permissions,omitempty"`
ModTime time.Time `json:"mtime,omitempty"`
AccessTime time.Time `json:"atime,omitempty"`
ChangeTime time.Time `json:"ctime,omitempty"`
StructType string `json:"struct_type"` // "node"
size uint64 // Target for Size pointer.
}{
Name: node.Name,
Type: node.Type,
Path: path,
UID: node.UID,
GID: node.GID,
size: node.Size,
Mode: node.Mode,
Permissions: node.Mode.String(),
ModTime: node.ModTime,
AccessTime: node.AccessTime,
ChangeTime: node.ChangeTime,
StructType: "node",
}
// Always print size for regular files, even when empty,
// but never for other types.
if node.Type == "file" {
n.Size = &n.size
}
return enc.Encode(n)
}
func runLs(ctx context.Context, opts LsOptions, gopts GlobalOptions, args []string) error {
if len(args) == 0 {
return errors.Fatal("no snapshot ID specified, specify snapshot ID or use special ID 'latest'")
2014-12-07 15:30:52 +00:00
}
// extract any specific directories to walk
2018-08-11 04:10:02 +00:00
var dirs []string
if len(args) > 1 {
dirs = args[1:]
2018-08-11 21:25:22 +00:00
for _, dir := range dirs {
if !strings.HasPrefix(dir, "/") {
return errors.Fatal("All path filters must be absolute, starting with a forward slash '/'")
}
}
2018-08-11 04:10:02 +00:00
}
withinDir := func(nodepath string) bool {
if len(dirs) == 0 {
return true
}
for _, dir := range dirs {
// we're within one of the selected dirs, example:
// nodepath: "/test/foo"
// dir: "/test"
if fs.HasPathPrefix(dir, nodepath) {
return true
}
}
return false
}
approachingMatchingTree := func(nodepath string) bool {
if len(dirs) == 0 {
return true
}
for _, dir := range dirs {
// the current node path is a prefix for one of the
// directories, so we're interested in something deeper in the
// tree. Example:
// nodepath: "/test"
// dir: "/test/foo"
if fs.HasPathPrefix(nodepath, dir) {
return true
}
}
return false
}
repo, err := OpenRepository(ctx, gopts)
if err != nil {
return err
}
snapshotLister, err := backend.MemorizeList(ctx, repo.Backend(), restic.SnapshotFile)
if err != nil {
return err
}
if err = repo.LoadIndex(ctx); err != nil {
return err
}
var (
printSnapshot func(sn *restic.Snapshot)
printNode func(path string, node *restic.Node)
)
if gopts.JSON {
enc := json.NewEncoder(gopts.stdout)
printSnapshot = func(sn *restic.Snapshot) {
err := enc.Encode(lsSnapshot{
Snapshot: sn,
ID: sn.ID(),
ShortID: sn.ID().Str(),
StructType: "snapshot",
})
if err != nil {
Warnf("JSON encode failed: %v\n", err)
}
}
printNode = func(path string, node *restic.Node) {
err := lsNodeJSON(enc, path, node)
if err != nil {
Warnf("JSON encode failed: %v\n", err)
}
}
} else {
printSnapshot = func(sn *restic.Snapshot) {
Verbosef("snapshot %s of %v filtered by %v at %s):\n", sn.ID().Str(), sn.Paths, dirs, sn.Time)
}
printNode = func(path string, node *restic.Node) {
Printf("%s\n", formatNode(path, node, lsOptions.ListLong))
}
}
sn, err := (&restic.SnapshotFilter{
Hosts: opts.Hosts,
Paths: opts.Paths,
Tags: opts.Tags,
}).FindLatest(ctx, snapshotLister, repo, args[0])
if err != nil {
return err
}
2017-01-12 11:24:08 +00:00
printSnapshot(sn)
err = walker.Walk(ctx, repo, *sn.Tree, nil, func(_ restic.ID, nodepath string, node *restic.Node, err error) (bool, error) {
if err != nil {
return false, err
}
if node == nil {
return false, nil
}
2018-08-11 21:25:22 +00:00
if withinDir(nodepath) {
// if we're within a dir, print the node
printNode(nodepath, node)
// if recursive listing is requested, signal the walker that it
// should continue walking recursively
if opts.Recursive {
return false, nil
}
}
2018-08-11 21:25:22 +00:00
// if there's an upcoming match deeper in the tree (but we're not
// there yet), signal the walker to descend into any subdirs
if approachingMatchingTree(nodepath) {
2018-06-09 21:31:31 +00:00
return false, nil
}
// otherwise, signal the walker to not walk recursively into any
// subdirs
if node.Type == "dir" {
return false, walker.ErrSkipNode
2017-01-12 11:24:08 +00:00
}
return false, nil
})
if err != nil {
return err
2014-10-05 12:44:59 +00:00
}
return nil
2014-10-05 12:44:59 +00:00
}