ls.go 6.45 KB
Newer Older
Matt Bell's avatar
Matt Bell committed
1 2 3 4
package commands

import (
	"fmt"
5
	"io"
6
	"os"
7
	"sort"
8
	"text/tabwriter"
Jeromy's avatar
Jeromy committed
9

10
	cmdenv "github.com/ipfs/go-ipfs/core/commands/cmdenv"
11

Jakub Sztandera's avatar
Jakub Sztandera committed
12
	cmds "github.com/ipfs/go-ipfs-cmds"
13 14
	unixfs "github.com/ipfs/go-unixfs"
	unixfs_pb "github.com/ipfs/go-unixfs/pb"
Jakub Sztandera's avatar
Jakub Sztandera committed
15 16
	iface "github.com/ipfs/interface-go-ipfs-core"
	options "github.com/ipfs/interface-go-ipfs-core/options"
17
	path "github.com/ipfs/interface-go-ipfs-core/path"
Matt Bell's avatar
Matt Bell committed
18 19
)

20
// LsLink contains printable data for a single ipld link in ls output
21
type LsLink struct {
Matt Bell's avatar
Matt Bell committed
22 23
	Name, Hash string
	Size       uint64
24
	Type       unixfs_pb.Data_DataType
Steven Allen's avatar
Steven Allen committed
25
	Target     string
Matt Bell's avatar
Matt Bell committed
26 27
}

28
// LsObject is an element of LsOutput
29
// It can represent all or part of a directory
30
type LsObject struct {
31 32
	Hash  string
	Links []LsLink
33 34
}

35 36
// LsOutput is a set of printable data for directories,
// it can be complete or partial
37
type LsOutput struct {
38
	Objects []LsObject
39 40
}

Kejie Zhang's avatar
Kejie Zhang committed
41 42 43
const (
	lsHeadersOptionNameTime = "headers"
	lsResolveTypeOptionName = "resolve-type"
44
	lsSizeOptionName        = "size"
45
	lsStreamOptionName      = "stream"
Kejie Zhang's avatar
Kejie Zhang committed
46 47
)

48
var LsCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
49
	Helptext: cmds.HelpText{
50
		Tagline: "List directory contents for Unix filesystem objects.",
51
		ShortDescription: `
52 53
Displays the contents of an IPFS or IPNS object(s) at the given path, with
the following format:
54 55

  <link base58 hash> <link size in bytes> <link name>
56 57

The JSON output contains type information.
58
`,
59
	},
60

Steven Allen's avatar
Steven Allen committed
61 62
	Arguments: []cmds.Argument{
		cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from.").EnableStdin(),
63
	},
Steven Allen's avatar
Steven Allen committed
64 65 66 67
	Options: []cmds.Option{
		cmds.BoolOption(lsHeadersOptionNameTime, "v", "Print table headers (Hash, Size, Name)."),
		cmds.BoolOption(lsResolveTypeOptionName, "Resolve linked objects to find out their types.").WithDefault(true),
		cmds.BoolOption(lsSizeOptionName, "Resolve linked objects to find out their file size.").WithDefault(true),
68
		cmds.BoolOption(lsStreamOptionName, "s", "Enable experimental streaming of directory entries as they are traversed."),
Henry's avatar
Henry committed
69
	},
70
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
71
		api, err := cmdenv.GetApi(env, req)
72
		if err != nil {
73
			return err
74 75
		}

76
		resolveType, _ := req.Options[lsResolveTypeOptionName].(bool)
77
		resolveSize, _ := req.Options[lsSizeOptionName].(bool)
Łukasz Magiera's avatar
Łukasz Magiera committed
78
		stream, _ := req.Options[lsStreamOptionName].(bool)
79

80 81 82 83 84
		err = req.ParseBodyArgs()
		if err != nil {
			return err
		}
		paths := req.Arguments
85

86 87 88 89 90
		enc, err := cmdenv.GetCidEncoder(req)
		if err != nil {
			return err
		}

Łukasz Magiera's avatar
Łukasz Magiera committed
91 92
		var processLink func(path string, link LsLink) error
		var dirDone func(i int)
Jan Winkelmann's avatar
Jan Winkelmann committed
93

Łukasz Magiera's avatar
Łukasz Magiera committed
94 95 96 97 98 99 100 101 102 103
		processDir := func() (func(path string, link LsLink) error, func(i int)) {
			return func(path string, link LsLink) error {
				output := []LsObject{{
					Hash:  path,
					Links: []LsLink{link},
				}}
				return res.Emit(&LsOutput{output})
			}, func(i int) {}
		}
		done := func() error { return nil }
104

105
		if !stream {
106
			output := make([]LsObject, len(req.Arguments))
107

Łukasz Magiera's avatar
Łukasz Magiera committed
108 109 110 111 112 113 114 115 116
			processDir = func() (func(path string, link LsLink) error, func(i int)) {
				// for each dir
				outputLinks := make([]LsLink, 0)
				return func(path string, link LsLink) error {
						// for each link
						outputLinks = append(outputLinks, link)
						return nil
					}, func(i int) {
						// after each dir
117 118 119 120
						sort.Slice(outputLinks, func(i, j int) bool {
							return outputLinks[i].Name < outputLinks[j].Name
						})

Łukasz Magiera's avatar
Łukasz Magiera committed
121 122 123 124
						output[i] = LsObject{
							Hash:  paths[i],
							Links: outputLinks,
						}
125 126 127
					}
			}

Łukasz Magiera's avatar
Łukasz Magiera committed
128 129 130
			done = func() error {
				return cmds.EmitOnce(res, &LsOutput{output})
			}
131 132
		}

