Skip to content

Commit d2f18d7

Browse files
committed
Implemented AWS S3 image service.
1 parent 7a33d12 commit d2f18d7

File tree

9 files changed

+534
-537
lines changed

9 files changed

+534
-537
lines changed

.env

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ POSTGRES_DB=vrt_db_dev
1414

1515
# static
1616
STATIC_SERVICE=hdd # hdd | s3 - hdd as default if not provided
17-
18-
# AWS_ACCESS_KEY_ID=
19-
# AWS_SECRET_ACCESS_KEY=
20-
# AWS_REGION=
21-
# AWS_S3_BUCKET_NAME=
17+
# Enter below values if STATIC_SERVICE=s3
18+
AWS_ACCESS_KEY_ID=
19+
AWS_SECRET_ACCESS_KEY=
20+
AWS_REGION=
21+
AWS_S3_BUCKET_NAME=
2222

2323
# optional
2424
#HTTPS_KEY_PATH='./secrets/ssl.key'

package-lock.json

Lines changed: 417 additions & 496 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"node": ">=18.12.0"
2727
},
2828
"dependencies": {
29-
"@aws-sdk/client-s3": "^3.715.0",
29+
"@aws-sdk/client-s3": "^3.717.0",
30+
"@aws-sdk/s3-request-presigner": "^3.717.0",
3031
"@nestjs/cache-manager": "^2.1.0",
3132
"@nestjs/common": "^10.2.5",
3233
"@nestjs/config": "^3.1.1",

src/compare/libs/odiff/odiff.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export class OdiffService implements ImageComparator {
2626

2727
constructor(private staticService: StaticService) {
2828
if (!isHddStaticServiceConfigured()) {
29-
throw new Error('OdiffService can only be used with HddService');
29+
return undefined;
30+
// If we throw an exception, the application does not start.
31+
// throw new Error('OdiffService can only be used with HddService');
3032
}
3133
this.hddService = this.staticService as unknown as HddService;
3234
}

src/main.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { readFileSync, existsSync } from 'fs';
88
import { HttpsOptions } from '@nestjs/common/interfaces/external/https-options.interface';
99
import { NestExpressApplication } from '@nestjs/platform-express';
1010
import { HDD_IMAGE_PATH } from './static/hdd/constants';
11-
import { isHddStaticServiceConfigured } from './static/utils';
1211

1312
function getHttpsOptions(): HttpsOptions | null {
1413
const keyPath = './secrets/ssl.key';
@@ -35,16 +34,13 @@ async function bootstrap() {
3534
app.use(bodyParser.json({ limit: process.env.BODY_PARSER_JSON_LIMIT }));
3635
}
3736

38-
// serve images only if hdd configuration
39-
if (isHddStaticServiceConfigured()) {
40-
app.useStaticAssets(join(process.cwd(), HDD_IMAGE_PATH), {
41-
maxAge: 31536000,
42-
// allow cors
43-
setHeaders: (res) => {
44-
res.set('Access-Control-Allow-Origin', '*');
45-
},
46-
});
47-
}
37+
app.useStaticAssets(join(process.cwd(), HDD_IMAGE_PATH), {
38+
maxAge: 31536000,
39+
// allow cors
40+
setHeaders: (res) => {
41+
res.set('Access-Control-Allow-Origin', '*');
42+
},
43+
});
4844

4945
await app.listen(process.env.APP_PORT || 3000);
5046
}

src/static/aws/s3.service.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,66 @@
1-
import { PNGWithMetadata } from 'pngjs';
1+
import { PNG, PNGWithMetadata } from 'pngjs';
22
import { Logger } from '@nestjs/common';
33
import { Static } from '../static.interface';
4-
// import { S3Client } from '@aws-sdk/client-s3';
4+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
5+
import { Readable } from 'stream';
56

67
export class AWSS3Service implements Static {
78
private readonly logger: Logger = new Logger(AWSS3Service.name);
8-
// private readonly AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
9-
// private readonly AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
10-
// private readonly AWS_REGION = process.env.AWS_REGION;
11-
// private readonly AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
9+
private readonly AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
10+
private readonly AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
11+
private readonly AWS_REGION = process.env.AWS_REGION;
12+
private readonly AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
1213

13-
// private s3Client: S3Client;
14+
private s3Client: S3Client;
1415

1516
constructor() {
16-
// this.s3Client = new S3Client({
17-
// credentials: {
18-
// accessKeyId: this.AWS_ACCESS_KEY_ID,
19-
// secretAccessKey: this.AWS_SECRET_ACCESS_KEY,
20-
// },
21-
// region: this.AWS_REGION,
22-
// });
17+
this.s3Client = new S3Client({
18+
credentials: {
19+
accessKeyId: this.AWS_ACCESS_KEY_ID,
20+
secretAccessKey: this.AWS_SECRET_ACCESS_KEY,
21+
},
22+
region: this.AWS_REGION,
23+
});
2324
this.logger.log('AWS S3 service is being used for file storage.');
2425
}
25-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
26-
saveImage(type: 'screenshot' | 'diff' | 'baseline', imageBuffer: Buffer): Promise<string> {
27-
throw new Error('Method not implemented.');
26+
27+
async saveImage(type: 'screenshot' | 'diff' | 'baseline', imageBuffer: Buffer): Promise<string> {
28+
const imageName = `${Date.now()}.${type}.png`;
29+
try {
30+
await this.s3Client.send(
31+
new PutObjectCommand({
32+
Bucket: this.AWS_S3_BUCKET_NAME,
33+
Key: imageName,
34+
ContentType: 'image/png',
35+
Body: imageBuffer,
36+
})
37+
);
38+
return imageName;
39+
} catch (ex) {
40+
throw new Error('Could not save file at AWS S3 : ' + ex);
41+
}
2842
}
2943

30-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
31-
getImage(fileName: string): Promise<PNGWithMetadata> {
32-
throw new Error('Method not implemented.');
44+
async getImage(fileName: string): Promise<PNGWithMetadata> {
45+
if (!fileName) return null;
46+
try {
47+
const command = new GetObjectCommand({ Bucket: this.AWS_S3_BUCKET_NAME, Key: fileName });
48+
const s3Response = await this.s3Client.send(command);
49+
const stream = s3Response.Body as Readable;
50+
return PNG.sync.read(Buffer.concat(await stream.toArray()));
51+
} catch (ex) {
52+
this.logger.error(`Error from read : Cannot get image: ${fileName}. ${ex}`);
53+
}
3354
}
3455

35-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36-
deleteImage(imageName: string): Promise<boolean> {
37-
throw new Error('Method not implemented.');
56+
async deleteImage(imageName: string): Promise<boolean> {
57+
if (!imageName) return false;
58+
try {
59+
await this.s3Client.send(new DeleteObjectCommand({ Bucket: this.AWS_S3_BUCKET_NAME, Key: imageName }));
60+
return true;
61+
} catch (error) {
62+
this.logger.log(`Failed to delete file at AWS S3 for image ${imageName}:`, error);
63+
return false;
64+
}
3865
}
3966
}

src/static/static.controller.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Controller, Get, Logger, Param, Res } from '@nestjs/common';
2+
import { Response } from 'express';
3+
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
4+
import { isHddStaticServiceConfigured, isS3ServiceConfigured } from './utils';
5+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
6+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
7+
8+
@ApiTags('images')
9+
@Controller('images')
10+
export class StaticController {
11+
private readonly logger: Logger = new Logger(StaticController.name);
12+
13+
@Get('/:fileName')
14+
@ApiOkResponse()
15+
async getUrlAndRedirect(@Param('fileName') fileName: string, @Res() res: Response) {
16+
try {
17+
if (isHddStaticServiceConfigured()) {
18+
res.redirect('/' + fileName);
19+
}
20+
if (isS3ServiceConfigured()) {
21+
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID;
22+
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY;
23+
const AWS_REGION = process.env.AWS_REGION;
24+
const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME;
25+
26+
const s3Client = new S3Client({
27+
credentials: {
28+
accessKeyId: `${AWS_ACCESS_KEY_ID}`,
29+
secretAccessKey: `${AWS_SECRET_ACCESS_KEY}`,
30+
},
31+
region: `${AWS_REGION}`,
32+
});
33+
const command = new GetObjectCommand({
34+
Bucket: `${AWS_S3_BUCKET_NAME}`,
35+
Key: fileName,
36+
});
37+
res.redirect(await getSignedUrl(s3Client, command, { expiresIn: 3600 }));
38+
}
39+
} catch (error) {
40+
this.logger.error('Error fetching file from S3:' + fileName, error);
41+
res.status(500).send('Error occurred while getting the file.');
42+
}
43+
}
44+
}

src/static/static.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
33
import { StaticService } from './static.service';
44
import { StaticFactoryService } from './static.factory';
5+
import { StaticController } from './static.controller';
56

67
@Module({
78
imports: [ConfigModule],
89
providers: [StaticService, StaticFactoryService],
910
exports: [StaticService],
11+
controllers: [StaticController],
1012
})
1113
export class StaticModule {}

src/static/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
export function isHddStaticServiceConfigured() {
22
return !process.env.STATIC_SERVICE || process.env.STATIC_SERVICE === 'hdd';
33
}
4+
5+
export function isS3ServiceConfigured() {
6+
return !process.env.STATIC_SERVICE || process.env.STATIC_SERVICE === 's3';
7+
}

0 commit comments

Comments
 (0)