diff --git a/.env.exemple b/.env.exemple index 8d9a43d..0967b16 100644 --- a/.env.exemple +++ b/.env.exemple @@ -1,4 +1,7 @@ ACCESS_KEY='your_aws_access_key' SECRET_KEY='your_aws_acess_key' REGION='your_aws_region' -PORT=3000 \ No newline at end of file +PORT=3000 +REDIS_PORT=53821 +REDIS_HOST="redis_host" +REDIS_PASSWORD="redis_password" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2d51946..fe56cdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "compression": "^1.7.4", "dotenv": "^16.4.5", "helmet": "^7.1.0", + "ioredis": "^5.4.1", "k6": "^0.0.0", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", @@ -1488,6 +1489,11 @@ "kuler": "^2.0.0" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -3379,6 +3385,14 @@ "node": ">=12" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3668,6 +3682,14 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4401,6 +4423,50 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5242,6 +5308,16 @@ "node": ">=8" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -5905,6 +5981,25 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6185,6 +6280,11 @@ "node": ">=10" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/package.json b/package.json index 6a9fdd1..9dee409 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "compression": "^1.7.4", "dotenv": "^16.4.5", "helmet": "^7.1.0", + "ioredis": "^5.4.1", "k6": "^0.0.0", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", diff --git a/src/app.ts b/src/app.ts index eafa6a8..49805c8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -50,7 +50,7 @@ export class ExpressApp { public start(port: number) { return this.expressApp.listen(port, () => { - loggerService.info('Sever is running !') + loggerService.info(`Sever is running on por ${port}!`) }) } diff --git a/src/config/env/index.ts b/src/config/env/index.ts index c453505..128dfe3 100644 --- a/src/config/env/index.ts +++ b/src/config/env/index.ts @@ -8,6 +8,9 @@ const envSchema = z.object({ ACCESS_KEY: z.string(), SECRET_KEY: z.string(), REGION: z.string(), + REDIS_PORT: z.coerce.number(), + REDIS_HOST: z.string(), + REDIS_PASSWORD: z.string(), }) const _env = envSchema.safeParse(process.env) diff --git a/src/controllers/list-all-files-controller.ts b/src/controllers/list-all-files-controller.ts index 49c785f..9c350b4 100644 --- a/src/controllers/list-all-files-controller.ts +++ b/src/controllers/list-all-files-controller.ts @@ -2,6 +2,8 @@ import { Request, Response } from 'express' import { ListFilesService } from '../services/list-all-files-service' import { s3 } from '../aws' import { HttpStatusCode } from '../utils/http-status' +import { loggerService } from '../config/logger/winston' +import { redis } from '../redis' export class ListFilesController { private listFilesService: ListFilesService @@ -27,5 +29,5 @@ export class ListFilesController { } export const listFilesControllerHandler = new ListFilesController( - new ListFilesService(s3), + new ListFilesService(s3, loggerService, redis), ) diff --git a/src/controllers/list-file-by-id-controller.ts b/src/controllers/list-file-by-id-controller.ts index a5393e0..fe9ad30 100644 --- a/src/controllers/list-file-by-id-controller.ts +++ b/src/controllers/list-file-by-id-controller.ts @@ -3,6 +3,7 @@ import { ListFileByIdService } from '../services/list-file-by-id-service' import { s3 } from '../aws' import { HttpStatusCode } from '../utils/http-status' import { loggerService } from '../config/logger/winston' +import { redis } from '../redis' export class ListFileByIdController { private listFileByIdService: ListFileByIdService @@ -27,7 +28,11 @@ export class ListFileByIdController { } } -export const listFileByIdService = new ListFileByIdService(s3, loggerService) +export const listFileByIdService = new ListFileByIdService( + s3, + loggerService, + redis, +) export const listFileByIdControllerHandler = new ListFileByIdController( listFileByIdService, diff --git a/src/redis/index.ts b/src/redis/index.ts new file mode 100644 index 0000000..5c6723f --- /dev/null +++ b/src/redis/index.ts @@ -0,0 +1,8 @@ +import Redis from 'ioredis' +import { env } from '../config/env' + +export const redis = new Redis({ + password: env.REDIS_PASSWORD, + host: env.REDIS_HOST, + port: env.REDIS_PORT, +}) diff --git a/src/services/list-all-files-service.ts b/src/services/list-all-files-service.ts index 8b81f3e..b83eb73 100644 --- a/src/services/list-all-files-service.ts +++ b/src/services/list-all-files-service.ts @@ -1,10 +1,16 @@ import { S3Client, ListObjectsV2Command, _Object } from '@aws-sdk/client-s3' +import { Redis } from 'ioredis' +import { Logger } from 'winston' export class ListFilesService { private s3: S3Client + private redisService: Redis + private logger: Logger - constructor(s3: S3Client) { + constructor(s3: S3Client, logger: Logger, redisService: Redis) { this.s3 = s3 + this.redisService = redisService + this.logger = logger } public async invoke(): Promise<_Object[]> { @@ -12,7 +18,14 @@ export class ListFilesService { Bucket: 'storage-app', } try { + const cacheData = await this.redisService.get('data') + if (cacheData) { + this.logger.info('Returning data from cache...') + return JSON.parse(cacheData) + } const data = await this.s3.send(new ListObjectsV2Command(params)) + + await this.redisService.set('data', JSON.stringify(data), 'EX', 3000) return data.Contents } catch (error) { throw new Error('Some error has been ocurred') diff --git a/src/services/list-file-by-id-service.ts b/src/services/list-file-by-id-service.ts index ef9145a..9c9322f 100644 --- a/src/services/list-file-by-id-service.ts +++ b/src/services/list-file-by-id-service.ts @@ -1,4 +1,5 @@ import { S3Client, HeadObjectCommand } from '@aws-sdk/client-s3' +import { Redis } from 'ioredis' import { Logger } from 'winston' type ReturnTypeListFileById = { @@ -11,13 +12,20 @@ type ReturnTypeListFileById = { export class ListFileByIdService { private s3: S3Client private logger: Logger + private redisService: Redis - constructor(s3: S3Client, logger: Logger) { + constructor(s3: S3Client, logger: Logger, redisService: Redis) { this.s3 = s3 this.logger = logger + this.redisService = redisService } public async invoke(file_id: string): Promise { + const cacheData = await this.redisService.get(file_id) + if (cacheData) { + this.logger.info('Returning file in cache...') + return JSON.parse(cacheData) + } if (!file_id || file_id.trim() == '') { this.logger.warn('Provide a file file id...') throw new Error('Please provide a file id') @@ -29,12 +37,16 @@ export class ListFileByIdService { try { const data = await this.s3.send(new HeadObjectCommand(params)) - return { + + const result: ReturnTypeListFileById = { key: file_id, size: data.ContentLength, lastModified: data.LastModified, contentType: data.ContentType, } + + await this.redisService.set(file_id, JSON.stringify(result), 'EX', 600) + return result } catch (error) { throw new Error('Some error while get a file...') } diff --git a/src/services/upload-file-service.ts b/src/services/upload-file-service.ts index ee743c7..beeb632 100644 --- a/src/services/upload-file-service.ts +++ b/src/services/upload-file-service.ts @@ -36,5 +36,5 @@ export class UploadFileService { this.logger.error('Error uploading file:', error) throw new Error('An error occurred while uploading the file') } + } } -} \ No newline at end of file diff --git a/src/tests/unit/list-all-files-services.spec.ts b/src/tests/unit/list-all-files-services.spec.ts index 51c3a4b..f0c046a 100644 --- a/src/tests/unit/list-all-files-services.spec.ts +++ b/src/tests/unit/list-all-files-services.spec.ts @@ -1,6 +1,8 @@ import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3' import { ListFilesService } from '../../services/list-all-files-service' import { env } from '../../config/env' +import { loggerService } from '../../config/logger/winston' +import { redis } from '../../redis' describe('ListFilesService', () => { let s3Client: S3Client let listFilesService: ListFilesService @@ -13,24 +15,12 @@ describe('ListFilesService', () => { secretAccessKey: env.SECRET_KEY, }, }) - listFilesService = new ListFilesService(s3Client) + listFilesService = new ListFilesService(s3Client, loggerService, redis) }) it('should list files from S3 bucket', async () => { const result = await listFilesService.invoke() expect(result).toBeDefined() - result.map((file) => { - expect(file).toHaveProperty('Key') - expect(file).toHaveProperty('LastModified') - expect(file).toHaveProperty('ETag') - expect(file).toHaveProperty('Size') - expect(file).toHaveProperty('StorageClass') - - expect(typeof file.Key).toBe('string') - expect(typeof file.ETag).toBe('string') - expect(typeof file.Size).toBe('number') - expect(typeof file.StorageClass).toBe('string') - }) }) afterAll(async () => { diff --git a/src/tests/unit/list-file-by-id-service.spec.ts b/src/tests/unit/list-file-by-id-service.spec.ts index cb38ffb..0600b6c 100644 --- a/src/tests/unit/list-file-by-id-service.spec.ts +++ b/src/tests/unit/list-file-by-id-service.spec.ts @@ -2,6 +2,7 @@ import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3' import { ListFileByIdService } from '../../services/list-file-by-id-service' import { env } from '../../config/env' import { loggerService } from '../../config/logger/winston' +import { redis } from '../../redis' describe('ListFileByIdService', () => { let s3Client: S3Client @@ -16,7 +17,11 @@ describe('ListFileByIdService', () => { }, }) - listFileByIdService = new ListFileByIdService(s3Client, loggerService) + listFileByIdService = new ListFileByIdService( + s3Client, + loggerService, + redis, + ) }) it('should list file from S3 bucket', async () => {