From 8c146eac4b8a936618426ec36d58b3b094b9530e Mon Sep 17 00:00:00 2001
From: Matthew Holt <mholt@users.noreply.github.com>
Date: Fri, 10 Aug 2018 19:48:42 -0600
Subject: [PATCH] ls: Implement directory filter, optionally subfolders

---
 cmd/restic/cmd_ls.go | 47 +++++++++++++++++++++++++++++++++++++-------
 1 file changed, 40 insertions(+), 7 deletions(-)

diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go
index f7996b438..cbfe73b42 100644
--- a/cmd/restic/cmd_ls.go
+++ b/cmd/restic/cmd_ls.go
@@ -2,6 +2,8 @@ package main
 
 import (
 	"context"
+	"path/filepath"
+	"strings"
 
 	"github.com/spf13/cobra"
 
@@ -11,7 +13,7 @@ import (
 )
 
 var cmdLs = &cobra.Command{
-	Use:   "ls [flags] [snapshot-ID ...]",
+	Use:   "ls [flags] [snapshot-ID] [dir...]",
 	Short: "List files in a snapshot",
 	Long: `
 The "ls" command allows listing files and directories in a snapshot.
@@ -26,10 +28,11 @@ The special snapshot-ID "latest" can be used to list files and directories of th
 
 // LsOptions collects all options for the ls command.
 type LsOptions struct {
-	ListLong bool
-	Host     string
-	Tags     restic.TagLists
-	Paths    []string
+	ListLong  bool
+	Host      string
+	Tags      restic.TagLists
+	Paths     []string
+	Recursive bool
 }
 
 var lsOptions LsOptions
@@ -43,6 +46,7 @@ func init() {
 	flags.StringVarP(&lsOptions.Host, "host", "H", "", "only consider snapshots for this `host`, when no snapshot ID is given")
 	flags.Var(&lsOptions.Tags, "tag", "only consider snapshots which include this `taglist`, when no snapshot ID is given")
 	flags.StringArrayVar(&lsOptions.Paths, "path", nil, "only consider snapshots which include this (absolute) `path`, when no snapshot ID is given")
+	flags.BoolVar(&lsOptions.Recursive, "recursive", false, "include files in subfolders of the listed directories")
 }
 
 func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
@@ -59,19 +63,48 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error {
 		return err
 	}
 
+	// extract any specific directories to walk
+	dirs := args[1:]
+
 	ctx, cancel := context.WithCancel(gopts.ctx)
 	defer cancel()
-	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) {
+	for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args[:1]) {
 		Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time)
 
 		err := walker.Walk(ctx, repo, *sn.Tree, nil, func(nodepath string, node *restic.Node, err error) (bool, error) {
 			if err != nil {
 				return false, err
 			}
-
 			if node == nil {
 				return false, nil
 			}
+
+			// apply any directory filters
+			if len(dirs) > 0 {
+				var nodeDir string
+				if !opts.Recursive {
+					// only needed for exact directory match; i.e. no subfolders
+					nodeDir = filepath.Dir(nodepath)
+				}
+				var match bool
+				for _, dir := range dirs {
+					if opts.Recursive {
+						if strings.HasPrefix(nodepath, dir) {
+							match = true
+							break
+						}
+					} else {
+						if nodeDir == dir {
+							match = true
+							break
+						}
+					}
+				}
+				if !match {
+					return true, nil
+				}
+			}
+
 			Printf("%s\n", formatNode(nodepath, node, lsOptions.ListLong))
 			return false, nil
 		})