refactor(file v2) - deletion (#20356)

This commit is contained in:
Etienne
2026-05-11 18:16:46 +02:00
committed by GitHub
parent b1f7a2c544
commit 1b09c69c39
8 changed files with 34 additions and 228 deletions
@@ -73,52 +73,6 @@ describe('FileStorageService', () => {
mockFileStorageDriverFactory.getCurrentDriver.mockReturnValue(mockDriver);
});
describe('deleteLegacy', () => {
it('should delegate to the current driver with filename', async () => {
const deleteParams = {
folderPath: 'documents',
filename: 'test.txt',
};
mockDriver.delete.mockResolvedValue(undefined);
await service.deleteLegacy(deleteParams);
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
expect(mockDriver.delete).toHaveBeenCalledWith(deleteParams);
});
it('should delegate to the current driver without filename (delete folder)', async () => {
const deleteParams = {
folderPath: 'documents',
};
mockDriver.delete.mockResolvedValue(undefined);
await service.deleteLegacy(deleteParams);
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
expect(mockDriver.delete).toHaveBeenCalledWith(deleteParams);
});
it('should handle delete errors', async () => {
const deleteParams = {
folderPath: 'documents',
filename: 'test.txt',
};
const error = new Error('Delete failed');
mockDriver.delete.mockRejectedValue(error);
await expect(service.deleteLegacy(deleteParams)).rejects.toThrow(
'Delete failed',
);
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
expect(mockDriver.delete).toHaveBeenCalledWith(deleteParams);
});
});
describe('copyLegacy', () => {
it('should delegate to the current driver', async () => {
const copyParams = {
@@ -192,35 +146,5 @@ describe('FileStorageService', () => {
expect(result).toBeNull();
});
});
describe('checkFolderExistsLegacy', () => {
it('should delegate to the current driver and return true', async () => {
const checkParams = {
folderPath: 'documents',
};
mockDriver.checkFolderExists.mockResolvedValue(true);
const result = await service.checkFolderExistsLegacy(checkParams);
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
expect(mockDriver.checkFolderExists).toHaveBeenCalledWith(checkParams);
expect(result).toBe(true);
});
it('should delegate to the current driver and return false', async () => {
const checkParams = {
folderPath: 'nonexistent',
};
mockDriver.checkFolderExists.mockResolvedValue(false);
const result = await service.checkFolderExistsLegacy(checkParams);
expect(fileStorageDriverFactory.getCurrentDriver).toHaveBeenCalled();
expect(mockDriver.checkFolderExists).toHaveBeenCalledWith(checkParams);
expect(result).toBe(false);
});
});
});
});
@@ -152,15 +152,6 @@ export class FileStorageService {
});
}
deleteLegacy(params: {
folderPath: string;
filename?: string;
}): Promise<void> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
return driver.delete(params);
}
async deleteApplicationFiles({
applicationUniversalIdentifier,
workspaceId,
@@ -224,16 +215,33 @@ export class FileStorageService {
path: Like(`${fileFolder}/%`),
},
});
const application = await this.applicationRepository.findOneOrFail({
where: { id: file.applicationId, workspaceId: file.workspaceId },
});
const driver = this.fileStorageDriverFactory.getCurrentDriver();
await driver.delete({
folderPath: `${file.workspaceId}/${file.applicationId}`,
folderPath: `${file.workspaceId}/${application.universalIdentifier}`,
filename: file.path,
});
await this.fileRepository.delete(fileId);
}
async checkIfWorkspaceFolderExists(workspaceId: string): Promise<boolean> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
return driver.checkFolderExists({ folderPath: workspaceId });
}
async deleteWorkspaceFolder(workspaceId: string): Promise<void> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
await driver.delete({ folderPath: workspaceId });
}
copyLegacy(params: {
from: { folderPath: string; filename?: string };
to: { folderPath: string; filename?: string };
@@ -270,12 +278,6 @@ export class FileStorageService {
});
}
checkFolderExistsLegacy(params: { folderPath: string }): Promise<boolean> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
return driver.checkFolderExists(params);
}
checkFileExists(params: ResourceIdentifier): Promise<boolean> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
const onStoragePath = this.buildOnStoragePath(params);
@@ -7,8 +7,6 @@ import { FileAiChatModule } from 'src/engine/core-modules/file/file-ai-chat/file
import { FilePathGuard } from 'src/engine/core-modules/file/guards/file-path-guard';
import { FileDeletionJob } from 'src/engine/core-modules/file/jobs/file-deletion.job';
import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/jobs/file-workspace-folder-deletion.job';
import { FileAttachmentListener } from 'src/engine/core-modules/file/listeners/file-attachment.listener';
import { FileWorkspaceMemberListener } from 'src/engine/core-modules/file/listeners/file-workspace-member.listener';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -42,8 +40,6 @@ import { FileService } from './services/file.service';
FileService,
FilePathGuard,
FileByIdGuard,
FileAttachmentListener,
FileWorkspaceMemberListener,
FileWorkspaceFolderDeletionJob,
FileDeletionJob,
],
@@ -1,42 +1,35 @@
import { UnrecoverableError } from 'bullmq';
import { extractFolderPathFilenameAndTypeOrThrow } from 'twenty-shared/utils';
import { FileFolder } from 'twenty-shared/types';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
export type FileDeletionJobData = {
workspaceId: string;
fullPath: string;
fileId: string;
fileFolder: FileFolder;
};
@Processor(MessageQueue.deleteCascadeQueue)
export class FileDeletionJob {
constructor(private readonly fileService: FileService) {}
constructor(private readonly fileStorageService: FileStorageService) {}
@Process(FileDeletionJob.name)
async handle(data: FileDeletionJobData): Promise<void> {
const { workspaceId, fullPath } = data;
const { folderPath, filename } =
extractFolderPathFilenameAndTypeOrThrow(fullPath);
if (!filename) {
throw new UnrecoverableError(
`[${FileDeletionJob.name}] Cannot parse filename from full path - ${fullPath}`,
);
}
async handle({
workspaceId,
fileId,
fileFolder,
}: FileDeletionJobData): Promise<void> {
try {
await this.fileService.deleteFile({
await this.fileStorageService.deleteByFileId({
fileId,
workspaceId,
filename,
folderPath,
fileFolder,
});
} catch {
throw new Error(
`[${FileDeletionJob.name}] Cannot delete file - ${fullPath}`,
`[${FileDeletionJob.name}] Cannot delete file - ${fileId} in folder ${fileFolder}`,
);
}
}
@@ -18,7 +18,6 @@ export class FileWorkspaceFolderDeletionJob {
try {
await this.fileService.deleteWorkspaceFolder(workspaceId);
} catch (error) {
//todo: clean up error message once issue on workspace folder deletion is fixed + in s3 driver file
throw new Error(
`[${FileWorkspaceFolderDeletionJob.name}] Cannot delete workspace folder - ${workspaceId} - ${error?.message || error}`,
);
@@ -1,40 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecordDestroyEvent } from 'twenty-shared/database-events';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import {
FileDeletionJob,
type FileDeletionJobData,
} from 'src/engine/core-modules/file/jobs/file-deletion.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { type AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity';
@Injectable()
export class FileAttachmentListener {
constructor(
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@OnDatabaseBatchEvent('attachment', DatabaseEventAction.DESTROYED)
async handleDestroyEvent(
payload: WorkspaceEventBatch<
ObjectRecordDestroyEvent<AttachmentWorkspaceEntity>
>,
) {
for (const event of payload.events) {
await this.messageQueueService.add<FileDeletionJobData>(
FileDeletionJob.name,
{
workspaceId: payload.workspaceId,
fullPath: event.properties.before.fullPath ?? '',
},
);
}
}
}
@@ -1,43 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type ObjectRecordDestroyEvent } from 'twenty-shared/database-events';
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import {
FileDeletionJob,
type FileDeletionJobData,
} from 'src/engine/core-modules/file/jobs/file-deletion.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event-batch.type';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class FileWorkspaceMemberListener {
constructor(
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
private readonly messageQueueService: MessageQueueService,
) {}
@OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DESTROYED)
async handleDestroyEvent(
payload: WorkspaceEventBatch<
ObjectRecordDestroyEvent<WorkspaceMemberWorkspaceEntity>
>,
) {
for (const event of payload.events) {
const avatarUrl = event.properties.before.avatarUrl;
if (!avatarUrl) {
continue;
}
this.messageQueueService.add<FileDeletionJobData>(FileDeletionJob.name, {
workspaceId: payload.workspaceId,
fullPath: event.properties.before.avatarUrl ?? '',
});
}
}
}
@@ -189,39 +189,14 @@ export class FileService {
};
}
/** @deprecated Use FileStorageService.deleteByFileId instead */
async deleteFile({
folderPath,
filename,
workspaceId,
}: {
folderPath: string;
filename: string;
workspaceId: string;
}) {
const workspaceFolderPath = `workspace-${workspaceId}/${folderPath}`;
return await this.fileStorageService.deleteLegacy({
folderPath: workspaceFolderPath,
filename,
});
}
/** @deprecated */
async deleteWorkspaceFolder(workspaceId: string) {
const workspaceFolderPath = `workspace-${workspaceId}`;
const isWorkspaceFolderFound =
await this.fileStorageService.checkFolderExistsLegacy({
folderPath: workspaceFolderPath,
});
await this.fileStorageService.checkIfWorkspaceFolderExists(workspaceId);
if (!isWorkspaceFolderFound) {
return;
}
return await this.fileStorageService.deleteLegacy({
folderPath: workspaceFolderPath,
});
return await this.fileStorageService.deleteWorkspaceFolder(workspaceId);
}
}