diff --git a/core/commands/files.go b/core/commands/files.go index 3f7e3e6b9a9..213d8ee61e4 100644 --- a/core/commands/files.go +++ b/core/commands/files.go @@ -437,11 +437,68 @@ func getNodeFromPath(ctx context.Context, node *core.IpfsNode, api iface.CoreAPI type filesLsOutput struct { Entries []mfs.NodeListing + // Optionally set the path for the recursive listing option. + Path string +} + +func listNode(req *cmds.Request, fsNode mfs.FSNode, path string) ([]mfs.NodeListing, error) { + long, _ := req.Options[longOptionName].(bool) + + enc, err := cmdenv.GetCidEncoder(req) + if err != nil { + return nil, err + } + + switch fsn := fsNode.(type) { + case *mfs.Directory: + if !long { + var output []mfs.NodeListing + names, err := fsn.ListNames(req.Context) + if err != nil { + return nil, err + } + + for _, name := range names { + output = append(output, mfs.NodeListing{ + Name: name, + Type: int(fsn.Type()), + }) + } + return output, nil + } + listing, err := fsn.List(req.Context) + if err != nil { + return nil, err + } + return listing, nil + case *mfs.File: + // FIXME: `mfs.FSNode` should expose a `Name()` API (it stores the name + // in its inode) and we shouldn't require the path argument here. + _, name := gopath.Split(path) + out := []mfs.NodeListing{{Name: name, Type: int(fsn.Type())}} + if long { + size, err := fsn.Size() + if err != nil { + return nil, err + } + out[0].Size = size + + nd, err := fsn.GetNode() + if err != nil { + return nil, err + } + out[0].Hash = enc.Encode(nd.Cid()) + } + return out, nil + default: + return nil, errors.New("unrecognized type") + } } const ( - longOptionName = "long" - dontSortOptionName = "U" + longOptionName = "long" + dontSortOptionName = "U" + filesLsRecursiveOptionName = "recursive" ) var filesLsCmd = &cmds.Command{ @@ -471,6 +528,7 @@ Examples: Options: []cmds.Option{ cmds.BoolOption(longOptionName, "l", "Use long listing format."), cmds.BoolOption(dontSortOptionName, "Do not sort; list entries in directory order."), + cmds.BoolOption(filesLsRecursiveOptionName, "R", "List subdirectories recursively.").WithDefault(false), }, Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { var arg string @@ -496,56 +554,59 @@ Examples: return err } - long, _ := req.Options[longOptionName].(bool) - - enc, err := cmdenv.GetCidEncoder(req) + entries, err := listNode(req, fsn, path) if err != nil { return err } - switch fsn := fsn.(type) { - case *mfs.Directory: - if !long { - var output []mfs.NodeListing - names, err := fsn.ListNames(req.Context) - if err != nil { - return err - } + recursive := req.Options[filesLsRecursiveOptionName].(bool) + if !recursive || mfs.IsFile(fsn) { + return cmds.EmitOnce(res, filesLsOutput{Entries: entries}) + } - for _, name := range names { - output = append(output, mfs.NodeListing{ - Name: name, - }) - } - return cmds.EmitOnce(res, &filesLsOutput{output}) - } - listing, err := fsn.List(req.Context) + // Recursive listing directories; mimic Unix `ls -R` behavior (BFS). + listsToProcess := make([]filesLsOutput, 1) + listsToProcess[0] = filesLsOutput{Entries: entries, Path: path} + for len(listsToProcess) > 0 { + listOut := listsToProcess[0] + listsToProcess = listsToProcess[1:] + + err = res.Emit(listOut) if err != nil { return err } - return cmds.EmitOnce(res, &filesLsOutput{listing}) - case *mfs.File: - _, name := gopath.Split(path) - out := &filesLsOutput{[]mfs.NodeListing{{Name: name}}} - if long { - out.Entries[0].Type = int(fsn.Type()) - - size, err := fsn.Size() + + for _, entry := range listOut.Entries { + if entry.Type != int(mfs.TDir) { + continue + } + dirPath, err := checkPath(listOut.Path) + if err != nil { + return err + } + if dirPath[len(dirPath)-1] != '/' { + dirPath += "/" + } + dirPath += entry.Name + + fsn, err := mfs.Lookup(nd.FilesRoot, dirPath) if err != nil { return err } - out.Entries[0].Size = size - nd, err := fsn.GetNode() + childList, err := listNode(req, fsn, dirPath) if err != nil { return err } - out.Entries[0].Hash = enc.Encode(nd.Cid()) + listsToProcess = append(listsToProcess, + filesLsOutput{Entries: childList, Path: dirPath}) } - return cmds.EmitOnce(res, out) - default: - return errors.New("unrecognized type") } + // FIXME(BLOCKING): Evaluate joining this loop with the basic non-recursive case, + // but it might not be worth it adding complexity to the most common basic case. + // (We almost never will list recursively.) + + return nil }, Encoders: cmds.EncoderMap{ cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *filesLsOutput) error { @@ -557,6 +618,10 @@ Examples: } long, _ := req.Options[longOptionName].(bool) + if out.Path != "" { + // We are listing recursively and the listed path is a directory. + fmt.Fprintf(w, "%s:\n", out.Path) + } for _, o := range out.Entries { if long { if o.Type == int(mfs.TDir) { @@ -567,6 +632,9 @@ Examples: fmt.Fprintf(w, "%s\n", o.Name) } } + if out.Path != "" { + fmt.Fprint(w, "\n") + } return nil }),