diff --git a/README.md b/README.md index 51302c3..7855e6d 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,16 @@ This is a simple wrapper of [Aws S3](https://github.com/aws/aws-sdk-js) client library for NestJS. -### Installation +### Installation (AWS-SDK V3) ```bash -npm install --save @appotter/nestjs-s3 @aws-sdk/client-s3 @aws-sdk/lib-storage uuid +npm install --save @appotter/nestjs-s3 @aws-sdk/client-s3 @aws-sdk/lib-storage @aws-sdk/s3-request-presigner uuid multer +``` + +### Installation (AWS-SDK V2) (Not recommended) + +```bash +npm install --save @appotter/nestjs-s3@2.0.0 aws-sdk uuid multer ``` ### Usage @@ -131,6 +137,15 @@ export class YourService { // Also available with all S3 instance methods // this.s3Service.getClient().[all-method-of-S3-instance](); + + // Signed Url (support V3 only) + async signedUrl(file: string): Promise { + // expires in 1 hour + const signed = await this.s3Service.signedUrl(file, 60*60); + + console.log(signed); + // https://test.s3.ap-southeast-1.amazonaws.com/fake.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=test%2F20240131%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240131T110201Z&X-Amz-Expires=60&X-Amz-Signature=25875526097b1f0182b27009005e70f9e92cd67294fb60583de9bd0b5f1cc5a7&X-Amz-SignedHeaders=host&x-id=GetObject + } } ``` diff --git a/package-lock.json b/package-lock.json index 5da2283..9206303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.502.0", "@aws-sdk/lib-storage": "^3.502.0", + "@aws-sdk/s3-request-presigner": "^3.503.1", "@nestjs/common": "^10.3.1", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.1", @@ -41,6 +42,7 @@ "peerDependencies": { "@aws-sdk/client-s3": "^3.502.0", "@aws-sdk/lib-storage": "^3.502.0", + "@aws-sdk/s3-request-presigner": "^3.503.1", "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "uuid": "^9.0.1" } @@ -773,6 +775,25 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.503.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.503.1.tgz", + "integrity": "sha512-HYUQb7E+K9cbZct8hnhKX0ZnApLdQ+DtbM1YV3Cvk0j/sC6USmk/r+lX688bOtBEx8Cq0SlueQtVNQvw8E3NoA==", + "dev": true, + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-format-url": "3.502.0", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/signature-v4-multi-region": { "version": "3.502.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.502.0.tgz", @@ -847,6 +868,21 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.502.0.tgz", + "integrity": "sha512-4+0zBD0ZIJqtTzSE6VRruRwUx3lG+is8Egv+LN99X5y7i6OdrS9ePYHbCJ9FxkzTThgbkUq6k2W7psEDYvn4VA==", + "dev": true, + "dependencies": { + "@aws-sdk/types": "3.502.0", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.495.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.495.0.tgz", @@ -9446,6 +9482,22 @@ "tslib": "^2.5.0" } }, + "@aws-sdk/s3-request-presigner": { + "version": "3.503.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.503.1.tgz", + "integrity": "sha512-HYUQb7E+K9cbZct8hnhKX0ZnApLdQ+DtbM1YV3Cvk0j/sC6USmk/r+lX688bOtBEx8Cq0SlueQtVNQvw8E3NoA==", + "dev": true, + "requires": { + "@aws-sdk/signature-v4-multi-region": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-format-url": "3.502.0", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + } + }, "@aws-sdk/signature-v4-multi-region": { "version": "3.502.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.502.0.tgz", @@ -9505,6 +9557,18 @@ "tslib": "^2.5.0" } }, + "@aws-sdk/util-format-url": { + "version": "3.502.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.502.0.tgz", + "integrity": "sha512-4+0zBD0ZIJqtTzSE6VRruRwUx3lG+is8Egv+LN99X5y7i6OdrS9ePYHbCJ9FxkzTThgbkUq6k2W7psEDYvn4VA==", + "dev": true, + "requires": { + "@aws-sdk/types": "3.502.0", + "@smithy/querystring-builder": "^2.1.1", + "@smithy/types": "^2.9.1", + "tslib": "^2.5.0" + } + }, "@aws-sdk/util-locate-window": { "version": "3.495.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.495.0.tgz", diff --git a/package.json b/package.json index 9e4f096..93112db 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.502.0", "@aws-sdk/lib-storage": "^3.502.0", + "@aws-sdk/s3-request-presigner": "^3.503.1", "@nestjs/common": "^10.3.1", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.1", @@ -63,9 +64,10 @@ "uuid": "^9.0.1" }, "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "@aws-sdk/client-s3": "^3.502.0", "@aws-sdk/lib-storage": "^3.502.0", + "@aws-sdk/s3-request-presigner": "^3.503.1", + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "uuid": "^9.0.1" }, "jest": { @@ -82,4 +84,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/s3.service.spec.ts b/src/s3.service.spec.ts index 38a64a7..3cc5c9f 100644 --- a/src/s3.service.spec.ts +++ b/src/s3.service.spec.ts @@ -111,6 +111,8 @@ describe('S3Service', () => { } catch (error) { expect(error.message).toBe('cannot put a file'); } + + expect(Logger.error).toHaveBeenCalled(); }); it('put a file as unique name', async () => { @@ -166,6 +168,8 @@ describe('S3Service', () => { } catch (error) { expect(error.message).toBe('cannot list all files'); } + + expect(Logger.error).toHaveBeenCalled(); }); it('get a file', async () => { @@ -198,6 +202,8 @@ describe('S3Service', () => { } catch (error) { expect(error.message).toBe('cannot get a file'); } + + expect(Logger.error).toHaveBeenCalled(); }); it('delete a file', async () => { @@ -223,5 +229,26 @@ describe('S3Service', () => { } catch (error) { expect(error.message).toBe('cannot delete a file'); } + + expect(Logger.error).toHaveBeenCalled(); + }); + + it('signed url with 60 second', async () => { + const data = await service.signedUrl('fake.png', 60); + + expect(data).toContain('X-Amz-Algorithm'); + expect(data).toContain('X-Amz-Signature'); + }); + + it('should throws exception if cannot signed url', async () => { + fakeS3Client.on(GetObjectCommand).rejects(new Error('cannot signed url')); + + try { + await service.signedUrl('fake.png', 60); + } catch (error) { + console.log(error); + + expect(error.message).toBe('cannot signed url'); + } }); }); diff --git a/src/s3.service.ts b/src/s3.service.ts index 6fbfcf5..9a5d4d4 100644 --- a/src/s3.service.ts +++ b/src/s3.service.ts @@ -25,6 +25,7 @@ import { } from '@aws-sdk/client-s3'; import { Upload } from '@aws-sdk/lib-storage'; import { Readable } from 'stream'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; @Injectable() export class S3Service { @@ -188,4 +189,23 @@ export class S3Service { throw error; } } + + public async signedUrl(key: string, expiresIn: number): Promise { + const objectParams = { + Bucket: this.bucket, + Key: key, + }; + + try { + return await getSignedUrl( + this.client, + new GetObjectCommand(objectParams), + { expiresIn }, + ); + } catch (error) { + Logger.error(error); + + throw error; + } + } }