@@ -41,6 +41,12 @@ interface CopyObjectParams {
41
41
ifUnmodifiedSince ?: Date
42
42
}
43
43
}
44
+ export interface ListObjectsV2Result {
45
+ folders : Obj [ ]
46
+ objects : Obj [ ]
47
+ hasNext : boolean
48
+ nextCursor ?: string
49
+ }
44
50
45
51
/**
46
52
* ObjectStorage
@@ -586,18 +592,27 @@ export class ObjectStorage {
586
592
startAfter ?: string
587
593
maxKeys ?: number
588
594
encodingType ?: 'url'
589
- } ) {
595
+ sortBy ?: {
596
+ column : 'name' | 'created_at' | 'updated_at'
597
+ order ?: string
598
+ }
599
+ } ) : Promise < ListObjectsV2Result > {
590
600
const limit = Math . min ( options ?. maxKeys || 1000 , 1000 )
591
601
const prefix = options ?. prefix || ''
592
602
const delimiter = options ?. delimiter
593
603
594
- const cursor = options ?. cursor ? decodeContinuationToken ( options ? .cursor ) : undefined
604
+ const cursor = options ?. cursor ? decodeContinuationToken ( options . cursor ) : undefined
595
605
let searchResult = await this . db . listObjectsV2 ( this . bucketId , {
596
606
prefix : options ?. prefix ,
597
607
delimiter : options ?. delimiter ,
598
608
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
+ } ,
601
616
} )
602
617
603
618
let prevPrefix = ''
@@ -638,15 +653,31 @@ export class ObjectStorage {
638
653
const objects : Obj [ ] = [ ]
639
654
searchResult . forEach ( ( obj ) => {
640
655
const target = obj . id === null ? folders : objects
656
+ const name = obj . id === null && ! obj . name . endsWith ( '/' ) ? obj . name + '/' : obj . name
641
657
target . push ( {
642
658
...obj ,
643
- name : options ?. encodingType === 'url' ? encodeURIComponent ( obj . name ) : obj . name ,
659
+ name : options ?. encodingType === 'url' ? encodeURIComponent ( name ) : name ,
644
660
} )
645
661
} )
646
662
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
+ }
650
681
651
682
return {
652
683
hasNext : isTruncated ,
@@ -806,16 +837,42 @@ export class ObjectStorage {
806
837
}
807
838
}
808
839
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
811
845
}
812
846
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
+ }
815
853
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
+ }
818
860
}
861
+ return Buffer . from ( result . slice ( 0 , - 1 ) ) . toString ( 'base64' )
862
+ }
819
863
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
821
878
}
0 commit comments