@@ -41,6 +41,12 @@ interface CopyObjectParams {
4141 ifUnmodifiedSince ?: Date
4242 }
4343}
44+ export interface ListObjectsV2Result {
45+ folders : Obj [ ]
46+ objects : Obj [ ]
47+ hasNext : boolean
48+ nextCursor ?: string
49+ }
4450
4551/**
4652 * ObjectStorage
@@ -586,18 +592,27 @@ export class ObjectStorage {
586592 startAfter ?: string
587593 maxKeys ?: number
588594 encodingType ?: 'url'
589- } ) {
595+ sortBy ?: {
596+ column : 'name' | 'created_at' | 'updated_at'
597+ order ?: string
598+ }
599+ } ) : Promise < ListObjectsV2Result > {
590600 const limit = Math . min ( options ?. maxKeys || 1000 , 1000 )
591601 const prefix = options ?. prefix || ''
592602 const delimiter = options ?. delimiter
593603
594- const cursor = options ?. cursor ? decodeContinuationToken ( options ? .cursor ) : undefined
604+ const cursor = options ?. cursor ? decodeContinuationToken ( options . cursor ) : undefined
595605 let searchResult = await this . db . listObjectsV2 ( this . bucketId , {
596606 prefix : options ?. prefix ,
597607 delimiter : options ?. delimiter ,
598608 maxKeys : limit + 1 ,
599- nextToken : cursor ,
600- startAfter : cursor || options ?. startAfter ,
609+ nextToken : cursor ?. startAfter ,
610+ startAfter : cursor ?. startAfter || options ?. startAfter ,
611+ sortBy : {
612+ order : cursor ?. sortOrder || options ?. sortBy ?. order ,
613+ column : cursor ?. sortColumn || options ?. sortBy ?. column ,
614+ after : cursor ?. sortColumnAfter ,
615+ } ,
601616 } )
602617
603618 let prevPrefix = ''
@@ -638,15 +653,31 @@ export class ObjectStorage {
638653 const objects : Obj [ ] = [ ]
639654 searchResult . forEach ( ( obj ) => {
640655 const target = obj . id === null ? folders : objects
656+ const name = obj . id === null && ! obj . name . endsWith ( '/' ) ? obj . name + '/' : obj . name
641657 target . push ( {
642658 ...obj ,
643- name : options ?. encodingType === 'url' ? encodeURIComponent ( obj . name ) : obj . name ,
659+ name : options ?. encodingType === 'url' ? encodeURIComponent ( name ) : name ,
644660 } )
645661 } )
646662
647- const nextContinuationToken = isTruncated
648- ? encodeContinuationToken ( searchResult [ searchResult . length - 1 ] . name )
649- : undefined
663+ let nextContinuationToken : string | undefined
664+ if ( isTruncated ) {
665+ const sortColumn = ( cursor ?. sortColumn || options ?. sortBy ?. column ) as
666+ | 'name'
667+ | 'created_at'
668+ | 'updated_at'
669+ | undefined
670+
671+ nextContinuationToken = encodeContinuationToken ( {
672+ startAfter : searchResult [ searchResult . length - 1 ] . name ,
673+ sortOrder : cursor ?. sortOrder || options ?. sortBy ?. order ,
674+ sortColumn,
675+ sortColumnAfter :
676+ sortColumn && sortColumn !== 'name' && searchResult [ searchResult . length - 1 ] [ sortColumn ]
677+ ? new Date ( searchResult [ searchResult . length - 1 ] [ sortColumn ] || '' ) . toISOString ( )
678+ : undefined ,
679+ } )
680+ }
650681
651682 return {
652683 hasNext : isTruncated ,
@@ -806,16 +837,42 @@ export class ObjectStorage {
806837 }
807838}
808839
809- function encodeContinuationToken ( name : string ) {
810- return Buffer . from ( `l:${ name } ` ) . toString ( 'base64' )
840+ interface ContinuationToken {
841+ startAfter : string
842+ sortOrder ?: string // 'asc' | 'desc'
843+ sortColumn ?: string
844+ sortColumnAfter ?: string
811845}
812846
813- function decodeContinuationToken ( token : string ) {
814- const decoded = Buffer . from ( token , 'base64' ) . toString ( ) . split ( ':' )
847+ const CONTINUATION_TOKEN_PART_MAP : Record < string , keyof ContinuationToken > = {
848+ l : 'startAfter' ,
849+ o : 'sortOrder' ,
850+ c : 'sortColumn' ,
851+ a : 'sortColumnAfter' ,
852+ }
815853
816- if ( decoded . length === 0 ) {
817- throw new Error ( 'Invalid continuation token' )
854+ function encodeContinuationToken ( tokenInfo : ContinuationToken ) {
855+ let result = ''
856+ for ( const [ k , v ] of Object . entries ( CONTINUATION_TOKEN_PART_MAP ) ) {
857+ if ( tokenInfo [ v ] ) {
858+ result += `${ k } :${ tokenInfo [ v ] } \n`
859+ }
818860 }
861+ return Buffer . from ( result . slice ( 0 , - 1 ) ) . toString ( 'base64' )
862+ }
819863
820- return decoded [ 1 ]
864+ function decodeContinuationToken ( token : string ) : ContinuationToken {
865+ const decodedParts = Buffer . from ( token , 'base64' ) . toString ( ) . split ( '\n' )
866+ const result : ContinuationToken = {
867+ startAfter : '' ,
868+ sortOrder : 'asc' ,
869+ }
870+ for ( const part of decodedParts ) {
871+ const partMatch = part . match ( / ^ ( \S ) : ( .* ) / )
872+ if ( ! partMatch || partMatch . length !== 3 || ! ( partMatch [ 1 ] in CONTINUATION_TOKEN_PART_MAP ) ) {
873+ throw new Error ( 'Invalid continuation token' )
874+ }
875+ result [ CONTINUATION_TOKEN_PART_MAP [ partMatch [ 1 ] ] ] = partMatch [ 2 ]
876+ }
877+ return result
821878}
0 commit comments