Repository: internxt/drive-server-wip
Pull request #484: [PB-3705] feat: migrate storage endpoints
Author: login: evillalba94
Labels: name: ready-for-preview
Created: 2025-03-13T10:34:23Z | Merged: 2025-03-21T13:47:23Z

### Updates
- Migrating storage-related endpoints and database deletions
---

### Endpoints

- `drive-server` `DELETE` _/storage/folder/:id_ `->` `wip`  _/storage/trash/folder/:folderId_
Param: `:folderId` == Folder.id
- `drive-server` `DELETE` _/storage/folder/:folderid/file/:fileid_ 
`drive-server` `DELETE` /storage/bucket/:bucketid/file/:fileid_  `->` `wip` _/files/:uuid_
Param: `:uuid` == File.uuid



Diff:
diff --git a/src/externals/notifications/storage.notifications.service.ts b/src/externals/notifications/storage.notifications.service.ts
index c59f3b7ec..581e54552 100644
--- a/src/externals/notifications/storage.notifications.service.ts
+++ b/src/externals/notifications/storage.notifications.service.ts
@@ -11,6 +11,8 @@ enum StorageEvents {
   FILE_UPDATED = 'FILE_UPDATED',
   FOLDER_UPDATED = 'FOLDER_UPDATED',
   ITEMS_TO_TRASH = 'ITEMS_TO_TRASH',
+  FILE_DELETED = 'FILE_DELETED',
+  FOLDER_DELETED = 'FOLDER_DELETED',
   PLAN_UPDATED = 'PLAN_UPDATED',
 }
 
@@ -58,6 +60,20 @@ export class StorageNotificationService {
     this.getTokensAndSendApnNotification(user.uuid);
   }
 
