Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type listOptions struct {
timeFormat string
sortBy string
reverse bool
limit uint64
}

func formatTime(t time.Time, opts listOptions) string {
Expand Down
32 changes: 29 additions & 3 deletions cmd/ls.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import (
)

const deletedItemFormatString = "<<%s>>"
const maxListFolderLimit = uint64(^uint32(0))

type lsInput struct {
Path string `json:"path"`
Recursive bool `json:"recursive"`
IncludeDeleted bool `json:"include_deleted"`
OnlyDeleted bool `json:"only_deleted"`
Limit uint64 `json:"limit,omitempty"`
Long bool `json:"long"`
Sort string `json:"sort,omitempty"`
Reverse bool `json:"reverse"`
Expand Down Expand Up @@ -108,12 +110,14 @@ func parseLsOptions(cmd *cobra.Command) listOptions {
timeFormat, _ := cmd.Flags().GetString("time-format")
sortBy, _ := cmd.Flags().GetString("sort")
reverse, _ := cmd.Flags().GetBool("reverse")
limit, _ := cmd.Flags().GetUint64("limit")
return listOptions{
long: long,
timeField: timeField,
timeFormat: timeFormat,
sortBy: sortBy,
reverse: reverse,
limit: limit,
}
}

Expand All @@ -132,6 +136,12 @@ func ls(cmd *cobra.Command, args []string) (err error) {
onlyDeleted, _ := cmd.Flags().GetBool("only-deleted")
arg.IncludeDeleted = arg.IncludeDeleted || onlyDeleted
opts := parseLsOptions(cmd)
if opts.limit > 0 {
if opts.limit > maxListFolderLimit {
return invalidArgumentsErrorWithDetails("`ls --limit` is too large", flagErrorDetails("limit"))
}
arg.Limit = uint32(opts.limit)
}

dbx := filesNewFunc(config)

Expand Down Expand Up @@ -164,17 +174,17 @@ func ls(cmd *cobra.Command, args []string) (err error) {
metaRes, _ = getFileMetadata(dbx, path)
entries = []files.IsMetadata{metaRes}
} else {
entries = res.Entries
entries = appendMetadataWithLimit(entries, res.Entries, opts.limit)

for res.HasMore {
for res.HasMore && !metadataLimitReached(entries, opts.limit) {
arg := files.NewListFolderContinueArg(res.Cursor)

res, err = dbx.ListFolderContinue(arg)
if err != nil {
return err
}

entries = append(entries, res.Entries...)
entries = appendMetadataWithLimit(entries, res.Entries, opts.limit)
}
}

Expand All @@ -187,6 +197,20 @@ func ls(cmd *cobra.Command, args []string) (err error) {
return renderLsOutput(cmd, path, arg, onlyDeleted, opts, entries)
}

func appendMetadataWithLimit(entries []files.IsMetadata, next []files.IsMetadata, limit uint64) []files.IsMetadata {
for _, entry := range next {
if metadataLimitReached(entries, limit) {
break
}
entries = append(entries, entry)
}
return entries
}

func metadataLimitReached(entries []files.IsMetadata, limit uint64) bool {
return limit > 0 && uint64(len(entries)) >= limit
}

func prepareLsEntries(dbx files.Client, entries []files.IsMetadata, onlyDeleted bool) ([]files.IsMetadata, error) {
var filtered []files.IsMetadata
for _, entry := range entries {
Expand Down Expand Up @@ -251,6 +275,7 @@ func newLsInput(path string, arg *files.ListFolderArg, onlyDeleted bool, opts li
Recursive: arg.Recursive,
IncludeDeleted: arg.IncludeDeleted,
OnlyDeleted: onlyDeleted,
Limit: opts.limit,
Long: opts.long,
Sort: opts.sortBy,
Reverse: opts.reverse,
Expand Down Expand Up @@ -361,6 +386,7 @@ func init() {
lsCmd.Flags().BoolP("recurse", "R", false, "Recursively list all subfolders")
lsCmd.Flags().BoolP("include-deleted", "d", false, "Include deleted files")
lsCmd.Flags().BoolP("only-deleted", "D", false, "Only show deleted files")
lsCmd.Flags().Uint64("limit", 0, "Maximum number of entries to return")
lsCmd.Flags().String("sort", "", "Sort by: name, size, time, type")
lsCmd.Flags().BoolP("reverse", "r", false, "Reverse sort order")
lsCmd.Flags().String("time", "server", "Time field: server, client")
Expand Down
70 changes: 70 additions & 0 deletions cmd/ls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func TestLsJSONListsResultsAndInput(t *testing.T) {
setLsOutputJSON(t, cmd)
setLsFlag(t, cmd, "recurse", "true")
setLsFlag(t, cmd, "include-deleted", "true")
setLsFlag(t, cmd, "limit", "50")
setLsFlag(t, cmd, "long", "true")
setLsFlag(t, cmd, "sort", "name")
setLsFlag(t, cmd, "reverse", "true")
Expand Down Expand Up @@ -232,6 +233,9 @@ func TestLsJSONListsResultsAndInput(t *testing.T) {
if !listArg.Recursive || !listArg.IncludeDeleted {
t.Fatalf("ListFolder flags = recursive:%v include_deleted:%v, want true/true", listArg.Recursive, listArg.IncludeDeleted)
}
if listArg.Limit != 50 {
t.Fatalf("ListFolder limit = %d, want 50", listArg.Limit)
}

got := decodeLsOutput(t, stdout)
if got.Input.Path != "/" {
Expand All @@ -243,6 +247,9 @@ func TestLsJSONListsResultsAndInput(t *testing.T) {
if got.Input.Sort != "name" || !got.Input.Reverse || got.Input.Time != "client" || got.Input.TimeFormat != "rfc3339" {
t.Fatalf("input options = %+v, want sort/name reverse/client/rfc3339", got.Input)
}
if got.Input.Limit != 50 {
t.Fatalf("input limit = %d, want 50", got.Input.Limit)
}
if len(got.Results) != 2 {
t.Fatalf("results = %d, want 2", len(got.Results))
}
Expand All @@ -258,6 +265,68 @@ func TestLsJSONListsResultsAndInput(t *testing.T) {
}
}

func TestLsLimitStopsPaginationAndTruncatesOutput(t *testing.T) {
cmd, stdout := testLsCmd(t)
setLsOutputJSON(t, cmd)
setLsFlag(t, cmd, "limit", "2")

var listArg *files.ListFolderArg
mock := &mockFilesClient{
listFolderFn: func(arg *files.ListFolderArg) (*files.ListFolderResult, error) {
listArg = arg
return &files.ListFolderResult{
Entries: []files.IsMetadata{
&files.FileMetadata{Metadata: files.Metadata{PathDisplay: "/one.txt", PathLower: "/one.txt"}, Id: "id:one", Rev: "rev-one"},
&files.FileMetadata{Metadata: files.Metadata{PathDisplay: "/two.txt", PathLower: "/two.txt"}, Id: "id:two", Rev: "rev-two"},
&files.FileMetadata{Metadata: files.Metadata{PathDisplay: "/three.txt", PathLower: "/three.txt"}, Id: "id:three", Rev: "rev-three"},
},
HasMore: true,
Cursor: "cursor",
}, nil
},
listFolderContinueFn: func(arg *files.ListFolderContinueArg) (*files.ListFolderResult, error) {
t.Fatalf("ListFolderContinue should not be called after reaching limit: %v", arg)
return nil, nil
},
}
stubFilesClient(t, mock)

if err := ls(cmd, nil); err != nil {
t.Fatalf("ls error: %v", err)
}
if listArg == nil {
t.Fatal("ListFolder was not called")
}
if listArg.Limit != 2 {
t.Fatalf("ListFolder limit = %d, want 2", listArg.Limit)
}

got := decodeLsOutput(t, stdout)
if got.Input.Limit != 2 {
t.Fatalf("input limit = %d, want 2", got.Input.Limit)
}
if len(got.Results) != 2 {
t.Fatalf("results = %d, want 2", len(got.Results))
}
for _, result := range got.Results {
if result.Result.PathDisplay == "/three.txt" {
t.Fatalf("results include truncated entry: %#v", got.Results)
}
}
}

func TestLsLimitRejectsUint32Overflow(t *testing.T) {
cmd, stdout := testLsCmd(t)
setLsFlag(t, cmd, "limit", "4294967296")

if err := ls(cmd, nil); err == nil {
t.Fatal("expected ls limit overflow error")
}
if got := stdout.String(); got != "" {
t.Fatalf("stdout = %q, want empty output on validation error", got)
}
}

func TestLsJSONFilePathUsesMetadata(t *testing.T) {
cmd, stdout := testLsCmd(t)
setLsOutputJSON(t, cmd)
Expand Down Expand Up @@ -467,6 +536,7 @@ func testLsCmd(t *testing.T) (*cobra.Command, *bytes.Buffer) {
cmd.Flags().BoolP("recurse", "R", false, "")
cmd.Flags().BoolP("include-deleted", "d", false, "")
cmd.Flags().BoolP("only-deleted", "D", false, "")
cmd.Flags().Uint64("limit", 0, "")
cmd.Flags().String("sort", "", "")
cmd.Flags().BoolP("reverse", "r", false, "")
cmd.Flags().String("time", "server", "")
Expand Down
15 changes: 11 additions & 4 deletions cmd/revs.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (

type revsInput struct {
Path string `json:"path"`
Limit uint64 `json:"limit,omitempty"`
Long bool `json:"long"`
Time string `json:"time,omitempty"`
TimeFormat string `json:"time_format,omitempty"`
Expand All @@ -44,6 +45,10 @@ func revs(cmd *cobra.Command, args []string) (err error) {
}

arg := files.NewListRevisionsArg(path)
limit, _ := cmd.Flags().GetUint64("limit")
if limit > 0 {
arg.Limit = limit
}

dbx := filesNewFunc(config)
res, err := dbx.ListRevisions(arg)
Expand All @@ -53,18 +58,18 @@ func revs(cmd *cobra.Command, args []string) (err error) {

opts := parseLsOptions(cmd)

return renderRevisionsOutput(cmd, path, res.Entries, opts)
return renderRevisionsOutput(cmd, path, limit, res.Entries, opts)
}

func renderRevisionsOutput(cmd *cobra.Command, path string, entries []*files.FileMetadata, opts listOptions) error {
func renderRevisionsOutput(cmd *cobra.Command, path string, limit uint64, entries []*files.FileMetadata, opts listOptions) error {
out := commandOutput(cmd)
if commandOutputFormat(cmd) != output.FormatJSON {
return out.RenderText(func(w io.Writer) error {
return renderRevisionResults(w, entries, opts)
})
}

input := newRevsInput(path, opts)
input := newRevsInput(path, limit, opts)
metadata, err := jsonMetadataListFromRevisions(entries)
if err != nil {
return err
Expand All @@ -73,9 +78,10 @@ func renderRevisionsOutput(cmd *cobra.Command, path string, entries []*files.Fil
return renderJSONOperationOutput(cmd, input, results)
}

func newRevsInput(path string, opts listOptions) revsInput {
func newRevsInput(path string, limit uint64, opts listOptions) revsInput {
return revsInput{
Path: path,
Limit: limit,
Long: opts.long,
Time: opts.timeField,
TimeFormat: opts.timeFormat,
Expand Down Expand Up @@ -125,6 +131,7 @@ func init() {
RootCmd.AddCommand(revsCmd)

revsCmd.Flags().BoolP("long", "l", false, "Long listing")
revsCmd.Flags().Uint64("limit", 0, "Maximum number of revisions to return")
revsCmd.Flags().String("time", "server", "Time field: server, client")
revsCmd.Flags().String("time-format", "", "Time format: short (2006-01-02 15:04), rfc3339")
enableStructuredOutput(revsCmd)
Expand Down
14 changes: 13 additions & 1 deletion cmd/revs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ func TestRenderRevisionResultsLongModeUsesTimeOptions(t *testing.T) {
func TestRevsUsesListRevisionsAndCommandOutput(t *testing.T) {
cmd, stdout := testRevsCmd()
var gotPath string
var gotLimit uint64

stubFilesClient(t, &mockFilesClient{
listRevisionsFn: func(arg *files.ListRevisionsArg) (*files.ListRevisionsResult, error) {
gotPath = arg.Path
gotLimit = arg.Limit
return files.NewListRevisionsResult(false, []*files.FileMetadata{
{Rev: "rev-c"},
}), nil
Expand All @@ -90,6 +92,9 @@ func TestRevsUsesListRevisionsAndCommandOutput(t *testing.T) {
if gotPath != "/report.pdf" {
t.Fatalf("ListRevisions path = %q, want %q", gotPath, "/report.pdf")
}
if gotLimit != 10 {
t.Fatalf("ListRevisions limit = %d, want SDK default 10", gotLimit)
}
if got, want := stdout.String(), "rev-c\n"; got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
Expand All @@ -99,14 +104,17 @@ func TestRevsJSONOutputsInputAndResults(t *testing.T) {
cmd, stdout := testRevsCmd()
setRevsOutputJSON(t, cmd)
setRevsFlag(t, cmd, "long", "true")
setRevsFlag(t, cmd, "limit", "25")
setRevsFlag(t, cmd, "time", "client")
setRevsFlag(t, cmd, "time-format", "rfc3339")
var gotPath string
var gotLimit uint64
clientModified := time.Date(2026, 6, 22, 10, 0, 0, 0, time.UTC)

stubFilesClient(t, &mockFilesClient{
listRevisionsFn: func(arg *files.ListRevisionsArg) (*files.ListRevisionsResult, error) {
gotPath = arg.Path
gotLimit = arg.Limit
return files.NewListRevisionsResult(false, []*files.FileMetadata{
{
Metadata: files.Metadata{
Expand All @@ -128,9 +136,12 @@ func TestRevsJSONOutputsInputAndResults(t *testing.T) {
if gotPath != "/report.pdf" {
t.Fatalf("ListRevisions path = %q, want /report.pdf", gotPath)
}
if gotLimit != 25 {
t.Fatalf("ListRevisions limit = %d, want 25", gotLimit)
}

got := decodeRevsOutput(t, stdout)
if got.Input.Path != "/report.pdf" || !got.Input.Long || got.Input.Time != "client" || got.Input.TimeFormat != "rfc3339" {
if got.Input.Path != "/report.pdf" || got.Input.Limit != 25 || !got.Input.Long || got.Input.Time != "client" || got.Input.TimeFormat != "rfc3339" {
t.Fatalf("input = %#v, want path/long/time/time_format", got.Input)
}
if len(got.Results) != 1 {
Expand Down Expand Up @@ -180,6 +191,7 @@ func testRevsCmd() (*cobra.Command, *bytes.Buffer) {
cmd := &cobra.Command{Use: "revs"}
cmd.SetOut(&stdout)
cmd.Flags().BoolP("long", "l", false, "")
cmd.Flags().Uint64("limit", 0, "")
cmd.Flags().String("time", "server", "")
cmd.Flags().String("time-format", "", "")
cmd.Flags().String(outputFlag, "text", "")
Expand Down
2 changes: 2 additions & 0 deletions cmd/testdata/json_contract/success_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
],
"ls_input": [
"include_deleted",
"limit",
"long",
"only_deleted",
"path",
Expand Down Expand Up @@ -152,6 +153,7 @@
"revision"
],
"revs_input": [
"limit",
"long",
"path",
"time",
Expand Down
1 change: 1 addition & 0 deletions docs/commands/dbxcli_ls.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dbxcli ls [flags] [<path>]
```
-h, --help help for ls
-d, --include-deleted Include deleted files
--limit uint Maximum number of entries to return
-l, --long Long listing
-D, --only-deleted Only show deleted files
-R, --recurse Recursively list all subfolders
Expand Down
1 change: 1 addition & 0 deletions docs/commands/dbxcli_revs.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dbxcli revs [flags] <file>

```
-h, --help help for revs
--limit uint Maximum number of revisions to return
-l, --long Long listing
--time string Time field: server, client (default "server")
--time-format string Time format: short (2006-01-02 15:04), rfc3339
Expand Down
2 changes: 2 additions & 0 deletions docs/json-schema/v1/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
],
"ls_input": [
"include_deleted",
"limit",
"long",
"only_deleted",
"path",
Expand Down Expand Up @@ -152,6 +153,7 @@
"revision"
],
"revs_input": [
"limit",
"long",
"path",
"time",
Expand Down
Loading