ls.go
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 }