+  fileDeleted({ payload, user, clientId }: EventArguments) {
+    const event = new NotificationEvent(
+      'notification.itemDeleted',
+      payload,
+      user.email,
+      clientId,
+      user.uuid,
+      StorageEvents.FILE_DELETED,
+    );
+
+    this.notificationService.add(event);
+    this.getTokensAndSendApnNotification(user.uuid);
+  }
+
   folderCreated({ payload, user, clientId }: EventArguments) {
     const event = new NotificationEvent(
       'notification.itemCreated',
@@ -86,6 +102,20 @@ export class StorageNotificationService {
     this.getTokensAndSendApnNotification(user.uuid);
   }
 
+  folderDeleted({ payload, user, clientId }: EventArguments) {
+    const event = new NotificationEvent(
+      'notification.itemDeleted',
+      payload,
+      user.email,
+      clientId,
+      user.uuid,
+      StorageEvents.FOLDER_DELETED,
+    );
+
+    this.notificationService.add(event);
+    this.getTokensAndSendApnNotification(user.uuid);
+  }
+
   itemsTrashed({ payload, user, clientId }: EventArguments) {
     const event = new NotificationEvent(
       'notification.itemsToTrash',
diff --git a/src/modules/backups/backup.controller.ts b/src/modules/backups/backup.controller.ts
index 67423cc5c..b94cb4991 100644
--- a/src/modules/backups/backup.controller.ts
+++ b/src/modules/backups/backup.controller.ts
@@ -1,12 +1,4 @@
-import {
-  Body,
-  Controller,
-  Get,
-  HttpCode,
-  Param,
-  Patch,
-  Post,
-} from '@nestjs/common';
+import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
 import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
 import { User as UserDecorator } from '../auth/decorators/user.decorator';
 import { User } from '../user/user.domain';
@@ -20,7 +12,6 @@ export class BackupController {
   constructor(private readonly backupUseCases: BackupUseCase) {}
 
   @Post('/activate')
-  @HttpCode(200)
   @ApiOperation({
     summary: 'Activate user backup',
   })
@@ -30,7 +21,6 @@ export class BackupController {
   }
 
   @Post('/deviceAsFolder')
-  @HttpCode(200)
   @ApiOperation({
     summary: 'Create a folder using device name',
   })
@@ -43,7 +33,6 @@ export class BackupController {
   }
 
   @Get('/deviceAsFolder/:uuid')
-  @HttpCode(200)
   @ApiOperation({
     summary: 'Get device as folder by uuid',
   })
@@ -56,7 +45,6 @@ export class BackupController {
   }
 
   @Patch('/deviceAsFolder/:uuid')
-  @HttpCode(200)
   @ApiOperation({
     summary: 'Update device as folder by uuid',
   })
@@ -74,7 +62,6 @@ export class BackupController {
   }
 
   @Get('/deviceAsFolder')
-  @HttpCode(200)
   @ApiOperation({
     summary: 'Get all devices as folder',
   })
diff --git a/src/modules/file/file.controller.spec.ts b/src/modules/file/file.controller.spec.ts
index 5861425c9..a8eebccd6 100644
--- a/src/modules/file/file.controller.spec.ts
+++ b/src/modules/file/file.controller.spec.ts
@@ -17,11 +17,13 @@ import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
 import { ThumbnailDto } from '../thumbnail/dto/thumbnail.dto';
 import { CreateThumbnailDto } from '../thumbnail/dto/create-thumbnail.dto';
 import { ThumbnailModule } from '../thumbnail/thumbnail.module';
+import { BridgeModule } from './../../externals/bridge/bridge.module';
 
 describe('FileController', () => {
   let fileController: FileController;
   let fileUseCases: FileUseCases;
   let thumbnailUseCases: ThumbnailUseCases;
+
   let file: File;
   const clientId = 'drive-web';
 
@@ -59,7 +61,8 @@ describe('FileController', () => {
 
   beforeEach(async () => {
     const module: TestingModule = await Test.createTestingModule({
-      controllers: [FileController, ThumbnailModule],
+      imports: [BridgeModule, ThumbnailModule],
+      controllers: [FileController],
       providers: [FileUseCases],
     })
       .useMocker(() => createMock())
@@ -320,4 +323,33 @@ describe('FileController', () => {
       ).rejects.toThrow(InternalServerErrorException);
     });
   });
+
+  describe('deleteFileByUuid', () => {
+    const uuid = v4();
+    const clientId = 'clientId';
+    it('When a valid uuid is provided, then it should return a success response', async () => {
+      jest
+        .spyOn(fileUseCases, 'deleteFilePermanently')
+        .mockResolvedValue({ id: 1234, uuid: file.uuid });
+      const result = await fileController.deleteFileByUuid(
+        userMocked,
+        uuid,
+        clientId,
+      );
+      expect(result).toEqual({ deleted: true });
+      expect(fileUseCases.deleteFilePermanently).toHaveBeenCalledWith(
+        userMocked,
+        { uuid },
+      );
+    });
+
+    it('When an error occurs during deletion, then it should throw', async () => {
+      jest
+        .spyOn(fileUseCases, 'deleteFilePermanently')
+        .mockRejectedValue(new NotFoundException());
+      await expect(
+        fileController.deleteFileByUuid(userMocked, uuid, clientId),
+      ).rejects.toThrow(NotFoundException);
+    });
+  });
 });
diff --git a/src/modules/file/file.controller.ts b/src/modules/file/file.controller.ts
index 11d3ccb65..0a52c038c 100644
--- a/src/modules/file/file.controller.ts
+++ b/src/modules/file/file.controller.ts
@@ -2,6 +2,7 @@ import {
   BadRequestException,
   Body,
   Controller,
+  Delete,
   Get,
   Logger,
   NotFoundException,
@@ -464,4 +465,27 @@ export class FileController {
   ): Promise<ThumbnailDto> {
     return this.thumbnailUseCases.createThumbnail(user, createThumbnailDto);
   }
+
+  @Delete('/:uuid')
+  @ApiBearerAuth()
+  @ApiOperation({
+    summary: 'Delete file from storage and database',
+  })
+  async deleteFileByUuid(
+    @UserDecorator() user: User,
+    @Param('uuid', ValidateUUIDPipe) uuid: string,
+    @Client() clientId: string,
+  ) {
+    const { id } = await this.fileUseCases.deleteFilePermanently(user, {
+      uuid,
+    });
+
+    this.storageNotificationService.fileDeleted({
+      payload: { id, uuid },
+      user,
+      clientId,
+    });
+
+    return { deleted: true };
+  }
 }
diff --git a/src/modules/file/file.repository.spec.ts b/src/modules/file/file.repository.spec.ts
index b70c52240..bb555db86 100644
--- a/src/modules/file/file.repository.spec.ts
+++ b/src/modules/file/file.repository.spec.ts
@@ -539,4 +539,65 @@ describe('FileRepository', () => {
       expect(result).toEqual(0);
     });
   });
+
+  describe('deleteFilesByUser ', () => {
+    it('When files are deleted successfully, then it should call update with correct parameters', async () => {
+      const user = newUser();
+      const files = [{ id: 123456 }, { id: 654321 }] as any;
+
+      await repository.deleteFilesByUser(user, files);
+
+      expect(fileModel.update).toHaveBeenCalledWith(
+        {
+          removed: true,
+          removedAt: expect.any(Date),
+          status: expect.any(String),
+          updatedAt: expect.any(Date),
+        },
+        {
+          where: {
+            userId: user.id,
+            id: {
+              [Op.in]: files.map(({ id }) => id),
+            },
+          },
+        },
+      );
+    });
+
+    it('When an error occurs during the update, then it should throw an error', async () => {
+      const user = newUser();
+      const files = [{ id: 123456 }, { id: 654321 }] as any;
+
+      jest
+        .spyOn(fileModel, 'update')
+        .mockRejectedValue(new Error('Update failed'));
+
+      await expect(repository.deleteFilesByUser(user, files)).rejects.toThrow(
+        'Update failed',
+      );
+    });
+  });
+
+  describe('destroyFile', () => {
+    it('When destroyFile is called, then it should call destroy with correct parameters', async () => {
+      const where = { id: 1234567 };
+
+      await repository.destroyFile(where);
+
+      expect(fileModel.destroy).toHaveBeenCalledWith({ where });
+    });
+
+    it('When an error occurs during the destroy, then it should throw an error', async () => {
+      const where = { id: 1234567 };
+
+      jest
+        .spyOn(fileModel, 'destroy')
+        .mockRejectedValue(new Error('Destroy failed'));
+
+      await expect(repository.destroyFile(where)).rejects.toThrow(
+        'Destroy failed',
+      );
+    });
+  });
 });
diff --git a/src/modules/file/file.repository.ts b/src/modules/file/file.repository.ts
index 69a677c10..58c8d8297 100644
--- a/src/modules/file/file.repository.ts
+++ b/src/modules/file/file.repository.ts
@@ -22,6 +22,8 @@ import { WorkspaceAttributes } from '../workspaces/attributes/workspace.attribut
 export interface FileRepository {
   create(file: Omit<FileAttributes, 'id'>): Promise<File | null>;
   deleteByFileId(fileId: any): Promise<any>;
+  deleteFilesByUser(user: User, files: File[]): Promise<void>;
+  destroyFile(where: Partial<FileModel>): Promise<void>;
   findByIdNotDeleted(
     id: FileAttributes['id'],
     where: Partial<FileAttributes>,
@@ -764,6 +766,10 @@ export class SequelizeFileRepository implements FileRepository {
     );
   }
 
+  async destroyFile(where: Partial<FileModel>): Promise<void> {
+    await this.fileModel.destroy({ where });
+  }
+
   private toDomain(model: FileModel): File {
     const buildUser = (userData: UserModel | null) =>
       userData ? User.build(userData) : null;
diff --git a/src/modules/file/file.usecase.spec.ts b/src/modules/file/file.usecase.spec.ts
index e8494258c..5ca6f36a9 100644
--- a/src/modules/file/file.usecase.spec.ts
+++ b/src/modules/file/file.usecase.spec.ts
@@ -36,6 +36,8 @@ import { SharingService } from '../sharing/sharing.service';
 import { SharingItemType } from '../sharing/sharing.domain';
 import { CreateFileDto } from './dto/create-file.dto';
 import { UpdateFileMetaDto } from './dto/update-file-meta.dto';
+import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
+import { ThumbnailModule } from '../thumbnail/thumbnail.module';
 
 const fileId = '6295c99a241bb000083f1c6a';
 const userId = 1;
@@ -48,6 +50,7 @@ describe('FileUseCases', () => {
   let sharingService: SharingService;
   let bridgeService: BridgeService;
   let cryptoService: CryptoService;
+  let thumbnailUseCases: ThumbnailUseCases;
 
   const userMocked = User.build({
     id: 1,
@@ -94,6 +97,7 @@ describe('FileUseCases', () => {
     bridgeService = module.get<BridgeService>(BridgeService);
     cryptoService = module.get<CryptoService>(CryptoService);
     sharingService = module.get<SharingService>(SharingService);
+    thumbnailUseCases = module.get<ThumbnailUseCases>(ThumbnailUseCases);
   });
 
   afterEach(() => {
@@ -233,6 +237,8 @@ describe('FileUseCases', () => {
 
   describe('delete file use case', () => {
     const incrementalUserId = 15494;
+    const fileId = '2618494108';
+    const bucket = 'test';
     const userMock = User.build({
       id: incrementalUserId,
       userId: 'userId',
@@ -264,14 +270,6 @@ describe('FileUseCases', () => {
     });
 
     it.skip('should be able to delete a trashed file', async () => {
-      const fileId = '6f10f732-59b1-525c-a2d0-ff538f687903';
-      const file = {
-        id: 1,
-        fileId,
-        deleted: true,
-        userId: incrementalUserId,
-      } as File;
-
       jest
         .spyOn(fileRepository, 'deleteByFileId')
         .mockImplementationOnce(() => Promise.resolve());
@@ -280,57 +278,25 @@ describe('FileUseCases', () => {
         .spyOn(bridgeService, 'deleteFile')
         .mockImplementationOnce(() => Promise.resolve());
 
-      await service.deleteFilePermanently(file, userMock);
+      await service.deleteFilePermanently(userMock, { fileId, bucket });
 
       expect(fileRepository.deleteByFileId).toHaveBeenCalledWith(fileId);
     });
 
     it.skip('should fail when the folder trying to delete has not been trashed', async () => {
-      const fileId = 2618494108;
-      const file = File.build({
-        id: fileId,
-        fileId: '6f10f732-59b1-525c-a2d0-ff538f687903',
-        name: '',
-        type: '',
-        size: null,
-        bucket: '',
-        folderId: 4,
-        encryptVersion: '',
-        deleted: false,
-        deletedAt: new Date(),
-        userId: incrementalUserId,
-        creationTime: new Date(),
-        modificationTime: new Date(),
-        createdAt: new Date(),
-        updatedAt: new Date(),
-        uuid: '',
-        folderUuid: '',
-        removed: false,
-        removedAt: undefined,
-        plainName: '',
-        status: FileStatus.EXISTS,
-      });
-
       jest.spyOn(fileRepository, 'deleteByFileId');
 
-      expect(service.deleteFilePermanently(file, userMock)).rejects.toThrow(
-        new UnprocessableEntityException(
-          `file with id ${fileId} cannot be permanently deleted`,
-        ),
-      );
+      await service.deleteFilePermanently(userMock, { fileId, bucket });
       expect(fileRepository.deleteByFileId).not.toHaveBeenCalled();
     });
 
     it.skip('should fail when the folder trying to delete is not owned by the user', async () => {
-      const file = {
-        userId: incrementalUserId + 1,
-        deleted: true,
-      } as File;
-
       jest.spyOn(bridgeService, 'deleteFile');
       jest.spyOn(fileRepository, 'deleteByFileId');
 
-      expect(service.deleteFilePermanently(file, userMock)).rejects.toThrow(
+      expect(
+        service.deleteFilePermanently(userMock, { fileId, bucket }),
+      ).rejects.toThrow(
         new ForbiddenException(`You are not owner of this share`),
       );
       expect(bridgeService.deleteFile).not.toHaveBeenCalled();
@@ -338,31 +304,6 @@ describe('FileUseCases', () => {
     });
 
     it.skip('should not delete a file from storage if delete shares fails', async () => {
-      const fileId = 2618494108;
-      const file = File.build({
-        id: fileId,
-        fileId: '6f10f732-59b1-525c-a2d0-ff538f687903',
-        name: '',
-        type: '',
-        size: null,
-        bucket: '',
-        folderId: 4,
-        encryptVersion: '',
-        deleted: true,
-        deletedAt: new Date(),
-        userId: incrementalUserId,
-        creationTime: new Date(),
-        modificationTime: new Date(),
-        createdAt: new Date(),
-        updatedAt: new Date(),
-        uuid: '',
-        folderUuid: '',
-        removed: false,
-        removedAt: undefined,
-        plainName: '',
-        status: FileStatus.EXISTS,
-      });
-
       const errorReason = new Error('reason');
 
       jest
@@ -372,7 +313,7 @@ describe('FileUseCases', () => {
 
       expect.assertions(3);
       try {
-        await service.deleteFilePermanently(file, userMock);
+        await service.deleteFilePermanently(userMock, { fileId, bucket });
       } catch (err) {
         expect(err).toBe(errorReason);
       }
@@ -382,32 +323,6 @@ describe('FileUseCases', () => {
     });
 
     it.skip('should not delete a file from databse if could not be deleted from storage', async () => {
-      const fileId = 2618494108;
-      const file = File.build({
-        id: fileId,
-        fileId: '6f10f732-59b1-525c-a2d0-ff538f687903',
-        name: '',
-        type: '',
-        size: null,
-        bucket: '',
-        folderId: 4,
-        encryptVersion: '',
-        deleted: true,
-        deletedAt: new Date(),
-        userId: incrementalUserId,
-        creationTime: new Date(),
-        modificationTime: new Date(),
-        createdAt: new Date(),
-        updatedAt: new Date(),
-        user: userMock,
-        uuid: '',
-        folderUuid: '',
-        removed: false,
-        removedAt: undefined,
-        plainName: '',
-        status: FileStatus.EXISTS,
-      });
-
       const errorReason = new Error('reason');
 
       jest
@@ -420,7 +335,7 @@ describe('FileUseCases', () => {
 
       expect.assertions(2);
       try {
-        await service.deleteFilePermanently(file, userMock);
+        await service.deleteFilePermanently(userMock, { fileId, bucket });
       } catch (err) {
         expect(err).toBe(errorReason);
       }
@@ -1131,4 +1046,97 @@ describe('FileUseCases', () => {
       expect(result).toEqual(totalUsage);
     });
   });
+
+  describe('deleteFilePermanently', () => {
+    const mockFile = newFile({ owner: userMocked });
+
+    it('When the file exists and is owned by the user, then it should delete the file and return its id and uuid', async () => {
+      const mockFileFound = {
+        ...mockFile,
+        isOwnedBy: (user: User) => user.id === userMocked.id,
+      } as any;
+      jest.spyOn(fileRepository, 'findOneBy').mockResolvedValue(mockFileFound);
+
+      jest
+        .spyOn(sharingService, 'bulkRemoveSharings')
+        .mockResolvedValue(undefined);
+      jest
+        .spyOn(thumbnailUseCases, 'deleteThumbnailByFileId')
+        .mockResolvedValue(undefined);
+      jest
+        .spyOn(fileRepository, 'deleteFilesByUser')
+        .mockResolvedValue(undefined);
+
+      const result = await service.deleteFilePermanently(userMocked, {
+        id: mockFile.id,
+      });
+
+      expect(result).toEqual({ id: mockFile.id, uuid: mockFile.uuid });
+      expect(fileRepository.findOneBy).toHaveBeenCalledWith({
+        id: mockFile.id,
+        removed: false,
+      });
+      expect(sharingService.bulkRemoveSharings).toHaveBeenCalledWith(
+        userMocked,
+        [mockFile.uuid],
+        SharingItemType.File,
+      );
+      expect(thumbnailUseCases.deleteThumbnailByFileId).toHaveBeenCalledWith(
+        userMocked,
+        mockFile.id,
+      );
+      expect(fileRepository.deleteFilesByUser).toHaveBeenCalledWith(
+        userMocked,
+        [mockFileFound],
+      );
+    });
+
+    it('When the file does not exist, then it should throw a NotFoundException', async () => {
+      jest.spyOn(fileRepository, 'findOneBy').mockResolvedValue(null);
+
+      await expect(
+        service.deleteFilePermanently(userMocked, { id: mockFile.id }),
+      ).rejects.toThrow(NotFoundException);
+    });
+
+    it('When the file is not owned by the user, then it should throw a ForbiddenException', async () => {
+      const anotherUser = newUser();
+
+      jest.spyOn(fileRepository, 'findOneBy').mockResolvedValue({
+        ...mockFile,
+        userId: anotherUser.id,
+        isOwnedBy: (user: User) => user.id === anotherUser.id,
+      } as any);
+
+      await expect(
+        service.deleteFilePermanently(userMocked, { id: mockFile.id }),
+      ).rejects.toThrow(ForbiddenException);
+    });
+  });
+
+  describe('getFile', () => {
+    const mockFile = newFile();
+
+    it('When the file exists, then it should return the file', async () => {
+      jest.spyOn(fileRepository, 'findOneBy').mockResolvedValueOnce(mockFile);
+
+      const result = await service.getFile({ id: mockFile.id });
+
+      expect(result).toEqual(mockFile);
+      expect(fileRepository.findOneBy).toHaveBeenCalledWith({
+        id: mockFile.id,
+      });
+    });
+
+    it('When the file does not exist, then it should return null', async () => {
+      jest.spyOn(fileRepository, 'findOneBy').mockResolvedValueOnce(null);
+
+      const result = await service.getFile({ id: mockFile.id });
+
+      expect(result).toBeNull();
+      expect(fileRepository.findOneBy).toHaveBeenCalledWith({
+        id: mockFile.id,
+      });
+    });
+  });
 });
diff --git a/src/modules/file/file.usecase.ts b/src/modules/file/file.usecase.ts
index ef89d0926..f38a14e05 100644
--- a/src/modules/file/file.usecase.ts
+++ b/src/modules/file/file.usecase.ts
@@ -37,6 +37,8 @@ import { Folder } from '../folder/folder.domain';
 import { getPathFileData } from '../../lib/path';
 import { isStringEmpty } from '../../lib/validators';
 import { FileModel } from './file.model';
+import { ShareUseCases } from '../share/share.usecase';
+import { ThumbnailUseCases } from '../thumbnail/thumbnail.usecase';
 
 export type SortParamsFile = Array<[SortableFileAttributes, 'ASC' | 'DESC']>;
 
@@ -50,6 +52,8 @@ export class FileUseCases {
     private sharingUsecases: SharingService,
     private network: BridgeService,
     private cryptoService: CryptoService,
+    private shareUsecases: ShareUseCases,
+    private thumbnailUsecases: ThumbnailUseCases,
   ) {}
 
   getByUuid(uuid: FileAttributes['uuid']): Promise<File> {
@@ -75,8 +79,38 @@ export class FileUseCases {
     throw new Error('Method not implemented.');
   }
 
-  async deleteFilePermanently(file: File, user: User): Promise<void> {
-    throw new Error('Method not implemented.');
+  async deleteFilePermanently(
+    user: User,
+    where: Partial<File>,
+  ): Promise<{ id: number; uuid: string }> {
+    const file = await this.fileRepository.findOneBy({
+      ...where,
+      removed: false,
+    });
+
+    if (!file) {
+      throw new NotFoundException('File not found');
+    }
+
+    if (!file.isOwnedBy(user)) {
+      throw new ForbiddenException('This file is not yours');
+    }
+
+    const { id, uuid } = file;
+
+    await Promise.all([
+      this.shareUsecases.deleteFileShare(id, user),
+      this.sharingUsecases.bulkRemoveSharings(
+        user,
+        [uuid],
+        SharingItemType.File,
+      ),
+      this.thumbnailUsecases.deleteThumbnailByFileId(user, id),
+    ]);
+
+    await this.fileRepository.deleteFilesByUser(user, [file]);
+
+    return { id, uuid };
   }
 
   async getFileMetadata(user: User, fileUuid: File['uuid']): Promise<File> {
@@ -747,4 +781,8 @@ export class FileUseCases {
     );
     return file;
   }
+
+  async getFile(where: Partial<File>): Promise<File> {
+    return this.fileRepository.findOneBy(where);
+  }
 }
diff --git a/src/modules/folder/folder.controller.spec.ts b/src/modules/folder/folder.controller.spec.ts
index 5fd7458d9..dfaf5c34c 100644
--- a/src/modules/folder/folder.controller.spec.ts
+++ b/src/modules/folder/folder.controller.spec.ts
@@ -20,6 +20,7 @@ import { CalculateFolderSizeTimeoutException } from './exception/calculate-folde
 import { User } from '../user/user.domain';
 import { FileStatus } from '../file/file.domain';
 import { InvalidParentFolderException } from './exception/invalid-parent-folder';
+import { StorageNotificationService } from './../../externals/notifications/storage.notifications.service';
 
 const requester = newUser();
 
@@ -28,6 +29,7 @@ describe('FolderController', () => {
   let folderUseCases: FolderUseCases;
   let fileUseCases: FileUseCases;
   let folder: Folder;
+  let storageNotificationService: StorageNotificationService;
 
   const userMocked = User.build({
     id: 1,
@@ -73,6 +75,9 @@ describe('FolderController', () => {
     folderController = module.get<FolderController>(FolderController);
     folderUseCases = module.get<FolderUseCases>(FolderUseCases);
     fileUseCases = module.get<FileUseCases>(FileUseCases);
+    storageNotificationService = module.get<StorageNotificationService>(
+      StorageNotificationService,
+    );
     folder = newFolder();
   });
 
@@ -644,4 +649,77 @@ describe('FolderController', () => {
       ).rejects.toThrow('Path is too deep');
     });
   });
+
+  describe('deleteFolder', () => {
+    const folderUuidToDelete = 'uuid-to-delete';
+    const userMocked = newUser();
+    const folder = newFolder({
+      attributes: { id: 1, uuid: folderUuidToDelete, userId: userMocked.id },
+    });
+
+    it('When a valid folderUuid is provided, then it should delete the folder and send a notification', async () => {
+      jest
+        .spyOn(folderUseCases, 'getFolderByUuidAndUser')
+        .mockResolvedValue(folder);
+      jest.spyOn(folderUseCases, 'deleteByUser').mockResolvedValue(undefined);
+      jest
+        .spyOn(storageNotificationService, 'folderDeleted')
+        .mockImplementation(() => {});
+
+      const result = await folderController.deleteFolder(
+        userMocked,
+        folderUuidToDelete,
+        'clientId',
+      );
+
+      expect(result).toBeUndefined();
+      expect(folderUseCases.getFolderByUuidAndUser).toHaveBeenCalledWith(
+        folderUuidToDelete,
+        userMocked,
+      );
+      expect(folderUseCases.deleteByUser).toHaveBeenCalledWith(userMocked, [
+        folder,
+      ]);
+      expect(storageNotificationService.folderDeleted).toHaveBeenCalledWith({
+        payload: {
+          id: folder.id,
+          uuid: folderUuidToDelete,
+          userId: userMocked.id,
+        },
+        user: userMocked,
+        clientId: 'clientId',
+      });
+    });
+
+    it('When a non-existent folderUuid is provided, then it should throw an error', async () => {
+      jest
+        .spyOn(folderUseCases, 'getFolderByUuidAndUser')
+        .mockRejectedValue(new NotFoundException());
+
+      await expect(
+        folderController.deleteFolder(
+          userMocked,
+          folderUuidToDelete,
+          'clientId',
+        ),
+      ).rejects.toThrow(NotFoundException);
+    });
+
+    it('When an error occurs during deletion, then it should throw an error', async () => {
+      jest
+        .spyOn(folderUseCases, 'getFolderByUuidAndUser')
+        .mockResolvedValue(folder);
+      jest
+        .spyOn(folderUseCases, 'deleteByUser')
+        .mockRejectedValue(new Error('Deletion failed'));
+
+      await expect(
+        folderController.deleteFolder(
+          userMocked,
+          folderUuidToDelete,
+          'clientId',
+        ),
+      ).rejects.toThrow('Deletion failed');
+    });
+  });
 });
diff --git a/src/modules/folder/folder.controller.ts b/src/modules/folder/folder.controller.ts
index 442b1f9cc..be01843e7 100644
--- a/src/modules/folder/folder.controller.ts
+++ b/src/modules/folder/folder.controller.ts
@@ -5,6 +5,7 @@ import {
   Delete,
   ForbiddenException,
   Get,
+  HttpCode,
   Logger,
   NotFoundException,
   NotImplementedException,
@@ -900,4 +901,24 @@ export class FolderController {
     }
     return folder;
   }
+
+  @Delete('/:uuid')
+  @HttpCode(204)
+  @ApiBearerAuth()
+  @ApiOperation({
+    summary: 'Delete Folder',
+  })
+  async deleteFolder(
+    @UserDecorator() user: User,
+    @Param('uuid', ValidateUUIDPipe) uuid: string,
+    @Client() clientId: string,
+  ) {
+    const folder = await this.folderUseCases.getFolderByUuidAndUser(uuid, user);
+    await this.folderUseCases.deleteByUser(user, [folder]);
+    this.storageNotificationService.folderDeleted({
+      payload: { id: folder.id, uuid, userId: user.id },
+      user: user,
+      clientId,
+    });
+  }
 }
diff --git a/src/modules/folder/folder.repository.ts b/src/modules/folder/folder.repository.ts
index cbb5727c9..06a989aea 100644
--- a/src/modules/folder/folder.repository.ts
+++ b/src/modules/folder/folder.repository.ts
@@ -41,6 +41,7 @@ export interface FolderRepository {
   createWithAttributes(
     newFolder: Omit<FolderAttributes, 'id'>,
   ): Promise<Folder>;
+  deleteByUser(user: User, folders: Folder[]): Promise<void>;
   findAll(): Promise<Array<Folder> | []>;
   findAllByParentIdAndUserId(
     parentId: FolderAttributes['parentId'],
diff --git a/src/modules/folder/folder.usecase.spec.ts b/src/modules/folder/folder.usecase.spec.ts
index 44ea550bc..5070c9a79 100644
--- a/src/modules/folder/folder.usecase.spec.ts
+++ b/src/modules/folder/folder.usecase.spec.ts
@@ -9,6 +9,7 @@ import {
   BadRequestException,
   ConflictException,
   ForbiddenException,
+  NotAcceptableException,
   NotFoundException,
   UnprocessableEntityException,
 } from '@nestjs/common';
@@ -1400,4 +1401,44 @@ describe('FolderUseCases', () => {
       expect(result).toBe(mockFolder);
     });
   });
+
+  describe('deleteByUser ', () => {
+    const userMocked = newUser();
+    userMocked.rootFolderId = 1;
+    const folderMocked: Folder[] = [
+      { id: 2, parentId: 1 } as Folder,
+      { id: 3, parentId: null } as Folder,
+    ];
+
+    it('When folders are deleted successfully, then it should call deleteByUser ', async () => {
+      jest.spyOn(folderRepository, 'deleteByUser').mockResolvedValue(undefined);
+
+      await service.deleteByUser(userMocked, [folderMocked[0]]);
+
+      expect(folderRepository.deleteByUser).toHaveBeenCalledWith(userMocked, [
+        folderMocked[0],
+      ]);
+    });
+
+    it('When trying to delete the root folder, then it should throw an error', async () => {
+      const rootFolder = {
+        id: userMocked.rootFolderId,
+        parentId: null,
+      } as Folder;
+
+      await expect(
+        service.deleteByUser(userMocked, [rootFolder]),
+      ).rejects.toThrow(NotAcceptableException);
+    });
+
+    it('When an error occurs during deletion, then it should throw an error', async () => {
+      jest
+        .spyOn(folderRepository, 'deleteByUser')
+        .mockRejectedValue(new Error('Deletion failed'));
+
+      await expect(
+        service.deleteByUser(userMocked, [folderMocked[0]]),
+      ).rejects.toThrow('Deletion failed');
+    });
+  });
 });
diff --git a/src/modules/folder/folder.usecase.ts b/src/modules/folder/folder.usecase.ts
index 73f0e4ad8..0c2fba38e 100644
--- a/src/modules/folder/folder.usecase.ts
+++ b/src/modules/folder/folder.usecase.ts
@@ -5,6 +5,7 @@ import {
   Inject,
   Injectable,
   Logger,
+  NotAcceptableException,
   NotFoundException,
   UnprocessableEntityException,
   forwardRef,
@@ -911,6 +912,12 @@ export class FolderUseCases {
   }
 
   async deleteByUser(user: User, folders: Folder[]): Promise<void> {
+    const isRootFolder = folders.some(
+      (folder) => folder.id === user.rootFolderId || folder.parentId === null,
+    );
+    if (isRootFolder) {
+      throw new NotAcceptableException('Cannot delete root folder');
+    }
     await this.folderRepository.deleteByUser(user, folders);
   }
 
diff --git a/src/modules/thumbnail/thumbnail.usecase.spec.ts b/src/modules/thumbnail/thumbnail.usecase.spec.ts
index 713cad2c1..5f532aea0 100644
--- a/src/modules/thumbnail/thumbnail.usecase.spec.ts
+++ b/src/modules/thumbnail/thumbnail.usecase.spec.ts
@@ -188,4 +188,55 @@ describe('ThumbnailUseCases', () => {
       expect(result).toEqual(existingThumbnail);
     });
   });
+
+  describe('deleteThumbnailByFileId', () => {
+    it('When the thumbnail exists, it should delete the thumbnail and call the network service', async () => {
+      jest
+        .spyOn(thumbnailRepository, 'findByFileId')
+        .mockResolvedValue(existingThumbnail);
+      jest.spyOn(networkService, 'deleteFile').mockResolvedValue(undefined);
+      jest.spyOn(thumbnailRepository, 'deleteBy').mockResolvedValue(undefined);
+
+      await thumbnailUseCases.deleteThumbnailByFileId(
+        userMocked,
+        createThumbnailDto.fileId,
+      );
+
+      expect(networkService.deleteFile).toHaveBeenCalledWith(
+        userMocked,
+        existingThumbnail.bucket_id,
+        existingThumbnail.bucket_file,
+      );
+      expect(thumbnailRepository.deleteBy).toHaveBeenCalledWith({
+        fileId: createThumbnailDto.fileId,
+      });
+    });
+
+    it('When an error occurs while deleting the file in the network service, it should propagate the error', async () => {
+      jest
+        .spyOn(thumbnailRepository, 'findByFileId')
+        .mockResolvedValue(existingThumbnail);
+      jest
+        .spyOn(networkService, 'deleteFile')
+        .mockRejectedValue(new Error('Network error'));
+
+      await expect(
+        thumbnailUseCases.deleteThumbnailByFileId(
+          userMocked,
+          createThumbnailDto.fileId,
+        ),
+      ).rejects.toThrow('Network error');
+    });
+
+    it('When the thumbnail is not found then should return without throw an error', async () => {
+      jest.spyOn(thumbnailRepository, 'findByFileId').mockResolvedValue(null);
+
+      await expect(
+        thumbnailUseCases.deleteThumbnailByFileId(
+          userMocked,
+          createThumbnailDto.fileId,
+        ),
+      ).resolves.not.toThrow();
+    });
+  });
 });
diff --git a/src/modules/thumbnail/thumbnail.usecase.ts b/src/modules/thumbnail/thumbnail.usecase.ts
index 01f44c09e..acef99d65 100644
--- a/src/modules/thumbnail/thumbnail.usecase.ts
+++ b/src/modules/thumbnail/thumbnail.usecase.ts
@@ -57,9 +57,24 @@ export class ThumbnailUseCases {
       updatedAt: new Date(),
     });
   }
+
+  async deleteThumbnailByFileId(user: User, fileId: number) {
+    const thumbnail = await this.thumbnailRepository.findByFileId(fileId);
+    if (!thumbnail) {
+      return;
+    }
+    await this.network.deleteFile(
+      user,
+      thumbnail.bucket_id,
+      thumbnail.bucket_file,
+    );
+    await this.thumbnailRepository.deleteBy({ fileId });
+  }
+
   async findAll(where: Partial<ThumbnailAttributes>): Promise<Thumbnail[]> {
     return this.thumbnailRepository.findAll(where);
   }
+
   async deleteBy(where: Partial<ThumbnailAttributes>): Promise<void> {
     return this.thumbnailRepository.deleteBy(where);
   }
diff --git a/src/modules/trash/trash.usecase.spec.ts b/src/modules/trash/trash.usecase.spec.ts
index 0ad345445..ca3c1f139 100644
--- a/src/modules/trash/trash.usecase.spec.ts
+++ b/src/modules/trash/trash.usecase.spec.ts
@@ -48,7 +48,12 @@ describe('Trash Use Cases', () => {
       jest.spyOn(folderUseCases, 'getFolder').mockResolvedValue({} as Folder);
       jest
         .spyOn(fileUseCases, 'deleteFilePermanently')
-        .mockImplementation(() => Promise.resolve());
+        .mockImplementationOnce(() =>
+          Promise.resolve({
+            id: 2176796544,
+            uuid: 'bbe6d386-e215-53a0-88ef-1e4c318e6ff9',
+          }),
+        );
       jest
         .spyOn(folderUseCases, 'deleteFolderPermanently')
         .mockImplementation(() => Promise.resolve());
@@ -150,10 +155,20 @@ describe('Trash Use Cases', () => {
       jest.spyOn(folderUseCases, 'getFolder').mockResolvedValue({} as Folder);
       jest
         .spyOn(fileUseCases, 'deleteFilePermanently')
-        .mockImplementationOnce(() => Promise.resolve())
+        .mockImplementationOnce(() =>
+          Promise.resolve({
+            id: 2176796544,
+            uuid: 'bbe6d386-e215-53a0-88ef-1e4c318e6ff9',
+          }),
+        )
         .mockImplementationOnce(() => Promise.reject(error))
         .mockImplementationOnce(() => Promise.reject(error))
-        .mockImplementationOnce(() => Promise.resolve())
+        .mockImplementationOnce(() =>
+          Promise.resolve({
+            id: 2751197087,
+            uuid: '38473164-6261-51af-8eb3-223c334986ce',
+          }),
+        )
         .mockImplementationOnce(() => Promise.reject(error));
       jest
         .spyOn(folderUseCases, 'deleteFolderPermanently')


Reviews:
user: login: apsantiso
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-17T14:26:34Z
user: login: apsantiso
body: Solo cambia lo del endpoint para tratar de tener los endpoints lo mas estructurados posibles y ya puedes pedirle a @sg-gs que corra los scripts
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-18T20:11:20Z
user: login: apsantiso
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-03-19T15:18:15Z
user: login: apsantiso
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T16:15:09Z
user: login: evillalba94
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:22:39Z
user: login: evillalba94
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:27:53Z
user: login: sg-gs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:51:29Z
user: login: apsantiso
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-21T13:43:16Z

Review comments:
diff_hunk: @@ -445,4 +451,68 @@ export class FileController {
       );
     }
   }
+
+  @Post('/thumbnail')
+  @HttpCode(200)
+  @ApiOperation({
+    summary: 'Create Thumbnail',
+  })
+  @ApiOkResponse({ type: ThumbnailDto })
+  @ApiBearerAuth()
+  @RequiredSharingPermissions(SharingActionName.UploadFile)
+  @UseGuards(SharingPermissionsGuard)
+  async createThumbnail(
+    @UserDecorator() user: User,
+    @Body() createThumbnailDto: CreateThumbnailDto,
+  ): Promise<ThumbnailDto> {
+    return this.thumbnailUseCases.createThumbnail(user, createThumbnailDto);
+  }
+
+  @Delete('/:fileId/bucket/:bucketId')
+  @HttpCode(200)
+  @ApiBearerAuth()
+  @ApiOperation({
+    summary: 'Delete file from storage and database',
+  })
+  async deleteFileFromBucket(
+    @UserDecorator() user: User,
+    @Param('fileId') fileId: string,
+    @Param('bucketId') bucketId: string,
+    @Client() clientId: string,
+  ) {
+    return this.handleFileDeletion(
+      user,
+      { fileId, bucket: bucketId },
+      clientId,
+    );
+  }
+
+  @Delete('/:uuid')
+  @HttpCode(200)
+  @ApiBearerAuth()
+  @ApiOperation({
+    summary: 'Delete file from storage and database',
+  })
+  async deleteFileByUuid(
+    @UserDecorator() user: User,
+    @Param('uuid') uuid: string,
+    @Client() clientId: string,
+  ) {
+    return this.handleFileDeletion(user, { uuid }, clientId);
+  }
path: src/modules/file/file.controller.ts
user: login: apsantiso
body: Por favor elimina `/:fileId/bucket/:bucketId`. No necesitamos dos formas de eliminar archivos, con el de `files/:uuid` esta bien, ya que el uuid es unico en la base de datos.  Y asi eliminas el metodo que tienes abajo de `handleFileDeletion`, ya que no sera necesario
created_at: 2025-03-13T19:42:07Z
author_association: COLLABORATOR
diff_hunk: @@ -335,15 +335,23 @@ export class TrashController {
   async deleteFolder(
     @Param('folderId') folderId: number,
     @UserDecorator() user: User,
+    @Client() clientId: string,
   ) {
-    const folders = await this.folderUseCases.getFolders(user.id, {
-      id: folderId,
-    });
+    const folder = await this.folderUseCases.getFolderByUserId(
+      folderId,
+      user.id,
+    );
 
-    if (folders.length === 0) {
-      throw new NotFoundException();
+    if (!folder) {
+      throw new NotFoundException('Folder not found');
     }
 
-    await this.trashUseCases.deleteItems(user, [], [folders[0]]);
+    await this.folderUseCases.deleteByUser(user, [folder]);
+
+    this.storageNotificationService.folderDeleted({
+      payload: { id: folderId, userId: user.id },
+      user: user,
+      clientId,
+    });
   }
path: src/modules/trash/trash.controller.ts
user: login: apsantiso
body: El endpoint para borrar folders es `trash/folders/:folderId` y el de files `files/:uuid`. Podrias crear un endpoint `DELETE folders/:uuid` en el controller de folders para mantener la misma estructura de los endpoints? 

Y por fa, en ese endpoint usa UUID en vez del ID. Este metodo lo puedes dejar como estaba antes si quieres
created_at: 2025-03-18T20:06:32Z
author_association: COLLABORATOR
diff_hunk: @@ -752,6 +754,8 @@ export class SequelizeFileRepository implements FileRepository {
         removedAt: new Date(),
         status: FileStatus.DELETED,
         updatedAt: new Date(),
+        deleted: true,
+        deletedAt: new Date(),
path: src/modules/file/file.repository.ts
user: login: apsantiso
body: If you do this, you might break the background process that deletes files under the hood for other use cases. Please do not modify functionality that doesn't relate to your PR. Since you don't use `deleteFilesByUser` in your implementation, there's no need to change it.

created_at: 2025-03-20T15:29:31Z
author_association: COLLABORATOR
diff_hunk: @@ -75,8 +79,35 @@ export class FileUseCases {
     throw new Error('Method not implemented.');
   }
 
-  async deleteFilePermanently(file: File, user: User): Promise<void> {
-    throw new Error('Method not implemented.');
+  async deleteFilePermanently(
+    user: User,
+    where: Partial<File>,
+  ): Promise<{ id: number; uuid: string }> {
+    const file = await this.fileRepository.findOneBy(where);
+
+    if (!file) {
+      throw new NotFoundException(`File not found`);
+    }
+
+    if (!file.isOwnedBy(user)) {
+      throw new ForbiddenException('This file is not yours');
+    }
+
+    const { id, uuid, fileId, bucket } = file;
+    await this.network.deleteFile(user, bucket, fileId);
+    await this.shareUsecases.deleteFileShare(id, user);
+    await this.sharingUsecases.bulkRemoveSharings(
+      user,
+      [uuid],
+      SharingItemType.File,
+    );
+    await this.thumbnailUsecases.deleteBy({ fileId: id });
path: src/modules/file/file.usecase.ts
user: login: apsantiso
body: On drive-server we delete the thumbnails files from the buckets and the database, don't you need to do the same here?
created_at: 2025-03-20T16:01:29Z
author_association: COLLABORATOR
diff_hunk: @@ -75,8 +79,35 @@ export class FileUseCases {
     throw new Error('Method not implemented.');
   }
 
-  async deleteFilePermanently(file: File, user: User): Promise<void> {
-    throw new Error('Method not implemented.');
+  async deleteFilePermanently(
+    user: User,
+    where: Partial<File>,
+  ): Promise<{ id: number; uuid: string }> {
+    const file = await this.fileRepository.findOneBy(where);
+
+    if (!file) {
+      throw new NotFoundException(`File not found`);
+    }
+
+    if (!file.isOwnedBy(user)) {
+      throw new ForbiddenException('This file is not yours');
+    }
+
+    const { id, uuid, fileId, bucket } = file;
+    await this.network.deleteFile(user, bucket, fileId);
+    await this.shareUsecases.deleteFileShare(id, user);
+    await this.sharingUsecases.bulkRemoveSharings(
+      user,
+      [uuid],
+      SharingItemType.File,
+    );
+    await this.thumbnailUsecases.deleteBy({ fileId: id });
+    await this.fileRepository.destroyFile({
+      userId: user.id,
+      fileId,
+      bucket,
+    });
path: src/modules/file/file.usecase.ts
user: login: apsantiso
body: @sg-gs  Should we mimic [drive-server's approach](https://github.com/internxt/drive-server/blob/c58a8912441460e2dce444e6fb7d4cc9459758ae/src/app/services/files.js#L122C9-L122C19) (delete from network + database) or standardize by marking files as "DELETED" so the background process takes care of these too? 

Since this endpoint is mainly used for backup files, direct deletion reduces database entries we might not use anymore (as they are for backups). But shouldn't we use the same background process for consistency? Drive-server is an old implementation so I just want to confirm if we should just mimic it for this case.
created_at: 2025-03-20T16:12:01Z
author_association: COLLABORATOR
diff_hunk: @@ -752,6 +754,8 @@ export class SequelizeFileRepository implements FileRepository {
         removedAt: new Date(),
         status: FileStatus.DELETED,
         updatedAt: new Date(),
+        deleted: true,
+        deletedAt: new Date(),
path: src/modules/file/file.repository.ts
user: login: evillalba94
body: @apsantiso I changed this because I assumed that a removed file would also be deleted, since removal is the final step in the process. But I'll reverse it.
created_at: 2025-03-20T16:22:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,8 +79,35 @@ export class FileUseCases {
     throw new Error('Method not implemented.');
   }
 
-  async deleteFilePermanently(file: File, user: User): Promise<void> {
-    throw new Error('Method not implemented.');
+  async deleteFilePermanently(
+    user: User,
+    where: Partial<File>,
+  ): Promise<{ id: number; uuid: string }> {
+    const file = await this.fileRepository.findOneBy(where);
+
+    if (!file) {
+      throw new NotFoundException(`File not found`);
+    }
+
+    if (!file.isOwnedBy(user)) {
+      throw new ForbiddenException('This file is not yours');
+    }
+
+    const { id, uuid, fileId, bucket } = file;
+    await this.network.deleteFile(user, bucket, fileId);
+    await this.shareUsecases.deleteFileShare(id, user);
+    await this.sharingUsecases.bulkRemoveSharings(
+      user,
+      [uuid],
+      SharingItemType.File,
+    );
+    await this.thumbnailUsecases.deleteBy({ fileId: id });
path: src/modules/file/file.usecase.ts
user: login: evillalba94
body: @apsantiso Nice catch, you're right! Thanks, I will make the adaptation
created_at: 2025-03-20T16:27:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -75,8 +79,35 @@ export class FileUseCases {
     throw new Error('Method not implemented.');
   }
 
-  async deleteFilePermanently(file: File, user: User): Promise<void> {
-    throw new Error('Method not implemented.');
+  async deleteFilePermanently(
+    user: User,
+    where: Partial<File>,
+  ): Promise<{ id: number; uuid: string }> {
+    const file = await this.fileRepository.findOneBy(where);
+
+    if (!file) {
+      throw new NotFoundException(`File not found`);
+    }
+
+    if (!file.isOwnedBy(user)) {
+      throw new ForbiddenException('This file is not yours');
+    }
+
+    const { id, uuid, fileId, bucket } = file;
+    await this.network.deleteFile(user, bucket, fileId);
+    await this.shareUsecases.deleteFileShare(id, user);
+    await this.sharingUsecases.bulkRemoveSharings(
+      user,
+      [uuid],
+      SharingItemType.File,
+    );
+    await this.thumbnailUsecases.deleteBy({ fileId: id });
+    await this.fileRepository.destroyFile({
+      userId: user.id,
+      fileId,
+      bucket,
+    });
path: src/modules/file/file.usecase.ts
user: login: sg-gs
body: Standardize as 'DELETED' better
created_at: 2025-03-20T16:51:28Z
author_association: MEMBER

PR comments:
user: login: sg-gs
created_at: 2025-03-19T15:34:25Z
body: This PR does multiple things and is well above >500 LOCs @evillalba94. Hence, you need to split it and try to make a PR per thing. I suggest splitting it into at least two: the trigger update and the migrated endpoints. If any of them is above 500LOCs, divide it again (per endpoint, for instance) @evillalba94 
author_association: MEMBER
user: login: sonarqubecloud[bot]
created_at: 2025-03-20T22:51:03Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=internxt_drive-server-wip&pullRequest=484) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=internxt_drive-server-wip&pullRequest=484&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=internxt_drive-server-wip&pullRequest=484&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=internxt_drive-server-wip&pullRequest=484&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [87.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=internxt_drive-server-wip&pullRequest=484&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=internxt_drive-server-wip&pullRequest=484&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=internxt_drive-server-wip&pullRequest=484)


author_association: NONE