Łukasz Magiera's avatar
Łukasz Magiera committed
133
		for i, fpath := range paths {
134
			results, err := api.Unixfs().Ls(req.Context, path.New(fpath),
135
				options.Unixfs.ResolveChildren(resolveSize || resolveType))
Łukasz Magiera's avatar
Łukasz Magiera committed
136 137
			if err != nil {
				return err
Jeromy's avatar
Jeromy committed
138 139
			}

Łukasz Magiera's avatar
Łukasz Magiera committed
140 141 142 143
			processLink, dirDone = processDir()
			for link := range results {
				if link.Err != nil {
					return link.Err
144
				}
145 146 147 148 149 150 151 152 153
				var ftype unixfs_pb.Data_DataType
				switch link.Type {
				case iface.TFile:
					ftype = unixfs.TFile
				case iface.TDirectory:
					ftype = unixfs.TDirectory
				case iface.TSymlink:
					ftype = unixfs.TSymlink
				}
Łukasz Magiera's avatar
Łukasz Magiera committed
154
				lsLink := LsLink{
155 156
					Name: link.Name,
					Hash: enc.Encode(link.Cid),
Łukasz Magiera's avatar
Łukasz Magiera committed
157

Steven Allen's avatar
Steven Allen committed
158 159 160
					Size:   link.Size,
					Type:   ftype,
					Target: link.Target,
161
				}
Łukasz Magiera's avatar
Łukasz Magiera committed
162
				if err := processLink(paths[i], lsLink); err != nil {
163
					return err
Matt Bell's avatar
Matt Bell committed
164 165
				}
			}
Łukasz Magiera's avatar
Łukasz Magiera committed
166
			dirDone(i)
Matt Bell's avatar
Matt Bell committed
167
		}
168
		return done()
Matt Bell's avatar
Matt Bell committed
169
	},
170 171 172 173
	PostRun: cmds.PostRunMap{
		cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
			req := res.Request()
			lastObjectHash := ""
174

175 176 177 178 179
			for {
				v, err := res.Next()
				if err != nil {
					if err == io.EOF {
						return nil
180
					}
181
					return err
182
				}
183 184
				out := v.(*LsOutput)
				lastObjectHash = tabularOutput(req, os.Stdout, out, lastObjectHash, false)
Matt Bell's avatar
Matt Bell committed
185
			}
186 187 188 189 190 191 192 193 194
		},
	},
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *LsOutput) error {
			// when streaming over HTTP using a text encoder, we cannot render breaks
			// between directories because we don't know the hash of the last
			// directory encoder
			ignoreBreaks, _ := req.Options[lsStreamOptionName].(bool)
			tabularOutput(req, w, out, "", ignoreBreaks)
195 196
			return nil
		}),
Matt Bell's avatar
Matt Bell committed
197
	},
198
	Type: LsOutput{},
Matt Bell's avatar
Matt Bell committed
199
}
200

201 202 203
func tabularOutput(req *cmds.Request, w io.Writer, out *LsOutput, lastObjectHash string, ignoreBreaks bool) string {
	headers, _ := req.Options[lsHeadersOptionNameTime].(bool)
	stream, _ := req.Options[lsStreamOptionName].(bool)
204
	size, _ := req.Options[lsSizeOptionName].(bool)
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227
	// in streaming mode we can't automatically align the tabs
	// so we take a best guess
	var minTabWidth int
	if stream {
		minTabWidth = 10
	} else {
		minTabWidth = 1
	}

	multipleFolders := len(req.Arguments) > 1

	tw := tabwriter.NewWriter(w, minTabWidth, 2, 1, ' ', 0)

	for _, object := range out.Objects {

		if !ignoreBreaks && object.Hash != lastObjectHash {
			if multipleFolders {
				if lastObjectHash != "" {
					fmt.Fprintln(tw)
				}
				fmt.Fprintf(tw, "%s:\n", object.Hash)
			}
			if headers {
228 229 230 231 232
				s := "Hash\tName"
				if size {
					s = "Hash\tSize\tName"
				}
				fmt.Fprintln(tw, s)
233 234 235 236 237
			}
			lastObjectHash = object.Hash
		}

		for _, link := range object.Links {
238 239 240 241 242 243 244 245 246 247 248 249 250 251
			var s string
			switch link.Type {
			case unixfs.TDirectory, unixfs.THAMTShard, unixfs.TMetadata:
				if size {
					s = "%[1]s\t-\t%[3]s/\n"
				} else {
					s = "%[1]s\t%[3]s/\n"
				}
			default:
				if size {
					s = "%s\t%v\t%s\n"
				} else {
					s = "%[1]s\t%[3]s\n"
				}
252 253
			}

254
			fmt.Fprintf(tw, s, link.Hash, link.Size, cmdenv.EscNonPrint(link.Name))
255 256 257 258 259
		}
	}
	tw.Flush()
	return lastObjectHash
}