@@ -7,22 +7,25 @@ import http from 'node:http'
77import AWS , { NoSuchKey , NotFound , S3 , S3ClientConfig } from '@aws-sdk/client-s3'
88import debug from 'debug'
99
10- import { DataStore , StreamSplitter , Upload } from '@tus/server'
11- import { ERRORS , TUS_RESUMABLE } from '@tus/server'
10+ import {
11+ DataStore ,
12+ StreamSplitter ,
13+ Upload ,
14+ ERRORS ,
15+ TUS_RESUMABLE ,
16+ KvStore ,
17+ MemoryKvStore ,
18+ } from '@tus/server'
1219
1320const log = debug ( 'tus-node-server:stores:s3store' )
1421
15- function calcOffsetFromParts ( parts ?: Array < AWS . Part > ) {
16- // @ts -expect-error not undefined
17- return parts && parts . length > 0 ? parts . reduce ( ( a , b ) => a + b . Size , 0 ) : 0
18- }
19-
2022type Options = {
2123 // The preferred part size for parts send to S3. Can not be lower than 5MiB or more than 5GiB.
2224 // The server calculates the optimal part size, which takes this size into account,
2325 // but may increase it to not exceed the S3 10K parts limit.
2426 partSize ?: number
2527 useTags ?: boolean
28+ cache ?: KvStore < MetadataValue >
2629 expirationPeriodInMilliseconds ?: number
2730 // Options to pass to the AWS S3 SDK.
2831 s3ClientConfig : S3ClientConfig & { bucket : string }
@@ -33,6 +36,12 @@ type MetadataValue = {
3336 'upload-id' : string
3437 'tus-version' : string
3538}
39+
40+ function calcOffsetFromParts ( parts ?: Array < AWS . Part > ) {
41+ // @ts -expect-error not undefined
42+ return parts && parts . length > 0 ? parts . reduce ( ( a , b ) => a + b . Size , 0 ) : 0
43+ }
44+
3645// Implementation (based on https://github.com/tus/tusd/blob/master/s3store/s3store.go)
3746//
3847// Once a new tus upload is initiated, multiple objects in S3 are created:
@@ -68,7 +77,7 @@ type MetadataValue = {
6877// to S3.
6978export class S3Store extends DataStore {
7079 private bucket : string
71- private cache : Map < string , MetadataValue > = new Map ( )
80+ private cache : KvStore < MetadataValue >
7281 private client : S3
7382 private preferredPartSize : number
7483 private expirationPeriodInMilliseconds = 0
@@ -93,6 +102,7 @@ export class S3Store extends DataStore {
93102 this . expirationPeriodInMilliseconds = options . expirationPeriodInMilliseconds ?? 0
94103 this . useTags = options . useTags ?? true
95104 this . client = new S3 ( restS3ClientConfig )
105+ this . cache = options . cache ?? new MemoryKvStore < MetadataValue > ( )
96106 }
97107
98108 protected shouldUseExpirationTags ( ) {
@@ -152,8 +162,8 @@ export class S3Store extends DataStore {
152162 * HTTP calls to S3.
153163 */
154164 private async getMetadata ( id : string ) : Promise < MetadataValue > {
155- const cached = this . cache . get ( id )
156- if ( cached ?. file ) {
165+ const cached = await this . cache . get ( id )
166+ if ( cached ) {
157167 return cached
158168 }
159169
@@ -162,7 +172,7 @@ export class S3Store extends DataStore {
162172 Key : this . infoKey ( id ) ,
163173 } )
164174 const file = JSON . parse ( ( await Body ?. transformToString ( ) ) as string )
165- this . cache . set ( id , {
175+ const metadata : MetadataValue = {
166176 'tus-version' : Metadata ?. [ 'tus-version' ] as string ,
167177 'upload-id' : Metadata ?. [ 'upload-id' ] as string ,
168178 file : new Upload ( {
@@ -172,8 +182,9 @@ export class S3Store extends DataStore {
172182 metadata : file . metadata ,
173183 creation_date : file . creation_date ,
174184 } ) ,
175- } )
176- return this . cache . get ( id ) as MetadataValue
185+ }
186+ await this . cache . set ( id , metadata )
187+ return metadata
177188 }
178189
179190 private infoKey ( id : string ) {
@@ -423,10 +434,12 @@ export class S3Store extends DataStore {
423434 id : string ,
424435 partNumberMarker ?: string
425436 ) : Promise < Array < AWS . Part > > {
437+ const metadata = await this . getMetadata ( id )
438+
426439 const params : AWS . ListPartsCommandInput = {
427440 Bucket : this . bucket ,
428441 Key : id ,
429- UploadId : this . cache . get ( id ) ?. [ 'upload-id' ] ,
442+ UploadId : metadata [ 'upload-id' ] ,
430443 PartNumberMarker : partNumberMarker ,
431444 }
432445
@@ -450,9 +463,9 @@ export class S3Store extends DataStore {
450463 /**
451464 * Removes cached data for a given file.
452465 */
453- private clearCache ( id : string ) {
466+ private async clearCache ( id : string ) {
454467 log ( `[${ id } ] removing cached data` )
455- this . cache . delete ( id )
468+ await this . cache . delete ( id )
456469 }
457470
458471 private calcOptimalPartSize ( size ?: number ) : number {
@@ -546,7 +559,7 @@ export class S3Store extends DataStore {
546559 const parts = await this . retrieveParts ( id )
547560 await this . finishMultipartUpload ( metadata , parts )
548561 await this . completeMetadata ( metadata . file )
549- this . clearCache ( id )
562+ await this . clearCache ( id )
550563 } catch ( error ) {
551564 log ( `[${ id } ] failed to finish upload` , error )
552565 throw error
@@ -579,8 +592,7 @@ export class S3Store extends DataStore {
579592 // Spaces, can also return NoSuchKey.
580593 if ( error . Code === 'NoSuchUpload' || error . Code === 'NoSuchKey' ) {
581594 return new Upload ( {
582- id,
583- ...this . cache . get ( id ) ?. file ,
595+ ...metadata . file ,
584596 offset : metadata . file . size as number ,
585597 size : metadata . file . size ,
586598 metadata : metadata . file . metadata ,
@@ -594,8 +606,7 @@ export class S3Store extends DataStore {
594606 const incompletePartSize = await this . getIncompletePartSize ( id )
595607
596608 return new Upload ( {
597- id,
598- ...this . cache . get ( id ) ?. file ,
609+ ...metadata . file ,
599610 offset : offset + ( incompletePartSize ?? 0 ) ,
600611 size : metadata . file . size ,
601612 } )
0 commit comments