@@ -25,6 +25,21 @@ type listingQuery struct {
2525 PerPage int
2626 ShowAll bool
2727 Search string
28+ // Cursors is the trail of the cursors of the preceding pages, one per page,
29+ // as collected by the pager of a cursor-paged view. Keeping the whole trail
30+ // in the URL rather than just the current cursor is what lets such a view
31+ // still number its pages and step back to the previous one.
32+ Cursors []string
33+ }
34+
35+ // Cursor is the key the current page's listing resumes after, empty on the first
36+ // page.
37+ func (q listingQuery ) Cursor () string {
38+ if len (q .Cursors ) == 0 {
39+ return ""
40+ }
41+
42+ return q .Cursors [len (q .Cursors )- 1 ]
2843}
2944
3045// HandleBucketView shows the details page of a bucket.
@@ -47,6 +62,8 @@ func HandleBucketView(instances S3Instances, templates fs.FS, opts Options) http
4762 ShowVersions bool
4863 VersionsUnavailable bool
4964 ShowMetadata bool
65+ Truncated bool
66+ MaxScanObjects int
5067 }
5168
5269 renderer := newPageRenderer (templates , "bucket.html.tmpl" )
@@ -65,21 +82,32 @@ func HandleBucketView(instances S3Instances, templates fs.FS, opts Options) http
6582
6683 query := parseListingQuery (r .URL .Query ())
6784 data := pageData {
68- RootURL : opts .RootURL ,
69- BucketName : bucketName ,
70- CurrentPath : path ,
71- Paths : removeEmptyStrings (strings .Split (path , "/" )),
72- Endpoint : instance .Client .EndpointURL ().String (),
73- AllowDelete : opts .AllowDelete ,
74- CurrentS3 : instance ,
75- S3Instances : instances ,
76- SortBy : query .SortBy ,
77- SortOrder : query .SortOrder ,
78- Search : query .Search ,
79- ShowMetadata : opts .ShowMetadata ,
85+ RootURL : opts .RootURL ,
86+ BucketName : bucketName ,
87+ CurrentPath : path ,
88+ Paths : removeEmptyStrings (strings .Split (path , "/" )),
89+ Endpoint : instance .Client .EndpointURL ().String (),
90+ AllowDelete : opts .AllowDelete ,
91+ CurrentS3 : instance ,
92+ S3Instances : instances ,
93+ SortBy : query .SortBy ,
94+ SortOrder : query .SortOrder ,
95+ Search : query .Search ,
96+ ShowMetadata : opts .ShowMetadata ,
97+ MaxScanObjects : maxScanObjects ,
8098 }
8199
82- objs , versionsShown , err := listObjects (r .Context (), instance .Client , bucketName , path , opts .ListRecursive , opts .ShowVersions )
100+ // The default view is served one page at a time straight from S3, which
101+ // keeps its cost independent of how many objects the bucket holds. Every
102+ // other view needs the whole prefix in memory to do its job.
103+ cursorPaging := cursorPagingPossible (query , opts )
104+
105+ var listing objectListing
106+ if cursorPaging {
107+ listing , err = listObjectPage (r .Context (), instance .Client , bucketName , path , query .Cursor (), opts .ListRecursive , query .PerPage )
108+ } else {
109+ listing , err = listAllObjects (r .Context (), instance .Client , bucketName , path , opts .ListRecursive , opts .ShowVersions )
110+ }
83111 if err != nil {
84112 // A failed listing is reported on the page itself so that the user
85113 // can switch instances or go back instead of being stuck on an
@@ -90,23 +118,43 @@ func HandleBucketView(instances S3Instances, templates fs.FS, opts Options) http
90118 return
91119 }
92120
93- if versionsShown {
94- annotateVersionGroups (objs )
95- }
96- if query .Search != "" {
97- objs = filterObjects (objs , query .Search )
121+ if cursorPaging {
122+ data .objectPage = cursorPage (listing , query )
123+ } else {
124+ if listing .VersionsShown {
125+ annotateVersionGroups (listing .Objects )
126+ }
127+ if query .Search != "" {
128+ listing .Objects = filterObjects (listing .Objects , query .Search )
129+ }
130+ data .objectPage = paginateObjects (listing .Objects , query , listing .VersionsShown )
98131 }
99132
100- data .objectPage = paginateObjects ( objs , query , versionsShown )
101- data .ShowVersions = versionsShown
133+ data .ShowVersions = listing . VersionsShown
134+ data .Truncated = listing . Truncated
102135 // Only warn about unavailable versions when there is content to show;
103136 // an empty bucket legitimately produces an empty versioned listing.
104- data .VersionsUnavailable = opts .ShowVersions && ! versionsShown && len (objs ) > 0
137+ data .VersionsUnavailable = opts .ShowVersions && ! listing . VersionsShown && len (listing . Objects ) > 0
105138
106139 renderer (w , data )
107140 }
108141}
109142
143+ // cursorPagingPossible reports whether a request can be served by listing only
144+ // the page it shows. S3 lists keys in ascending lexicographic order and can
145+ // resume after a given key, but it cannot sort by anything else, cannot list
146+ // backwards and has no search — so sorting by another column, sorting
147+ // descending, searching and asking for every object at once all still need the
148+ // full listing. So does a versioned listing, whose version groups have to be
149+ // assembled before they can be split into pages.
150+ func cursorPagingPossible (query listingQuery , opts Options ) bool {
151+ return ! opts .ShowVersions &&
152+ ! query .ShowAll &&
153+ query .Search == "" &&
154+ query .SortBy == "key" &&
155+ query .SortOrder == "asc"
156+ }
157+
110158// parseBucketPath extracts the bucket name and the path within the bucket from
111159// a bucket view URL.
112160func parseBucketPath (urlPath string ) (string , string , error ) {
@@ -127,6 +175,7 @@ func parseListingQuery(params url.Values) listingQuery {
127175 Page : 1 ,
128176 PerPage : defaultPerPage ,
129177 Search : strings .TrimSpace (params .Get ("search" )),
178+ Cursors : removeEmptyStrings (params ["cursor" ]),
130179 }
131180
132181 if query .SortBy == "" {
0 commit comments