diff --git a/config.yml b/config.yml index da989bd..de6b310 100644 --- a/config.yml +++ b/config.yml @@ -39,4 +39,7 @@ settings: # authentication & authorization queue auth_queue: ${AUTH_QUEUE}:alkemio-files # MILLISECONDS wait time for a response after a request on the message queue - response_timeout: ${QUEUE_RESPONSE_TIMEOUT}:10000 \ No newline at end of file + response_timeout: ${QUEUE_RESPONSE_TIMEOUT}:10000 + # TTL in seconds, how much time a document should be cached for + # The service tell browser clients to cache the resource via the 'Cache-Control' and 'Etag' headers + document_max_age: ${DOCUMENT_MAX_AGE}:86400 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 473fa2c..ce5e729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "file-service", - "version": "0.1.1", + "version": "0.1.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "file-service", - "version": "0.1.1", + "version": "0.1.2", "license": "EUPL-1.2", "dependencies": { "@fastify/cookie": "^9.3.1", + "@fastify/cors": "^9.0.1", + "@fastify/etag": "^5.2.0", "@fastify/helmet": "^11.1.1", "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.2.2", @@ -972,6 +974,14 @@ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" }, + "node_modules/@fastify/etag": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@fastify/etag/-/etag-5.2.0.tgz", + "integrity": "sha512-8uFpxgMCH2Q91j6Ty5aMOTdfEy2azu+elLR9leMlYWrk5pN+NbM30riVjgr42jj0Mm6VOvtXhSMVLL7zVxaRXQ==", + "dependencies": { + "fastify-plugin": "^4.0.0" + } + }, "node_modules/@fastify/fast-json-stringify-compiler": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", diff --git a/package.json b/package.json index 1dc2351..4a40dc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "file-service", - "version": "0.1.1", + "version": "0.1.2", "description": "A file serving microservice for the Alkemio platform", "main": "main.ts", "scripts": { @@ -28,6 +28,8 @@ "homepage": "https://github.com/alkem-io/file-service#readme", "dependencies": { "@fastify/cookie": "^9.3.1", + "@fastify/cors": "^9.0.1", + "@fastify/etag": "^5.2.0", "@fastify/helmet": "^11.1.1", "@nestjs/common": "^10.3.8", "@nestjs/config": "^3.2.2", diff --git a/src/config/config.type.ts b/src/config/config.type.ts index 36f3c44..d805d1a 100644 --- a/src/config/config.type.ts +++ b/src/config/config.type.ts @@ -1,5 +1,6 @@ export interface ConfigType { rabbitmq: { + /** # Connection in the form of 'amqp://[user]:[password]@[host]:[port]?heartbeat=[heartbeat]' */ connection: { host: string; port: number; @@ -10,20 +11,39 @@ export interface ConfigType { }; monitoring: { logging: { + /** A flag setting whether Winston Console transport will be enabled. + If the flag is set to true logs of the appropriate level (see below) will be outputted to the console + after the application has been bootstrapped. + The NestJS bootstrap process is handled by the internal NestJS logging. + */ enabled: boolean; + /** Logging level for outputs to console. + Valid values are log|error|warn|debug|verbose. */ level: string; + /** The logging format will be in json - useful for parsing + if disabled - will be in a human-readable form */ json: boolean; }; }; settings: { + /** */ application: { + /** */ storage: { + /** Absolute path to the local storage of the files */ storage_path: string; }; + /** */ address: string; + /** The port on which the service is running */ port: number; + /** authentication & authorization queue */ auth_queue: string; + /** MILLISECONDS wait time for a response after a request on the message queue */ response_timeout: number; + /** TTL in seconds, how much time a document should be cached for + The service tell browser clients to cache the resource via the 'Cache-Control' and 'Etag' headers */ + document_max_age: number; }; }; } diff --git a/src/main.ts b/src/main.ts index 8724cdb..53a5afe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,9 +4,11 @@ import { FastifyAdapter, NestFastifyApplication, } from '@nestjs/platform-fastify'; +import { ConfigService } from '@nestjs/config'; import helmet from '@fastify/helmet'; import fastifyCookie from '@fastify/cookie'; -import { ConfigService } from '@nestjs/config'; +import cors from '@fastify/cors'; +import etag from '@fastify/etag'; import { AppModule } from './app.module'; import { ConfigType } from './config'; @@ -29,18 +31,12 @@ const isProd = process.env.NODE_ENV === 'production'; const logger = app.get(WINSTON_MODULE_NEST_PROVIDER); app.useLogger(logger); - app.enableCors({ - origin: '*', - allowedHeaders: [ - 'Origin,X-Requested-With', - 'Content-Type,Accept', - 'Authorization', - ], - methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'], - }); - await app.register(fastifyCookie); await app.register(helmet, { contentSecurityPolicy: false }); + await app.register(cors, { origin: false }); + // set the header manually, because this plugin skips generation for Stream (our case) + // it only supports String and Buffer + await app.register(etag); const configService: ConfigService = app.get(ConfigService); const port = configService.get('settings.application.port', { infer: true }); diff --git a/src/services/file-reader/file.controller.ts b/src/services/file-reader/file.controller.ts index ff0cf6d..e0cbd59 100644 --- a/src/services/file-reader/file.controller.ts +++ b/src/services/file-reader/file.controller.ts @@ -12,18 +12,27 @@ import { Res, StreamableFile, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { FastifyReply } from 'fastify'; import { FileService } from './file.service'; import { DocumentData, FileInfoErrorCode } from './types'; import { FileInfoException } from './exceptions'; +import { ConfigType } from '../../config'; @Controller('/rest/storage') export class FileController { + private readonly documentMaxAge: number; constructor( private readonly fileService: FileService, + private readonly configService: ConfigService, @Inject(WINSTON_MODULE_NEST_PROVIDER) private logger: LoggerService, - ) {} + ) { + this.documentMaxAge = this.configService.get( + 'settings.application.document_max_age', + { infer: true }, + ); + } @Get('document/:id') public async file( @@ -46,11 +55,11 @@ export class FileController { res.headers({ 'Content-Type': `${documentData.mimeType}`, - 'Cache-Control': 'public, max-age=15552000', + 'Cache-Control': `public, max-age=${this.documentMaxAge}`, Pragma: 'public', - Expires: new Date(Date.now() + 15552000 * 1000).toUTCString(), + Expires: new Date(Date.now() + this.documentMaxAge * 1000).toUTCString(), + etag: id, }); - this.logger.verbose?.(`Serving document ${id}`); return documentData.file;