master
go 452 lines 11.6 KB
Raw
1 package commands
2
3 import (
4 "context"
5 "fmt"
6 "io"
7 "os"
8 "slices"
9 "strings"
10 "text/tabwriter"
11 "time"
12
13 cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
14 "github.com/ipfs/kubo/core/commands/cmdutils"
15
16 unixfs "github.com/ipfs/boxo/ipld/unixfs"
17 unixfs_pb "github.com/ipfs/boxo/ipld/unixfs/pb"
18 cmds "github.com/ipfs/go-ipfs-cmds"
19 iface "github.com/ipfs/kubo/core/coreiface"
20 options "github.com/ipfs/kubo/core/coreiface/options"
21 )
22
23 // LsLink contains printable data for a single ipld link in ls output
24 type LsLink struct {
25 Name, Hash string
26 Size uint64
27 Type unixfs_pb.Data_DataType
28 Target string
29 Mode os.FileMode
30 ModTime time.Time
31 }
32
33 // LsObject is an element of LsOutput
34 // It can represent all or part of a directory
35 type LsObject struct {
36 Hash string
37 Links []LsLink
38 }
39
40 // LsOutput is a set of printable data for directories,
41 // it can be complete or partial
42 type LsOutput struct {
43 Objects []LsObject
44 }
45
46 const (
47 lsHeadersOptionNameTime = "headers"
48 lsResolveTypeOptionName = "resolve-type"
49 lsSizeOptionName = "size"
50 lsStreamOptionName = "stream"
51 lsLongOptionName = "long"
52 )
53
54 var LsCmd = &cmds.Command{
55 Helptext: cmds.HelpText{
56 Tagline: "List directory contents for Unix filesystem objects.",
57 ShortDescription: `
58 Displays the contents of an IPFS or IPNS object(s) at the given path, with
59 the following format:
60
61 <cid> <size> <name>
62
63 With the --long (-l) option, display optional file mode (permissions) and
64 modification time in a format similar to Unix 'ls -l':
65
66 <mode> <cid> <size> <mtime> <name>
67
68 Mode and mtime are optional UnixFS metadata. They are only present if the
69 content was imported with 'ipfs add --preserve-mode' and '--preserve-mtime'.
70 Without preserved metadata, both mode and mtime display '-'. Times are in UTC.
71
72 Example with --long and preserved metadata:
73
74 -rw-r--r-- QmZULkCELmmk5XNf... 1234 Jan 15 10:30 document.txt
75 -rwxr-xr-x QmaRGe7bVmVaLmxb... 5678 Dec 01 2023 script.sh
76 drwxr-xr-x QmWWEQhcLufF3qPm... - Nov 20 2023 subdir/
77
78 Example with --long without preserved metadata:
79
80 - QmZULkCELmmk5XNf... 1234 - document.txt
81
82 The JSON output contains type information.
83 `,
84 },
85
86 Arguments: []cmds.Argument{
87 cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from.").EnableStdin(),
88 },
89 Options: []cmds.Option{
90 cmds.BoolOption(lsHeadersOptionNameTime, "v", "Print table headers (Hash, Size, Name)."),
91 cmds.BoolOption(lsResolveTypeOptionName, "Resolve linked objects to find out their types.").WithDefault(true),
92 cmds.BoolOption(lsSizeOptionName, "Resolve linked objects to find out their file size.").WithDefault(true),
93 cmds.BoolOption(lsStreamOptionName, "s", "Enable experimental streaming of directory entries as they are traversed."),
94 cmds.BoolOption(lsLongOptionName, "l", "Use a long listing format, showing file mode and modification time."),
95 },
96 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
97 api, err := cmdenv.GetApi(env, req)
98 if err != nil {
99 return err
100 }
101
102 resolveType, _ := req.Options[lsResolveTypeOptionName].(bool)
103 resolveSize, _ := req.Options[lsSizeOptionName].(bool)
104 stream, _ := req.Options[lsStreamOptionName].(bool)
105
106 err = req.ParseBodyArgs()
107 if err != nil {
108 return err
109 }
110 paths := req.Arguments
111
112 enc, err := cmdenv.GetCidEncoder(req)
113 if err != nil {
114 return err
115 }
116
117 var processLink func(path string, link LsLink) error
118 var dirDone func(i int)
119
120 processDir := func() (func(path string, link LsLink) error, func(i int)) {
121 return func(path string, link LsLink) error {
122 output := []LsObject{{
123 Hash: path,
124 Links: []LsLink{link},
125 }}
126 return res.Emit(&LsOutput{output})
127 }, func(i int) {}
128 }
129 done := func() error { return nil }
130
131 if !stream {
132 output := make([]LsObject, len(req.Arguments))
133
134 processDir = func() (func(path string, link LsLink) error, func(i int)) {
135 // for each dir
136 outputLinks := make([]LsLink, 0)
137 return func(path string, link LsLink) error {
138 // for each link
139 outputLinks = append(outputLinks, link)
140 return nil
141 }, func(i int) {
142 // after each dir
143 slices.SortFunc(outputLinks, func(a, b LsLink) int {
144 return strings.Compare(a.Name, b.Name)
145 })
146
147 output[i] = LsObject{
148 Hash: paths[i],
149 Links: outputLinks,
150 }
151 }
152 }
153
154 done = func() error {
155 return cmds.EmitOnce(res, &LsOutput{output})
156 }
157 }
158
159 lsCtx, cancel := context.WithCancel(req.Context)
160 defer cancel()
161
162 for i, fpath := range paths {
163 pth, err := cmdutils.PathOrCidPath(fpath)
164 if err != nil {
165 return err
166 }
167
168 results := make(chan iface.DirEntry)
169 lsErr := make(chan error, 1)
170 go func() {
171 lsErr <- api.Unixfs().Ls(lsCtx, pth, results,
172 options.Unixfs.ResolveChildren(resolveSize || resolveType))
173 }()
174
175 processLink, dirDone = processDir()
176 for link := range results {
177 var ftype unixfs_pb.Data_DataType
178 switch link.Type {
179 case iface.TFile:
180 ftype = unixfs.TFile
181 case iface.TDirectory:
182 ftype = unixfs.TDirectory
183 case iface.TSymlink:
184 ftype = unixfs.TSymlink
185 }
186 lsLink := LsLink{
187 Name: link.Name,
188 Hash: enc.Encode(link.Cid),
189
190 Size: link.Size,
191 Type: ftype,
192 Target: link.Target,
193
194 Mode: link.Mode,
195 ModTime: link.ModTime,
196 }
197 if err = processLink(paths[i], lsLink); err != nil {
198 return err
199 }
200 }
201 if err = <-lsErr; err != nil {
202 return err
203 }
204 dirDone(i)
205 }
206 return done()
207 },
208 PostRun: cmds.PostRunMap{
209 cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
210 req := res.Request()
211 lastObjectHash := ""
212
213 for {
214 v, err := res.Next()
215 if err != nil {
216 if err == io.EOF {
217 return nil
218 }
219 return err
220 }
221 out := v.(*LsOutput)
222 lastObjectHash = tabularOutput(req, os.Stdout, out, lastObjectHash, false)
223 }
224 },
225 },
226 Encoders: cmds.EncoderMap{
227 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *LsOutput) error {
228 // when streaming over HTTP using a text encoder, we cannot render breaks
229 // between directories because we don't know the hash of the last
230 // directory encoder
231 ignoreBreaks, _ := req.Options[lsStreamOptionName].(bool)
232 tabularOutput(req, w, out, "", ignoreBreaks)
233 return nil
234 }),
235 },
236 Type: LsOutput{},
237 }
238
239 // formatMode converts os.FileMode to a 10-character Unix ls-style string.
240 //
241 // Format: [type][owner rwx][group rwx][other rwx]
242 //
243 // Type indicators: - (regular), d (directory), l (symlink), p (named pipe),
244 // s (socket), c (char device), b (block device).
245 //
246 // Special bits replace the execute position: setuid on owner (s/S),
247 // setgid on group (s/S), sticky on other (t/T). Lowercase when the
248 // underlying execute bit is also set, uppercase when not.
249 func formatMode(mode os.FileMode) string {
250 var buf [10]byte
251
252 // File type - handle all special file types like ls does
253 switch {
254 case mode&os.ModeDir != 0:
255 buf[0] = 'd'
256 case mode&os.ModeSymlink != 0:
257 buf[0] = 'l'
258 case mode&os.ModeNamedPipe != 0:
259 buf[0] = 'p'
260 case mode&os.ModeSocket != 0:
261 buf[0] = 's'
262 case mode&os.ModeDevice != 0:
263 if mode&os.ModeCharDevice != 0 {
264 buf[0] = 'c'
265 } else {
266 buf[0] = 'b'
267 }
268 default:
269 buf[0] = '-'
270 }
271
272 // Owner permissions (bits 8,7,6)
273 buf[1] = permBit(mode, 0400, 'r') // read
274 buf[2] = permBit(mode, 0200, 'w') // write
275 // Handle setuid bit for owner execute
276 if mode&os.ModeSetuid != 0 {
277 if mode&0100 != 0 {
278 buf[3] = 's'
279 } else {
280 buf[3] = 'S'
281 }
282 } else {
283 buf[3] = permBit(mode, 0100, 'x') // execute
284 }
285
286 // Group permissions (bits 5,4,3)
287 buf[4] = permBit(mode, 0040, 'r') // read
288 buf[5] = permBit(mode, 0020, 'w') // write
289 // Handle setgid bit for group execute
290 if mode&os.ModeSetgid != 0 {
291 if mode&0010 != 0 {
292 buf[6] = 's'
293 } else {
294 buf[6] = 'S'
295 }
296 } else {
297 buf[6] = permBit(mode, 0010, 'x') // execute
298 }
299
300 // Other permissions (bits 2,1,0)
301 buf[7] = permBit(mode, 0004, 'r') // read
302 buf[8] = permBit(mode, 0002, 'w') // write
303 // Handle sticky bit for other execute
304 if mode&os.ModeSticky != 0 {
305 if mode&0001 != 0 {
306 buf[9] = 't'
307 } else {
308 buf[9] = 'T'
309 }
310 } else {
311 buf[9] = permBit(mode, 0001, 'x') // execute
312 }
313
314 return string(buf[:])
315 }
316
317 // permBit returns the permission character if the bit is set.
318 func permBit(mode os.FileMode, bit os.FileMode, char byte) byte {
319 if mode&bit != 0 {
320 return char
321 }
322 return '-'
323 }
324
325 // formatModTime formats time.Time for display, following Unix ls conventions.
326 //
327 // Returns "-" for zero time. Otherwise returns a 12-character string:
328 // recent files (within 6 months) show "Jan 02 15:04",
329 // older or future files show "Jan 02 2006".
330 //
331 // The output uses the timezone embedded in t (UTC for IPFS metadata).
332 func formatModTime(t time.Time) string {
333 if t.IsZero() {
334 return "-"
335 }
336
337 // Format: "Jan 02 15:04" for times within the last 6 months
338 // Format: "Jan 02 2006" for older times (similar to ls)
339 now := time.Now()
340 sixMonthsAgo := now.AddDate(0, -6, 0)
341
342 if t.After(sixMonthsAgo) && t.Before(now.Add(24*time.Hour)) {
343 return t.Format("Jan 02 15:04")
344 }
345 return t.Format("Jan 02 2006")
346 }
347
348 func tabularOutput(req *cmds.Request, w io.Writer, out *LsOutput, lastObjectHash string, ignoreBreaks bool) string {
349 headers, _ := req.Options[lsHeadersOptionNameTime].(bool)
350 stream, _ := req.Options[lsStreamOptionName].(bool)
351 size, _ := req.Options[lsSizeOptionName].(bool)
352 long, _ := req.Options[lsLongOptionName].(bool)
353
354 // in streaming mode we can't automatically align the tabs
355 // so we take a best guess
356 var minTabWidth int
357 if stream {
358 minTabWidth = 10
359 } else {
360 minTabWidth = 1
361 }
362
363 multipleFolders := len(req.Arguments) > 1
364
365 tw := tabwriter.NewWriter(w, minTabWidth, 2, 1, ' ', 0)
366
367 for _, object := range out.Objects {
368
369 if !ignoreBreaks && object.Hash != lastObjectHash {
370 if multipleFolders {
371 if lastObjectHash != "" {
372 fmt.Fprintln(tw)
373 }
374 fmt.Fprintf(tw, "%s:\n", object.Hash)
375 }
376 if headers {
377 var s string
378 if long {
379 // Long format: Mode Hash [Size] ModTime Name
380 if size {
381 s = "Mode\tHash\tSize\tModTime\tName"
382 } else {
383 s = "Mode\tHash\tModTime\tName"
384 }
385 } else {
386 // Standard format: Hash [Size] Name
387 if size {
388 s = "Hash\tSize\tName"
389 } else {
390 s = "Hash\tName"
391 }
392 }
393 fmt.Fprintln(tw, s)
394 }
395 lastObjectHash = object.Hash
396 }
397
398 for _, link := range object.Links {
399 var s string
400 isDir := link.Type == unixfs.TDirectory || link.Type == unixfs.THAMTShard || link.Type == unixfs.TMetadata
401
402 if long {
403 // Long format: Mode Hash Size ModTime Name
404 var mode string
405 if link.Mode == 0 {
406 // No mode metadata preserved. Show "-" to indicate
407 // "not available" rather than "----------" (mode 0000).
408 mode = "-"
409 } else {
410 mode = formatMode(link.Mode)
411 }
412 modTime := formatModTime(link.ModTime)
413
414 if isDir {
415 if size {
416 s = "%s\t%s\t-\t%s\t%s/\n"
417 } else {
418 s = "%s\t%s\t%s\t%s/\n"
419 }
420 fmt.Fprintf(tw, s, mode, link.Hash, modTime, cmdenv.EscNonPrint(link.Name))
421 } else {
422 if size {
423 s = "%s\t%s\t%v\t%s\t%s\n"
424 fmt.Fprintf(tw, s, mode, link.Hash, link.Size, modTime, cmdenv.EscNonPrint(link.Name))
425 } else {
426 s = "%s\t%s\t%s\t%s\n"
427 fmt.Fprintf(tw, s, mode, link.Hash, modTime, cmdenv.EscNonPrint(link.Name))
428 }
429 }
430 } else {
431 // Standard format: Hash [Size] Name
432 switch {
433 case isDir:
434 if size {
435 s = "%[1]s\t-\t%[3]s/\n"
436 } else {
437 s = "%[1]s\t%[3]s/\n"
438 }
439 default:
440 if size {
441 s = "%s\t%v\t%s\n"
442 } else {
443 s = "%[1]s\t%[3]s\n"
444 }
445 }
446 fmt.Fprintf(tw, s, link.Hash, link.Size, cmdenv.EscNonPrint(link.Name))
447 }
448 }
449 }
450 tw.Flush()
451 return lastObjectHash
452 }