Front component s3 redirect (#21116)
# Introduction Unload the server of the file stream when possible Also fix inconsistent pipeline exception management Needs to highly be QA, not sure how the cors will behave here
This commit is contained in:
+49
-27
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
Param,
|
||||
Res,
|
||||
UseFilters,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
FileStorageException,
|
||||
FileStorageExceptionCode,
|
||||
} from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { setFileResponseHeaders } from 'src/engine/core-modules/file/utils/set-file-response-headers.utils';
|
||||
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
@@ -39,6 +41,8 @@ import { WorkspaceMigrationRunnerRestApiExceptionFilter } from 'src/engine/works
|
||||
WorkspaceMigrationRunnerRestApiExceptionFilter,
|
||||
)
|
||||
export class FrontComponentController {
|
||||
private readonly logger = new Logger(FrontComponentController.name);
|
||||
|
||||
constructor(private readonly frontComponentService: FrontComponentService) {}
|
||||
|
||||
@Get(':frontComponentId')
|
||||
@@ -48,40 +52,58 @@ export class FrontComponentController {
|
||||
@Param('frontComponentId') frontComponentId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
) {
|
||||
try {
|
||||
const fileStream =
|
||||
await this.frontComponentService.getBuiltComponentStream({
|
||||
frontComponentId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
const fileResponse = await this.frontComponentService
|
||||
.getBuiltComponentPresignedUrlOrStream({
|
||||
frontComponentId,
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof FrontComponentException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/javascript');
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new FrontComponentException(
|
||||
'Front component built file not found',
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await pipeline(fileStream, res);
|
||||
} catch (error) {
|
||||
// Mid-stream error: client already received partial data, nothing to do
|
||||
if (res.headersSent) {
|
||||
return;
|
||||
}
|
||||
this.logger.error(
|
||||
'getBuiltComponentPresignedUrlOrStream failed unexpectedly',
|
||||
{ error },
|
||||
);
|
||||
|
||||
if (
|
||||
error instanceof FileStorageException &&
|
||||
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
|
||||
) {
|
||||
throw new FrontComponentException(
|
||||
'Front component built file not found',
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND,
|
||||
'Error retrieving front component built file',
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY,
|
||||
);
|
||||
});
|
||||
|
||||
if (fileResponse.type === 'redirect') {
|
||||
return res.redirect(fileResponse.presignedUrl);
|
||||
}
|
||||
|
||||
setFileResponseHeaders(res, fileResponse.mimeType);
|
||||
|
||||
try {
|
||||
await pipeline(fileResponse.stream, res);
|
||||
} catch (error) {
|
||||
this.logger.error('Front component stream failed mid-transfer', {
|
||||
error,
|
||||
});
|
||||
|
||||
if (!res.headersSent) {
|
||||
throw new FrontComponentException(
|
||||
'Error streaming front component built file',
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY,
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof FrontComponentException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new FrontComponentException(
|
||||
`Error retrieving front component built file: ${error.message}`,
|
||||
FrontComponentExceptionCode.FRONT_COMPONENT_NOT_READY,
|
||||
);
|
||||
res.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-5
@@ -1,13 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type';
|
||||
import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
@@ -32,6 +33,7 @@ export class FrontComponentService {
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<FrontComponentDTO[]> {
|
||||
@@ -296,13 +298,13 @@ export class FrontComponentService {
|
||||
return frontComponent;
|
||||
}
|
||||
|
||||
async getBuiltComponentStream({
|
||||
async getBuiltComponentPresignedUrlOrStream({
|
||||
frontComponentId,
|
||||
workspaceId,
|
||||
}: {
|
||||
frontComponentId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Readable> {
|
||||
}): Promise<FileResponse> {
|
||||
const frontComponent = await this.findByIdOrThrow(
|
||||
frontComponentId,
|
||||
workspaceId,
|
||||
@@ -315,11 +317,29 @@ export class FrontComponentService {
|
||||
},
|
||||
);
|
||||
|
||||
return this.fileStorageService.readFile({
|
||||
const mimeType = 'application/javascript';
|
||||
const resourceIdentifier = {
|
||||
workspaceId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
resourcePath: frontComponent.builtComponentPath,
|
||||
};
|
||||
|
||||
const presignedUrl = await this.fileStorageService.getPresignedUrl({
|
||||
...resourceIdentifier,
|
||||
expiresInSeconds: this.twentyConfigService.get(
|
||||
'STORAGE_S3_PRESIGNED_URL_EXPIRES_IN',
|
||||
),
|
||||
responseContentType: mimeType,
|
||||
responseContentDisposition: getContentDisposition(mimeType),
|
||||
});
|
||||
|
||||
if (presignedUrl) {
|
||||
return { type: 'redirect', presignedUrl };
|
||||
}
|
||||
|
||||
const stream = await this.fileStorageService.readFile(resourceIdentifier);
|
||||
|
||||
return { type: 'stream', stream, mimeType };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user