Skip to content

Commit 0d75d70

Browse files
authored
@tus/s3-store: add termination extension (#401)
1 parent 95366fb commit 0d75d70

File tree

5 files changed

+78
-7
lines changed

5 files changed

+78
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ The tus protocol supports optional [extensions][]. Below is a table of the suppo
110110
| [Creation With Upload][] ||||
111111
| [Expiration][] ||||
112112
| [Checksum][] ||||
113-
| [Termination][] || ||
113+
| [Termination][] || ||
114114
| [Concatenation][] ||||
115115

116116
## Demos

packages/s3-store/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ but may increase it to not exceed the S3 10K parts limit.
6767

6868
Options to pass to the AWS S3 SDK.
6969
Checkout the [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/s3clientconfig.html)
70-
docs for the supported options. You need to at least set the `region`, `bucket` name, and your preferred method of authentication.
70+
docs for the supported options. You need to at least set the `region`, `bucket` name, and your preferred method of authentication.
7171

7272
## Extensions
7373

@@ -79,9 +79,13 @@ The tus protocol supports optional [extensions][]. Below is a table of the suppo
7979
| [Creation With Upload][] ||
8080
| [Expiration][] ||
8181
| [Checksum][] ||
82-
| [Termination][] | |
82+
| [Termination][] | |
8383
| [Concatenation][] ||
8484

85+
### Termination
86+
87+
After a multipart upload is aborted, no additional parts can be uploaded using that upload ID. The storage consumed by any previously uploaded parts will be freed. However, if any part uploads are currently in progress, those part uploads might or might not succeed. As a result, it might be necessary to set an [S3 Lifecycle configuration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html) to abort incomplete multipart uploads.
88+
8589
## Examples
8690

8791
### Example: using `credentials` to fetch credentials inside a AWS container

packages/s3-store/index.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ export class S3Store extends DataStore {
7272
super()
7373
const {partSize, s3ClientConfig} = options
7474
const {bucket, ...restS3ClientConfig} = s3ClientConfig
75-
this.extensions = ['creation', 'creation-with-upload', 'creation-defer-length']
75+
this.extensions = [
76+
'creation',
77+
'creation-with-upload',
78+
'creation-defer-length',
79+
'termination',
80+
]
7681
this.bucket = bucket
7782
this.preferredPartSize = partSize || 8 * 1024 * 1024
7883
this.client = new aws.S3(restS3ClientConfig)
@@ -517,4 +522,36 @@ export class S3Store extends DataStore {
517522

518523
this.saveMetadata(file, upload_id)
519524
}
525+
526+
public async remove(id: string): Promise<void> {
527+
try {
528+
const {upload_id} = await this.getMetadata(id)
529+
if (upload_id) {
530+
await this.client
531+
.abortMultipartUpload({
532+
Bucket: this.bucket,
533+
Key: id,
534+
UploadId: upload_id,
535+
})
536+
.promise()
537+
}
538+
} catch (error) {
539+
if (error?.code && ['NoSuchKey', 'NoSuchUpload'].includes(error.code)) {
540+
log('remove: No file found.', error)
541+
throw ERRORS.FILE_NOT_FOUND
542+
}
543+
throw error
544+
}
545+
546+
await this.client
547+
.deleteObjects({
548+
Bucket: this.bucket,
549+
Delete: {
550+
Objects: [{Key: id}, {Key: `${id}.info`}],
551+
},
552+
})
553+
.promise()
554+
555+
this.clearCache(id)
556+
}
520557
}

packages/s3-store/test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ describe('S3DataStore', function () {
7373

7474
shared.shouldHaveStoreMethods()
7575
shared.shouldCreateUploads()
76-
// Termination extension not implemented yet
77-
// shared.shouldRemoveUploads()
76+
shared.shouldRemoveUploads() // Termination extension
7877
shared.shouldWriteUploads()
7978
shared.shouldHandleOffset()
8079
shared.shouldDeclareUploadLength() // Creation-defer-length extension

test/stores.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,37 @@ export const shouldRemoveUploads = function () {
8383
await this.datastore.create(file)
8484
return this.datastore.remove(file.id)
8585
})
86+
87+
it('should delete the file during upload', async function () {
88+
const file = new Upload({
89+
id: 'termination-test',
90+
size: this.testFileSize,
91+
offset: 0,
92+
metadata: {filename: 'terminate_during_upload.pdf', is_confidential: null},
93+
})
94+
await this.datastore.create(file)
95+
96+
const readable = fs.createReadStream(this.testFilePath, {highWaterMark: 100 * 1024})
97+
// Pause between chunks read to make sure that file is still uploading when terminate function is invoked
98+
readable.on('data', () => {
99+
readable.pause()
100+
setTimeout(() => readable.resume(), 1000)
101+
})
102+
103+
await Promise.allSettled([
104+
this.datastore.write(readable, file.id, 0),
105+
this.datastore.remove(file.id),
106+
])
107+
108+
try {
109+
await this.datastore.getUpload(file.id)
110+
assert.fail('getUpload should have thrown an error')
111+
} catch (error) {
112+
assert.equal([404, 410].includes(error?.status_code), true)
113+
}
114+
115+
readable.destroy()
116+
})
86117
})
87118
}
88119

@@ -93,7 +124,7 @@ export const shouldWriteUploads = function () {
93124
return this.datastore.write(stream, 'doesnt_exist', 0).should.be.rejected()
94125
})
95126

96-
it('should reject whean readable stream has an error', async function () {
127+
it('should reject when readable stream has an error', async function () {
97128
const stream = fs.createReadStream(this.testFilePath)
98129
return this.datastore.write(stream, 'doesnt_exist', 0).should.be.rejected()
99130
})

0 commit comments

Comments
 (0)