ls.go 6.49 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 13
	cmdkit "github.com/ipfs/go-ipfs-cmdkit"
	cmds "github.com/ipfs/go-ipfs-cmds"
14 15
	unixfs "github.com/ipfs/go-unixfs"
	unixfs_pb "github.com/ipfs/go-unixfs/pb"
Jakub Sztandera's avatar
Jakub Sztandera committed
16 17
	iface "github.com/ipfs/interface-go-ipfs-core"
	options "github.com/ipfs/interface-go-ipfs-core/options"
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{
Jan Winkelmann's avatar
Jan Winkelmann committed
49
	Helptext: cmdkit.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

Jan Winkelmann's avatar
Jan Winkelmann committed
61 62
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from.").EnableStdin(),
63
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
64
	Options: []cmdkit.Option{
Kejie Zhang's avatar
Kejie Zhang committed
65 66
		cmdkit.BoolOption(lsHeadersOptionNameTime, "v", "Print table headers (Hash, Size, Name)."),
		cmdkit.BoolOption(lsResolveTypeOptionName, "Resolve linked objects to find out their types.").WithDefault(true),
67
		cmdkit.BoolOption(lsSizeOptionName, "Resolve linked objects to find out their file size.").WithDefault(true),
68
		cmdkit.BoolOption(lsStreamOptionName, "s", "Enable exprimental 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 134 135 136
		for i, fpath := range paths {
			p, err := iface.ParsePath(fpath)
			if err != nil {
				return err
Jeromy's avatar
Jeromy committed
137 138
			}

Łukasz Magiera's avatar
Łukasz Magiera committed
139
			results, err := api.Unixfs().Ls(req.Context, p,
140
				options.Unixfs.ResolveChildren(resolveSize || resolveType))
Łukasz Magiera's avatar
Łukasz Magiera committed
141 142
			if err != nil {
				return err
Jeromy's avatar
Jeromy committed
143 144
			}

Łukasz Magiera's avatar
Łukasz Magiera committed
145 146 147 148
			processLink, dirDone = processDir()
			for link := range results {
				if link.Err != nil {
					return link.Err
149
				}
150 151 152 153 154 155 156 157 158
				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
159
				lsLink := LsLink{
160 161
					Name: link.Name,
					Hash: enc.Encode(link.Cid),
Łukasz Magiera's avatar
Łukasz Magiera committed
162

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

180 181 182 183 184
			for {
				v, err := res.Next()
				if err != nil {
					if err == io.EOF {
						return nil
185
					}
186
					return err
187
				}
188 189
				out := v.(*LsOutput)
				lastObjectHash = tabularOutput(req, os.Stdout, out, lastObjectHash, false)
Matt Bell's avatar
Matt Bell committed
190
			}
191 192 193 194 195 196 197 198 199
		},
	},
	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)
200 201
			return nil
		}),
Matt Bell's avatar
Matt Bell committed
202
	},
203
	Type: LsOutput{},
Matt Bell's avatar
Matt Bell committed
204
}
205

206 207 208
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)
209
	size, _ := req.Options[lsSizeOptionName].(bool)
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
	// 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 {
233 234 235 236 237
				s := "Hash\tName"
				if size {
					s = "Hash\tSize\tName"
				}
				fmt.Fprintln(tw, s)
238 239 240 241 242
			}
			lastObjectHash = object.Hash
		}

		for _, link := range object.Links {
243 244 245 246 247 248 249 250 251 252 253 254 255 256
			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"
				}
257 258
			}

259
			fmt.Fprintf(tw, s, link.Hash, link.Size, link.Name)
260 261 262 263 264
		}
	}
	tw.Flush()
	return lastObjectHash
}