Skip to content

Commit 2f0381d

Browse files
authored
fix: support s3 pagination with path prefix and url encoding enabled (#687)
1 parent c8e042d commit 2f0381d

File tree

2 files changed

+67
-13
lines changed

2 files changed

+67
-13
lines changed

src/storage/object.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -550,15 +550,14 @@ export class ObjectStorage {
550550
const delimiter = options?.delimiter
551551

552552
const cursor = options?.cursor ? decodeContinuationToken(options?.cursor) : undefined
553-
const searchResult = await this.db.listObjectsV2(this.bucketId, {
553+
let searchResult = await this.db.listObjectsV2(this.bucketId, {
554554
prefix: options?.prefix,
555555
delimiter: options?.delimiter,
556556
maxKeys: limit + 1,
557557
nextToken: cursor,
558558
startAfter: cursor || options?.startAfter,
559559
})
560560

561-
let results = searchResult
562561
let prevPrefix = ''
563562

564563
if (delimiter) {
@@ -575,32 +574,36 @@ export class ObjectStorage {
575574
prevPrefix = currPrefix
576575
delimitedResults.push({
577576
id: null,
578-
name: options?.encodingType === 'url' ? encodeURIComponent(currPrefix) : currPrefix,
577+
name: currPrefix,
579578
bucket_id: object.bucket_id,
580579
})
581580
continue
582581
}
583582

584-
delimitedResults.push({
585-
...object,
586-
name: options?.encodingType === 'url' ? encodeURIComponent(object.name) : object.name,
587-
})
583+
delimitedResults.push(object)
588584
}
589-
results = delimitedResults
585+
searchResult = delimitedResults
590586
}
591587

592588
let isTruncated = false
593589

594-
if (results.length > limit) {
595-
results = results.slice(0, limit)
590+
if (searchResult.length > limit) {
591+
searchResult = searchResult.slice(0, limit)
596592
isTruncated = true
597593
}
598594

599-
const folders = results.filter((obj) => obj.id === null)
600-
const objects = results.filter((obj) => obj.id !== null)
595+
const folders: Obj[] = []
596+
const objects: Obj[] = []
597+
searchResult.forEach((obj) => {
598+
const target = obj.id === null ? folders : objects
599+
target.push({
600+
...obj,
601+
name: options?.encodingType === 'url' ? encodeURIComponent(obj.name) : obj.name,
602+
})
603+
})
601604

602605
const nextContinuationToken = isTruncated
603-
? encodeContinuationToken(results[results.length - 1].name)
606+
? encodeContinuationToken(searchResult[searchResult.length - 1].name)
604607
: undefined
605608

606609
return {

src/test/s3-protocol.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ListMultipartUploadsCommand,
1717
ListObjectsCommand,
1818
ListObjectsV2Command,
19+
ListObjectsV2CommandOutput,
1920
ListPartsCommand,
2021
PutObjectCommand,
2122
S3Client,
@@ -377,6 +378,56 @@ describe('S3 Protocol', () => {
377378
})
378379
})
379380

381+
for (const urlEncode of [true, false]) {
382+
const enc = urlEncode ? 'url' : undefined
383+
it(`paginate objects in folder with prefix, Encoding=${enc}`, async () => {
384+
const bucket = await createBucket(client)
385+
const prefixPath = 'this/is/the/path/'
386+
387+
await Promise.all(
388+
new Array(11)
389+
.fill(1)
390+
.map((_, i) => uploadFile(client, bucket, prefixPath + `a-video-file-${i}.mp4`, 1))
391+
)
392+
393+
const seenTokens = new Set()
394+
395+
let continuationToken: string | undefined = undefined
396+
let isTruncated = true
397+
let totalCount = 0
398+
let totalPages = 0
399+
400+
while (isTruncated) {
401+
const resp: ListObjectsV2CommandOutput = await client.send(
402+
new ListObjectsV2Command({
403+
Bucket: bucket,
404+
Prefix: prefixPath,
405+
MaxKeys: 3,
406+
ContinuationToken: continuationToken,
407+
EncodingType: enc,
408+
Delimiter: '/',
409+
})
410+
)
411+
412+
isTruncated = resp.IsTruncated ?? false
413+
totalCount += resp.Contents?.length ?? 0
414+
totalPages++
415+
416+
if (isTruncated) {
417+
expect(resp.Contents?.length ?? 0).toBeGreaterThan(0)
418+
expect(resp.NextContinuationToken).toBeTruthy()
419+
}
420+
421+
expect(seenTokens.has(resp.NextContinuationToken)).toBe(false)
422+
seenTokens.add(resp.NextContinuationToken)
423+
424+
continuationToken = resp.NextContinuationToken
425+
}
426+
expect(totalCount).toBe(11)
427+
expect(totalPages).toBe(4)
428+
})
429+
}
430+
380431
describe('MultiPart Form Data Upload', () => {
381432
it('can upload using multipart/form-data', async () => {
382433
const bucketName = await createBucket(client)

0 commit comments

Comments
 (0)