storj/cmd/uplink/cmd_ls.go
Stefan Benten 345ab87b5c cmd/uplink: adding output flag for ls command
With this change users can use the uplink cli in
scripts (ie. bash) more easily, since the output
can be switched to an easier processable json format.
It keeps the default of tabbed output.


Change-Id: I37e2c55f75c2250c3119fd8df8b66a766ff9096b
2022-04-29 10:32:04 +00:00

228 lines
5.5 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"encoding/json"
"strconv"
"time"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplink/ulext"
"storj.io/storj/cmd/uplink/ulfs"
"storj.io/storj/cmd/uplink/ulloc"
"storj.io/uplink"
)
type cmdLs struct {
ex ulext.External
access string
recursive bool
encrypted bool
expanded bool
pending bool
utc bool
output string
prefix *ulloc.Location
}
func newCmdLs(ex ulext.External) *cmdLs {
return &cmdLs{ex: ex}
}
func (c *cmdLs) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Access name or value to use", "").(string)
c.recursive = params.Flag("recursive", "List recursively", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool), clingy.Boolean,
).(bool)
c.encrypted = params.Flag("encrypted", "Shows keys base64 encoded without decrypting", false,
clingy.Transform(strconv.ParseBool), clingy.Boolean,
).(bool)
c.pending = params.Flag("pending", "List pending object uploads instead", false,
clingy.Transform(strconv.ParseBool), clingy.Boolean,
).(bool)
c.expanded = params.Flag("expanded", "Use expanded output, showing object expiration times and whether there is custom metadata attached", false,
clingy.Short('x'),
clingy.Transform(strconv.ParseBool), clingy.Boolean,
).(bool)
c.utc = params.Flag("utc", "Show all timestamps in UTC instead of local time", false,
clingy.Transform(strconv.ParseBool), clingy.Boolean,
).(bool)
c.output = params.Flag("output", "Output Format (tabbed, json)", "tabbed",
clingy.Short('o'),
).(string)
c.prefix = params.Arg("prefix", "Prefix to list (sj://BUCKET[/KEY])", clingy.Optional,
clingy.Transform(ulloc.Parse),
).(*ulloc.Location)
}
func (c *cmdLs) Execute(ctx clingy.Context) error {
if c.prefix == nil {
return c.listBuckets(ctx)
}
return c.listLocation(ctx, *c.prefix)
}
func (c *cmdLs) listBuckets(ctx clingy.Context) error {
project, err := c.ex.OpenProject(ctx, c.access)
if err != nil {
return err
}
defer func() { _ = project.Close() }()
iter := project.ListBuckets(ctx, nil)
switch c.output {
case "tabbed":
return c.printTabbedBucket(ctx, iter)
case "json":
return c.printJSONBucket(ctx, iter)
default:
return errs.New("unknown output format, got %s", c.output)
}
}
func (c *cmdLs) listLocation(ctx clingy.Context, prefix ulloc.Location) error {
fs, err := c.ex.OpenFilesystem(ctx, c.access, ulext.BypassEncryption(c.encrypted))
if err != nil {
return err
}
defer func() { _ = fs.Close() }()
if fs.IsLocalDir(ctx, prefix) {
prefix = prefix.AsDirectoryish()
}
// create the object iterator of either existing objects or pending multipart uploads
iter, err := fs.List(ctx, prefix, &ulfs.ListOptions{
Recursive: c.recursive,
Pending: c.pending,
Expanded: c.expanded,
})
if err != nil {
return err
}
switch c.output {
case "tabbed":
return c.printTabbedLocation(ctx, iter)
case "json":
return c.printJSONLocation(ctx, iter)
default:
return errs.New("unknown output format, got %s", c.output)
}
}
func (c *cmdLs) printTabbedBucket(ctx clingy.Context, iter *uplink.BucketIterator) (err error) {
tw := newTabbedWriter(ctx.Stdout(), "CREATED", "NAME")
defer tw.Done()
for iter.Next() {
item := iter.Item()
tw.WriteLine(formatTime(c.utc, item.Created), item.Name)
}
return iter.Err()
}
func (c *cmdLs) printJSONBucket(ctx clingy.Context, iter *uplink.BucketIterator) (err error) {
jw := json.NewEncoder(ctx.Stdout())
for iter.Next() {
obj := iter.Item()
err = jw.Encode(obj)
if err != nil {
return err
}
}
return iter.Err()
}
func (c *cmdLs) printTabbedLocation(ctx clingy.Context, iter ulfs.ObjectIterator) (err error) {
headers := []string{"KIND", "CREATED", "SIZE", "KEY"}
if c.expanded {
headers = append(headers, "EXPIRES", "META")
}
tw := newTabbedWriter(ctx.Stdout(), headers...)
defer tw.Done()
// iterate and print the results
for iter.Next() {
obj := iter.Item()
var parts []interface{}
if obj.IsPrefix {
parts = append(parts, "PRE", "", "", obj.Loc.Loc())
if c.expanded {
parts = append(parts, "", "")
}
} else {
parts = append(parts, "OBJ", formatTime(c.utc, obj.Created), obj.ContentLength, obj.Loc.Loc())
if c.expanded {
parts = append(parts, formatTime(c.utc, obj.Expires), sumMetadataSize(obj.Metadata))
}
}
tw.WriteLine(parts...)
}
return iter.Err()
}
func (c *cmdLs) printJSONLocation(ctx clingy.Context, iter ulfs.ObjectIterator) (err error) {
jw := json.NewEncoder(ctx.Stdout())
for iter.Next() {
obj := iter.Item()
if obj.IsPrefix {
err = jw.Encode(struct {
Kind string `json:"kind"`
Key string `json:"key"`
}{"PRE", obj.Loc.Loc()})
} else {
err = jw.Encode(struct {
Kind string `json:"kind"`
Created string `json:"created"`
Size int64 `json:"size"`
Key string `json:"key"`
Expires string `json:"expires,omitempty"`
Metadata int `json:"meta,omitempty"`
}{"OBJ", formatTime(c.utc, obj.Created), obj.ContentLength, obj.Loc.Loc(), formatTime(c.utc, obj.Expires), sumMetadataSize(obj.Metadata)})
}
if err != nil {
return err
}
}
return iter.Err()
}
func formatTime(utc bool, x time.Time) string {
if x.IsZero() {
return ""
}
if utc {
x = x.UTC()
} else {
x = x.Local()
}
return x.Format("2006-01-02 15:04:05")
}
func sumMetadataSize(md uplink.CustomMetadata) int {
size := 0
for k, v := range md {
size += len(k)
size += len(v)
}
return size
}