From fbba632e31333fd1763fd996042185454d7cabbc Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Wed, 17 Dec 2025 09:08:14 +0100 Subject: [PATCH 01/10] fix: create file entry --- .../drive/services/file.service/uploadFile.ts | 115 ++++++++++++------ src/app/network/UploadManager.ts | 5 +- .../fileUtils/prepareFilesToUpload.test.ts | 17 +-- .../storage/fileUtils/prepareFilesToUpload.ts | 8 +- .../fileUtils/processDuplicateFiles.ts | 9 +- .../storage.thunks/uploadItemsThunk.test.ts | 21 ---- .../storage.thunks/uploadItemsThunk.ts | 20 +-- 7 files changed, 93 insertions(+), 102 deletions(-) diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 3dfb92ab3b..51ba14d2f5 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -27,6 +27,60 @@ class RetryableFileError extends Error { } } +interface UploadFileProps { + isWorkspaceUpload: boolean; + file: FileToUpload; + fileId?: string; + bucketId: string; + workspaceId?: string; + resourcesToken?: string; + ownerToken?: string; +} + +export const createFileEntry = async ({ + bucketId, + fileId, + file, + isWorkspaceUpload, + workspaceId, + resourcesToken, + ownerToken, +}: UploadFileProps) => { + const date = new Date(); + + if (isWorkspaceUpload && workspaceId) { + const workspaceFileEntry = { + name: file.name, + bucket: bucketId, + fileId: fileId, + encryptVersion: StorageTypes.EncryptionVersion.Aes03, + folderUuid: file.parentFolderId, + size: file.size, + plainName: file.name, + type: file.type, + modificationTime: date.toISOString(), + date: date.toISOString(), + }; + + return workspacesService.createFileEntry(workspaceFileEntry, workspaceId, resourcesToken); + } else { + const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); + const fileEntry: StorageTypes.FileEntryByUuid = { + fileId: fileId, + type: file.type, + size: file.size, + plainName: file.name, + bucket: bucketId, + folderUuid: file.parentFolderId, + encryptVersion: StorageTypes.EncryptionVersion.Aes03, + modificationTime: date.toISOString(), + date: date.toISOString(), + }; + + return storageClient.createFileEntryByUuid(fileEntry, ownerToken); + } +}; + export async function uploadFile( userEmail: string, file: FileToUpload, @@ -40,6 +94,22 @@ export async function uploadFile( ): Promise { const { bridgeUser, bridgePass, encryptionKey, bucketId } = options.ownerUserAuthenticationData ?? getEnvironmentConfig(options.isTeam); + const workspaceId = options?.ownerUserAuthenticationData?.workspaceId; + const workspacesToken = options?.ownerUserAuthenticationData?.workspacesToken; + const resourcesToken = options?.ownerUserAuthenticationData?.resourcesToken; + + const isWorkspacesUpload = workspaceId && workspacesToken; + + if (file.size === 0) { + return createFileEntry({ + bucketId: bucketId, + file, + isWorkspaceUpload: !!isWorkspacesUpload, + resourcesToken: resourcesToken ?? workspacesToken, + workspaceId: workspaceId, + ownerToken: workspacesToken, + }); + } if (!bucketId) { notificationsService.show({ text: 'Login again to start uploading files', type: ToastType.Warning }); @@ -67,43 +137,16 @@ export async function uploadFile( const fileId = await promise; if (fileId === undefined) throw new RetryableFileError(file); - const workspaceId = options?.ownerUserAuthenticationData?.workspaceId; - const workspacesToken = options?.ownerUserAuthenticationData?.workspacesToken; - const resourcesToken = options?.ownerUserAuthenticationData?.resourcesToken; - - const isWorkspacesUpload = workspaceId && workspacesToken; - let response; - - if (isWorkspacesUpload) { - const date = new Date(); - const workspaceFileEntry = { - name: file.name, - bucket: bucketId, - fileId: fileId, - encryptVersion: StorageTypes.EncryptionVersion.Aes03, - folderUuid: file.parentFolderId, - size: file.size, - plainName: file.name, - type: file.type, - modificationTime: date.toISOString(), - date: date.toISOString(), - }; - - response = await workspacesService.createFileEntry(workspaceFileEntry, workspaceId, resourcesToken); - } else { - const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); - const fileEntry: StorageTypes.FileEntryByUuid = { - fileId: fileId, - type: file.type, - size: file.size, - plainName: file.name, - bucket: bucketId, - folderUuid: file.parentFolderId, - encryptVersion: StorageTypes.EncryptionVersion.Aes03, - }; + let response = await createFileEntry({ + bucketId: bucketId, + fileId: fileId, + file, + isWorkspaceUpload: !!isWorkspacesUpload, + resourcesToken: resourcesToken ?? workspacesToken, + workspaceId: workspaceId, + ownerToken: workspacesToken, + }); - response = await storageClient.createFileEntryByUuid(fileEntry, options.ownerUserAuthenticationData?.token); - } if (!response.thumbnails) { response = { ...response, diff --git a/src/app/network/UploadManager.ts b/src/app/network/UploadManager.ts index 8cab3f7017..1d842dc369 100644 --- a/src/app/network/UploadManager.ts +++ b/src/app/network/UploadManager.ts @@ -98,7 +98,7 @@ class UploadManager { }, [FileSizeType.Small]: { upperBound: TWENTY_MEGABYTES - 1, - lowerBound: 1, + lowerBound: 0, concurrency: 6, }, }; @@ -135,13 +135,14 @@ class UploadManager { const upload = async () => { uploadAttempts++; - if (!existsRelatedTask) + if (!existsRelatedTask) { tasksService.updateTask({ taskId: taskId, merge: { status: TaskStatus.InProcess, }, }); + } const uploadStatus = this.uploadRepository?.getUploadState(fileData.relatedTaskId ?? taskId); const isPaused = (await uploadStatus) === TaskStatus.Paused; diff --git a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts index 111252677c..525bb44fd2 100644 --- a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts +++ b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.test.ts @@ -36,7 +36,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: [ { name: 'file1.txt', @@ -62,7 +61,6 @@ describe('prepareFilesToUpload', () => { parentFolderId, content: new File(['content'], 'file1.txt', { type: 'text/plain' }), }); - expect(result.zeroLengthFilesNumber).toBe(0); expect(checkDuplicatedFiles).toHaveBeenCalledTimes(1); expect(processDuplicateFiles).toHaveBeenCalledTimes(2); @@ -103,7 +101,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: [ { name: 'file1.txt', @@ -129,7 +126,6 @@ describe('prepareFilesToUpload', () => { parentFolderId, content: new File(['content'], 'file1.txt', { type: 'text/plain' }), }); - expect(result.zeroLengthFilesNumber).toBe(0); expect(checkDuplicatedFiles).toHaveBeenCalled(); expect(processDuplicateFiles).toHaveBeenCalled(); @@ -148,7 +144,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: largeFileBatch.slice(0, BATCH_SIZE).map((file) => ({ name: file.name, size: file.size, @@ -165,7 +160,7 @@ describe('prepareFilesToUpload', () => { }); expect(result.filesToUpload).toHaveLength(200); - expect(result.zeroLengthFilesNumber).toBe(0); + expect(checkDuplicatedFiles).toHaveBeenCalledTimes(2); expect(processDuplicateFiles).toHaveBeenCalledTimes(4); }); @@ -192,11 +187,10 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: mockProcessedFiles, }); - const { filesToUpload, zeroLengthFilesNumber } = await prepareFilesToUpload({ + const { filesToUpload } = await prepareFilesToUpload({ files: mockFiles, parentFolderId: 'parentFolderId', }); @@ -204,7 +198,6 @@ describe('prepareFilesToUpload', () => { expect(checkDuplicatedFiles).toHaveBeenCalledTimes(4); expect(processDuplicateFiles).toHaveBeenCalledTimes(8); expect(filesToUpload).toHaveLength(TOTAL_FILES); - expect(zeroLengthFilesNumber).toBe(0); }); it('should handle fileType parameter', async () => { @@ -226,7 +219,6 @@ describe('prepareFilesToUpload', () => { }); const mockProcessDuplicateFiles = vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: [ { name: 'file1.txt', @@ -275,7 +267,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: visibleFiles.map((file) => ({ name: file.name, size: file.size, @@ -312,7 +303,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: mockFilesWithHidden.map((file) => ({ name: file.name, size: file.size, @@ -347,7 +337,6 @@ describe('prepareFilesToUpload', () => { }); vi.mocked(processDuplicateFiles).mockResolvedValue({ - zeroLengthFiles: 0, newFilesToUpload: mockFilesWithHidden.map((file) => ({ name: file.name, size: file.size, @@ -381,7 +370,7 @@ describe('prepareFilesToUpload', () => { }); expect(result.filesToUpload).toHaveLength(0); - expect(result.zeroLengthFilesNumber).toBe(0); + expect(checkDuplicatedFiles).not.toHaveBeenCalled(); expect(processDuplicateFiles).not.toHaveBeenCalled(); }); diff --git a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts index 6ca3184a26..52bf87b3d1 100644 --- a/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts +++ b/src/app/store/slices/storage/fileUtils/prepareFilesToUpload.ts @@ -23,18 +23,17 @@ export const prepareFilesToUpload = async ({ fileType?: string; disableExistenceCheck?: boolean; notUploadHiddenFiles?: boolean; -}): Promise<{ filesToUpload: FileToUpload[]; zeroLengthFilesNumber: number }> => { +}): Promise<{ filesToUpload: FileToUpload[] }> => { const filteredFiles = notUploadHiddenFiles ? files.filter((file) => !isHiddenFile(file.name)) : files; let filesToUpload: FileToUpload[] = []; - let zeroLengthFilesNumber = 0; const processFiles = async ( filesBatch: File[], disableDuplicatedNamesCheckOverride: boolean, duplicatedFiles?: DriveFileData[], ) => { - const { zeroLengthFiles, newFilesToUpload } = await processDuplicateFiles({ + const { newFilesToUpload } = await processDuplicateFiles({ files: filesBatch, existingFilesToUpload: filesToUpload, fileType, @@ -43,7 +42,6 @@ export const prepareFilesToUpload = async ({ duplicatedFiles, }); filesToUpload = newFilesToUpload; - zeroLengthFilesNumber += zeroLengthFiles; }; const processFilesBatch = async (filesBatch: File[]) => { @@ -65,5 +63,5 @@ export const prepareFilesToUpload = async ({ await processFilesBatch(batch); } - return { filesToUpload, zeroLengthFilesNumber }; + return { filesToUpload }; }; diff --git a/src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts index ce1a9c2214..5d80f6df10 100644 --- a/src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts +++ b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.ts @@ -20,13 +20,10 @@ export const processDuplicateFiles = async ({ parentFolderId, disableDuplicatedNamesCheck, duplicatedFiles, -}: ProcessDuplicateFilesParams): Promise<{ newFilesToUpload: FileToUpload[]; zeroLengthFiles: number }> => { - const zeroLengthFiles = files.filter((file) => file.size === 0).length; +}: ProcessDuplicateFilesParams): Promise<{ newFilesToUpload: FileToUpload[] }> => { const newFilesToUpload: FileToUpload[] = [...existingFilesToUpload]; const processFile = async (file: File): Promise => { - if (file.size === 0) return; - const { filename, extension } = itemUtils.getFilenameAndExt(file.name); let finalFilename = filename; @@ -45,7 +42,7 @@ export const processDuplicateFiles = async ({ }); }; - await Promise.all(files.filter((file) => file.size > 0).map(processFile)); + await Promise.all(files.map(processFile)); - return { newFilesToUpload, zeroLengthFiles }; + return { newFilesToUpload }; }; diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts index 1b51fd61fc..a9a7e01dc0 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.test.ts @@ -79,7 +79,6 @@ describe('uploadItemsThunk', () => { it('should upload files successfully', async () => { (prepareFilesToUpload as Mock).mockResolvedValue({ filesToUpload: [mockFile], - zeroLengthFilesNumber: 0, }); await uploadItemsThunk({ @@ -91,28 +90,9 @@ describe('uploadItemsThunk', () => { expect(uploadFileWithManager).toHaveBeenCalled(); }); - it('should show notification for empty files', async () => { - const notificationsServiceSpy = vi.spyOn(notificationsService, 'show'); - (prepareFilesToUpload as Mock).mockResolvedValue({ - filesToUpload: [], - zeroLengthFilesNumber: 1, - }); - - await uploadItemsThunk({ - files: [mockFile], - parentFolderId: 'parent1', - })(dispatch, getState as () => RootState, {}); - - expect(notificationsServiceSpy).toHaveBeenCalledWith({ - text: 'Empty files are not supported.\n1 empty file not uploaded.', - type: ToastType.Warning, - }); - }); - it('should handle upload errors', async () => { (prepareFilesToUpload as Mock).mockResolvedValue({ filesToUpload: [mockFile], - zeroLengthFilesNumber: 0, }); const notificationsServiceSpy = vi.spyOn(notificationsService, 'show'); (uploadFileWithManager as Mock).mockRejectedValue(new Error('Upload failed')); @@ -131,7 +111,6 @@ describe('uploadItemsThunk', () => { it('should handle retry upload', async () => { (prepareFilesToUpload as Mock).mockResolvedValue({ filesToUpload: [mockFile], - zeroLengthFilesNumber: 0, }); (uploadFileWithManager as Mock).mockRejectedValueOnce(new Error('Upload failed')); const RetryChangeStatusSpy = vi.spyOn(RetryManager, 'changeStatus'); diff --git a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts index 8b61f83285..e2c97db3ee 100644 --- a/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts +++ b/src/app/store/slices/storage/storage.thunks/uploadItemsThunk.ts @@ -56,17 +56,6 @@ const DEFAULT_OPTIONS: Partial = { showErrors: true, }; -const showEmptyFilesNotification = (zeroLengthFilesNumber: number) => { - if (zeroLengthFilesNumber > 0) { - const fileText = zeroLengthFilesNumber === 1 ? 'file' : 'files'; - - notificationsService.show({ - text: `Empty files are not supported.\n${zeroLengthFilesNumber} empty ${fileText} not uploaded.`, - type: ToastType.Warning, - }); - } -}; - const isUploadAllowed = ({ state, files, @@ -146,15 +135,13 @@ export const uploadItemsThunk = createAsyncThunk ({ filecontent: file, fileType, @@ -323,7 +310,6 @@ export const uploadSharedItemsThunk = createAsyncThunk ({ filecontent: file, userEmail: user.email, From 21abbf3adf7d24fef57209f6bce5dc524c979ebe Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Wed, 17 Dec 2025 14:49:06 +0100 Subject: [PATCH 02/10] fix: disallow empty files for b2b workspaces --- package.json | 2 +- src/app/drive/services/file.service/uploadFile.ts | 11 ++++++++--- src/app/share/types/index.ts | 2 +- yarn.lock | 8 ++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 76359a6c88..52f0afe928 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "@iconscout/react-unicons": "^1.1.6", "@internxt/css-config": "1.1.0", "@internxt/lib": "1.4.1", - "@internxt/sdk": "=1.11.17", + "@internxt/sdk": "=1.11.23", "@internxt/ui": "0.1.1", "@phosphor-icons/react": "^2.1.7", "@popperjs/core": "^2.11.6", diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 51ba14d2f5..01ff9a58ab 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -1,6 +1,5 @@ import { StorageTypes } from '@internxt/sdk/dist/drive'; import { Network } from 'app/drive/services/network.service'; -import { DriveFileData } from 'app/drive/types'; import { SdkFactory } from 'app/core/factory/sdk'; import localStorageService from 'services/local-storage.service'; import navigationService from 'services/navigation.service'; @@ -11,6 +10,8 @@ import { getEnvironmentConfig } from '../network.service'; import { generateThumbnailFromFile } from '../thumbnail.service'; import { OwnerUserAuthenticationData } from 'app/network/types'; import { FileToUpload } from './types'; +import { FileEntry } from '@internxt/sdk/dist/workspaces'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; export interface FileUploadOptions { isTeam: boolean; @@ -49,7 +50,11 @@ export const createFileEntry = async ({ const date = new Date(); if (isWorkspaceUpload && workspaceId) { - const workspaceFileEntry = { + if (!fileId) { + throw new Error('File id is required for workspace upload'); + } + + const workspaceFileEntry: FileEntry = { name: file.name, bucket: bucketId, fileId: fileId, @@ -100,7 +105,7 @@ export async function uploadFile( const isWorkspacesUpload = workspaceId && workspacesToken; - if (file.size === 0) { + if (file.size === 0 && !isWorkspacesUpload) { return createFileEntry({ bucketId: bucketId, file, diff --git a/src/app/share/types/index.ts b/src/app/share/types/index.ts index 74aabd4f8e..8a912f3c1f 100644 --- a/src/app/share/types/index.ts +++ b/src/app/share/types/index.ts @@ -1,6 +1,6 @@ import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; -import { DriveFileData } from 'app/drive/types'; import { NetworkCredentials } from '../../network/download'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; export type AdvancedSharedItem = SharedFolders & SharedFiles & { diff --git a/yarn.lock b/yarn.lock index 06da92d175..920cb1ea9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1906,10 +1906,10 @@ version "1.0.2" resolved "https://codeload.github.com/internxt/prettier-config/tar.gz/9fa74e9a2805e1538b50c3809324f1c9d0f3e4f9" -"@internxt/sdk@=1.11.17": - version "1.11.17" - resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.11.17.tgz#2f5bdada5d3cbf5cfc685a21c24b5df3ff51d8c8" - integrity sha512-91iEUvZizlwX6KBEFJ3JdFiGrhMBQ9R54sTc3Pei9QtV2FYTU8nTVEPYAg39tLOGzT/kVuplYOtBxfk6wFtSDA== +"@internxt/sdk@=1.11.23": + version "1.11.23" + resolved "https://registry.yarnpkg.com/@internxt/sdk/-/sdk-1.11.23.tgz#dab99a44d9d02aa3adac5ce2cda0bd3a56331b76" + integrity sha512-fKmyBiTslgVhFY3CpQdc+7lujIHr4cIVU7RLoZ83ju4go+68/pCUqMHlMSDskHEwo8e7q6Pua23UqgejZUzrMQ== dependencies: axios "1.13.2" uuid "11.1.0" From 81c8b03461b224f2ec83995f18af6cb0808c9bcf Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Wed, 17 Dec 2025 16:36:27 +0100 Subject: [PATCH 03/10] feat: parse file size when fetching the items --- .../drive/services/downloadManager.service.ts | 3 +- .../services/item-transform.service.test.ts | 84 +++++++++++++++++++ .../drive/services/item-transform.service.ts | 9 ++ src/app/drive/services/new-storage.service.ts | 11 ++- src/services/workspace.service.test.ts | 46 ++++++++-- src/services/workspace.service.ts | 24 +++++- 6 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 src/app/drive/services/item-transform.service.test.ts diff --git a/src/app/drive/services/downloadManager.service.ts b/src/app/drive/services/downloadManager.service.ts index 2e71b5eb80..7c567e634d 100644 --- a/src/app/drive/services/downloadManager.service.ts +++ b/src/app/drive/services/downloadManager.service.ts @@ -1,6 +1,6 @@ import tasksService from 'app/tasks/services/tasks.service'; import { DownloadFilesTask, DownloadFileTask, DownloadFolderTask, TaskStatus, TaskType } from 'app/tasks/types'; -import { DriveFolderData, DriveItemData } from '../types'; +import { DriveFileData, DriveFolderData, DriveItemData } from '../types'; import { saveAs } from 'file-saver'; import { DriveItemBlobData } from 'app/database/services/database.service'; import { getDatabaseFileSourceData, updateDatabaseFileSourceData } from './database.service'; @@ -30,7 +30,6 @@ import { } from '../types/download-types'; import { downloadWorkerHandler } from './worker.service/downloadWorkerHandler'; import { isFileEmpty } from 'utils/isFileEmpty'; -import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; export type DownloadCredentials = { credentials: NetworkCredentials; diff --git a/src/app/drive/services/item-transform.service.test.ts b/src/app/drive/services/item-transform.service.test.ts new file mode 100644 index 0000000000..2af83cdb7f --- /dev/null +++ b/src/app/drive/services/item-transform.service.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from 'vitest'; +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import transformItemService from './item-transform.service'; + +const createMockFile = (overrides: Partial = {}): DriveFileData => + ({ + id: 1, + uuid: 'test-uuid', + name: 'test-file', + plainName: 'test-file', + plain_name: 'test-file', + type: 'txt', + size: 1024, + bucket: 'test-bucket', + folderId: 1, + folder_id: 1, + folderUuid: 'folder-uuid', + fileId: 'file-id', + createdAt: '2024-01-01', + created_at: '2024-01-01', + updatedAt: '2024-01-01', + deleted: false, + deletedAt: null, + encrypt_version: '1', + status: 'EXISTS', + thumbnails: [], + currentThumbnail: null, + ...overrides, + }) as DriveFileData; + +describe('Item Transform Service', () => { + describe('Map Files to Drive File Data', () => { + test('When the file size is a string, then should convert it to number', () => { + const files = [createMockFile({ size: '12345' as unknown as number })]; + + const result = transformItemService.mapFileSizeToNumber(files); + + expect(result[0].size).toBe(12345); + expect(typeof result[0].size).toBe('number'); + }); + + test('When converting the file size, then should preserve all other file properties', () => { + const originalFile = createMockFile({ + id: 42, + uuid: 'unique-uuid', + plainName: 'my-file', + type: 'pdf', + size: '5000' as unknown as number, + }); + + const result = transformItemService.mapFileSizeToNumber([originalFile]); + + expect(result[0].id).toBe(42); + expect(result[0].uuid).toBe('unique-uuid'); + expect(result[0].plainName).toBe('my-file'); + expect(result[0].type).toBe('pdf'); + expect(result[0].size).toBe(5000); + }); + + test('When the file size is already a number, then should keep it as number', () => { + const files = [createMockFile({ size: 9876 })]; + + const result = transformItemService.mapFileSizeToNumber(files); + + expect(result[0].size).toBe(9876); + expect(typeof result[0].size).toBe('number'); + }); + + test('When there are multiple files, then should handle them correctly', () => { + const files = [ + createMockFile({ size: '100' as unknown as number }), + createMockFile({ size: 200 }), + createMockFile({ size: '300' as unknown as number }), + ]; + + const result = transformItemService.mapFileSizeToNumber(files); + + expect(result[0].size).toBe(100); + expect(result[1].size).toBe(200); + expect(result[2].size).toBe(300); + result.forEach((file) => expect(typeof file.size).toBe('number')); + }); + }); +}); diff --git a/src/app/drive/services/item-transform.service.ts b/src/app/drive/services/item-transform.service.ts index ce8ed50c08..4eae881dee 100644 --- a/src/app/drive/services/item-transform.service.ts +++ b/src/app/drive/services/item-transform.service.ts @@ -1,3 +1,4 @@ +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { DriveItemData } from '../types'; const getItemPlainNameWithExtension = (item: DriveItemData) => { @@ -10,8 +11,16 @@ const getItemPlainNameWithExtension = (item: DriveItemData) => { return plainName + '.' + type; }; +const mapFileSize = (file: DriveFileData): DriveFileData => { + return { + ...file, + size: typeof file.size === 'string' ? Number(file.size) : (file.size as number), + } as DriveFileData; +}; + const transformItemService = { getItemPlainNameWithExtension, + mapFileSizeToNumber, }; export default transformItemService; diff --git a/src/app/drive/services/new-storage.service.ts b/src/app/drive/services/new-storage.service.ts index 1c3f3c5468..18a759d4f5 100644 --- a/src/app/drive/services/new-storage.service.ts +++ b/src/app/drive/services/new-storage.service.ts @@ -6,10 +6,12 @@ import { FolderAncestor, FolderMeta, FolderAncestorWorkspace, + DriveFileData, } from '@internxt/sdk/dist/drive/storage/types'; import { SdkFactory } from 'app/core/factory/sdk'; import { RequestCanceler } from '@internxt/sdk/dist/shared/http/types'; import { ItemType } from '@internxt/sdk/dist/workspaces/types'; +import transformItemService from './item-transform.service'; export async function hasUploadedFiles(): Promise<{ hasUploadedFiles: boolean }> { const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); @@ -66,13 +68,20 @@ export function getFolderContentByUuid({ workspacesToken?: string; }): [Promise, RequestCanceler] { const storageClient = SdkFactory.getNewApiInstance().createNewStorageClient(); - return storageClient.getFolderContentByUuid({ + const [responsePromise, canceler] = storageClient.getFolderContentByUuid({ folderUuid, limit, offset, trash, workspacesToken, }); + + const transformedPromise = responsePromise.then((response) => ({ + ...response, + files: transformItemService.mapFileSizeToNumber(response.files as DriveFileData[]), + })); + + return [transformedPromise, canceler]; } export function deleteFolderByUuid(folderId: string) { diff --git a/src/services/workspace.service.test.ts b/src/services/workspace.service.test.ts index 63a2fcbd37..caa797abf6 100644 --- a/src/services/workspace.service.test.ts +++ b/src/services/workspace.service.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { WorkspaceLogType } from '@internxt/sdk/dist/workspaces'; import errorService from './error.service'; -import workspacesService, * as workspaceService from './workspace.service'; +import * as workspaceService from './workspace.service'; const mockIds = { workspaceId: 'workspace-123', @@ -345,16 +345,42 @@ describe('workspace service', () => { expect(canceler).toBe(mockCanceler); }); - it.each([ - { type: 'folders', fn: 'getWorkspaceFolders', clientFn: 'getFolders' }, - { type: 'files', fn: 'getWorkspaceFiles', clientFn: 'getFiles' }, - ])('should retrieve workspace $type', ({ fn, clientFn }) => { - const mockPromise = Promise.resolve({ items: [] }); + it('should retrieve workspace folders', () => { + const mockPromise = Promise.resolve({ result: [] }); const mockCanceler = vi.fn(); - mockWorkspacesClient[clientFn].mockReturnValue([mockPromise, mockCanceler]); - const [promise] = workspaceService[fn](mockIds.workspaceId, 'folder-id', 0, 50); + mockWorkspacesClient.getFolders.mockReturnValue([mockPromise, mockCanceler]); + const [promise] = workspaceService.getWorkspaceFolders(mockIds.workspaceId, 'folder-id', 0, 50); expect(promise).toBe(mockPromise); - expect(mockWorkspacesClient[clientFn]).toHaveBeenCalledWith( + expect(mockWorkspacesClient.getFolders).toHaveBeenCalledWith( + mockIds.workspaceId, + 'folder-id', + 0, + 50, + 'plainName', + 'ASC', + ); + }); + + it('should retrieve workspace files and transform file sizes', async () => { + const mockFiles = [ + { + id: 1, + files: [ + { + size: '1024', + }, + ], + }, + ]; + const mockPromise = Promise.resolve({ result: mockFiles }); + const mockCanceler = vi.fn(); + mockWorkspacesClient.getFiles.mockReturnValue([mockPromise, mockCanceler]); + + const [promise, canceler] = workspaceService.getWorkspaceFiles(mockIds.workspaceId, 'folder-id', 0, 50); + const result = await promise; + + expect(canceler).toBe(mockCanceler); + expect(mockWorkspacesClient.getFiles).toHaveBeenCalledWith( mockIds.workspaceId, 'folder-id', 0, @@ -362,6 +388,8 @@ describe('workspace service', () => { 'plainName', 'ASC', ); + expect(result.result[0].files).toBeDefined(); + expect(result.result[0].files[0].size).toBeTypeOf('number'); }); }); diff --git a/src/services/workspace.service.ts b/src/services/workspace.service.ts index afd60cf263..8608ff4d48 100644 --- a/src/services/workspace.service.ts +++ b/src/services/workspace.service.ts @@ -33,6 +33,7 @@ import { } from '@internxt/sdk/dist/workspaces'; import { SdkFactory } from 'app/core/factory/sdk'; import errorService from 'services/error.service'; +import transformItemService from 'app/drive/services/item-transform.service'; export function getWorkspaces(): Promise { const workspaceClient = SdkFactory.getNewApiInstance().createWorkspacesClient(); @@ -346,7 +347,9 @@ export function getWorkspaceFolders( limit: number, ): [Promise, RequestCanceler] { const workspaceClient = SdkFactory.getNewApiInstance().createWorkspacesClient(); - return workspaceClient.getFolders(workspaceId, folderId, offset, limit, 'plainName', 'ASC'); + const folders = workspaceClient.getFolders(workspaceId, folderId, offset, limit, 'plainName', 'ASC'); + + return folders; } export function getWorkspaceFiles( workspaceId: string, @@ -355,7 +358,24 @@ export function getWorkspaceFiles( limit: number, ): [Promise, RequestCanceler] { const workspaceClient = SdkFactory.getNewApiInstance().createWorkspacesClient(); - return workspaceClient.getFiles(workspaceId, folderId, offset, limit, 'plainName', 'ASC'); + const [responsePromise, requestCanceller] = workspaceClient.getFiles( + workspaceId, + folderId, + offset, + limit, + 'plainName', + 'ASC', + ); + + const transformedPromise = responsePromise.then((response) => ({ + ...response, + result: response.result.map((folder) => ({ + ...folder, + files: transformItemService.mapFileSizeToNumber(folder.files), + })), + })); + + return [transformedPromise, requestCanceller]; } export function deactivateMember(workspaceId: string, memberId: string): Promise { From cf22e9f02590af77c1bdd5f541180798c1256496 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Wed, 17 Dec 2025 16:38:33 +0100 Subject: [PATCH 04/10] fix: upload missing function --- src/app/drive/services/item-transform.service.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/drive/services/item-transform.service.ts b/src/app/drive/services/item-transform.service.ts index 4eae881dee..3a11fbfbfc 100644 --- a/src/app/drive/services/item-transform.service.ts +++ b/src/app/drive/services/item-transform.service.ts @@ -18,6 +18,10 @@ const mapFileSize = (file: DriveFileData): DriveFileData => { } as DriveFileData; }; +const mapFileSizeToNumber = (files: DriveFileData[]): DriveFileData[] => { + return files.map(mapFileSize); +}; + const transformItemService = { getItemPlainNameWithExtension, mapFileSizeToNumber, From ab32524184d3fc7dbfc0bc54186a090dc318d31f Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Thu, 18 Dec 2025 11:22:12 +0100 Subject: [PATCH 05/10] test: add coverage for upload file flow --- .../services/file.service/upload.errors.ts | 26 ++ .../services/file.service/uploadFile.test.ts | 227 ++++++++++++++++++ .../drive/services/file.service/uploadFile.ts | 15 +- .../fileUtils/processDuplicateFiles.test.ts | 135 +++++++++++ 4 files changed, 392 insertions(+), 11 deletions(-) create mode 100644 src/app/drive/services/file.service/upload.errors.ts create mode 100644 src/app/drive/services/file.service/uploadFile.test.ts create mode 100644 src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts diff --git a/src/app/drive/services/file.service/upload.errors.ts b/src/app/drive/services/file.service/upload.errors.ts new file mode 100644 index 0000000000..622fbd09bc --- /dev/null +++ b/src/app/drive/services/file.service/upload.errors.ts @@ -0,0 +1,26 @@ +import { FileToUpload } from './types'; + +export class FileIdRequiredError extends Error { + constructor() { + super('File ID is required when uploading a file'); + this.name = 'FileIdRequiredWhenUploadingError'; + + Object.setPrototypeOf(this, FileIdRequiredError.prototype); + } +} + +export class BucketNotFoundError extends Error { + constructor() { + super('Bucket not found'); + this.name = 'BucketNotFoundError'; + Object.setPrototypeOf(this, BucketNotFoundError.prototype); + } +} + +export class RetryableFileError extends Error { + constructor(public file: FileToUpload) { + super('Retryable file'); + this.name = 'RetryableFileError'; + Object.setPrototypeOf(this, RetryableFileError.prototype); + } +} diff --git a/src/app/drive/services/file.service/uploadFile.test.ts b/src/app/drive/services/file.service/uploadFile.test.ts new file mode 100644 index 0000000000..be42ecbc9d --- /dev/null +++ b/src/app/drive/services/file.service/uploadFile.test.ts @@ -0,0 +1,227 @@ +import { StorageTypes } from '@internxt/sdk/dist/drive'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { createFileEntry, uploadFile } from './uploadFile'; +import { FileToUpload } from './types'; + +vi.mock('app/core/factory/sdk', () => ({ + SdkFactory: { + getNewApiInstance: vi.fn(() => ({ + createNewStorageClient: vi.fn(() => ({ + createFileEntryByUuid: vi.fn(), + })), + })), + }, +})); + +vi.mock('app/drive/services/network.service', () => ({ + Network: vi.fn(), + getEnvironmentConfig: vi.fn(), +})); + +vi.mock('../thumbnail.service', () => ({ + generateThumbnailFromFile: vi.fn(), +})); + +vi.mock('services/local-storage.service', () => ({ + default: { + clear: vi.fn(), + }, +})); + +vi.mock('services/navigation.service', () => ({ + default: { + push: vi.fn(), + }, +})); + +import { SdkFactory } from 'app/core/factory/sdk'; +import workspacesService from 'services/workspace.service'; +import { Network, getEnvironmentConfig } from 'app/drive/services/network.service'; +import { BucketNotFoundError, FileIdRequiredError } from './upload.errors'; + +const mockSdkFactory = vi.mocked(SdkFactory); +const mockNetwork = vi.mocked(Network); +const mockGetEnvironmentConfig = vi.mocked(getEnvironmentConfig); + +describe('Create File Entry', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('When creating a file entry for a workspace, then the file entry for workspaces should be created', async () => { + const file: FileToUpload = { + name: 'test-file', + size: 1024, + type: 'pdf', + content: new File(['content'], 'test-file.pdf'), + parentFolderId: 'folder-uuid-123', + }; + const bucketId = 'bucket-123'; + const fileId = 'file-id-123'; + const workspaceId = 'workspace-456'; + const resourcesToken = 'resources-token'; + + const expectedResponse = { id: 'created-file-id', name: file.name }; + const workspaceServiceSpy = vi + .spyOn(workspacesService, 'createFileEntry') + .mockResolvedValue(expectedResponse as never); + + const result = await createFileEntry({ + bucketId, + fileId, + file, + isWorkspaceUpload: true, + workspaceId, + resourcesToken, + }); + + expect(result).toEqual(expectedResponse); + expect(workspaceServiceSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: file.name, + bucket: bucketId, + fileId: fileId, + folderUuid: file.parentFolderId, + size: file.size, + plainName: file.name, + type: file.type, + encryptVersion: StorageTypes.EncryptionVersion.Aes03, + }), + workspaceId, + resourcesToken, + ); + }); + + test('When creating a file entry for a workspace without file Id, then an error indicating so is thrown', async () => { + const file: FileToUpload = { + name: 'test-file', + size: 1024, + type: 'pdf', + content: new File(['content'], 'test-file.pdf'), + parentFolderId: 'folder-uuid-123', + }; + + await expect( + createFileEntry({ + bucketId: 'bucket-123', + fileId: undefined, + file, + isWorkspaceUpload: true, + workspaceId: 'workspace-456', + }), + ).rejects.toThrow(FileIdRequiredError); + }); + + test('When creating a file entry for personal storage, then the file entry for personal storage should be created', async () => { + const file: FileToUpload = { + name: 'personal-file', + size: 2048, + type: 'txt', + content: new File(['content'], 'personal-file.txt'), + parentFolderId: 'folder-uuid-456', + }; + const bucketId = 'personal-bucket'; + const fileId = 'personal-file-id'; + const ownerToken = 'owner-token'; + + const expectedResponse = { id: 'personal-created-id', name: file.name }; + const mockCreateFileEntryByUuid = vi.fn().mockResolvedValue(expectedResponse); + mockSdkFactory.getNewApiInstance.mockReturnValue({ + createNewStorageClient: vi.fn(() => ({ + createFileEntryByUuid: mockCreateFileEntryByUuid, + })), + } as never); + + const result = await createFileEntry({ + bucketId, + fileId, + file, + isWorkspaceUpload: false, + ownerToken, + }); + + expect(result).toEqual(expectedResponse); + expect(mockCreateFileEntryByUuid).toHaveBeenCalledWith( + expect.objectContaining({ + fileId: fileId, + type: file.type, + size: file.size, + plainName: file.name, + bucket: bucketId, + folderUuid: file.parentFolderId, + encryptVersion: StorageTypes.EncryptionVersion.Aes03, + }), + ownerToken, + ); + }); +}); + +describe('Uploading a file', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('When uploading a file with size 0 and no workspace, then it should create file entry directly without uploading', async () => { + const file: FileToUpload = { + name: 'empty-file', + size: 0, + type: 'txt', + content: new File([], 'empty-file.txt'), + parentFolderId: 'folder-123', + }; + const bucketId = 'bucket-123'; + + mockGetEnvironmentConfig.mockReturnValue({ + bridgeUser: 'user', + bridgePass: 'pass', + encryptionKey: 'key', + bucketId, + } as never); + + const expectedResponse = { id: 'empty-file-id', name: file.name, uuid: 'uuid-123', thumbnails: [] }; + const mockCreateFileEntryByUuid = vi.fn().mockResolvedValue(expectedResponse); + mockSdkFactory.getNewApiInstance.mockReturnValue({ + createNewStorageClient: vi.fn(() => ({ + createFileEntryByUuid: mockCreateFileEntryByUuid, + })), + } as never); + + const result = await uploadFile( + 'user@test.com', + file, + vi.fn(), + { isTeam: false }, + { taskId: 'task-1', isPaused: false, isRetriedUpload: false }, + ); + + expect(result).toEqual(expectedResponse); + expect(mockNetwork).not.toHaveBeenCalled(); + }); + + test('When uploading a file without bucket id, then an error indicating so is thrown', async () => { + const file: FileToUpload = { + name: 'test-file', + size: 1024, + type: 'pdf', + content: new File(['content'], 'test-file.pdf'), + parentFolderId: 'folder-123', + }; + + mockGetEnvironmentConfig.mockReturnValue({ + bridgeUser: 'user', + bridgePass: 'pass', + encryptionKey: 'key', + bucketId: undefined, + } as never); + + await expect( + uploadFile( + 'user@test.com', + file, + vi.fn(), + { isTeam: false }, + { taskId: 'task-1', isPaused: false, isRetriedUpload: false }, + ), + ).rejects.toThrow(BucketNotFoundError); + }); +}); diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 01ff9a58ab..9efd1f1f4d 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -12,6 +12,7 @@ import { OwnerUserAuthenticationData } from 'app/network/types'; import { FileToUpload } from './types'; import { FileEntry } from '@internxt/sdk/dist/workspaces'; import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { BucketNotFoundError, FileIdRequiredError, RetryableFileError } from './upload.errors'; export interface FileUploadOptions { isTeam: boolean; @@ -21,17 +22,10 @@ export interface FileUploadOptions { isUploadedFromFolder?: boolean; } -class RetryableFileError extends Error { - constructor(public file: FileToUpload) { - super('Retryable file'); - this.name = 'RetryableFileError'; - } -} - interface UploadFileProps { - isWorkspaceUpload: boolean; file: FileToUpload; fileId?: string; + isWorkspaceUpload?: boolean; bucketId: string; workspaceId?: string; resourcesToken?: string; @@ -51,7 +45,7 @@ export const createFileEntry = async ({ if (isWorkspaceUpload && workspaceId) { if (!fileId) { - throw new Error('File id is required for workspace upload'); + throw new FileIdRequiredError(); } const workspaceFileEntry: FileEntry = { @@ -109,7 +103,6 @@ export async function uploadFile( return createFileEntry({ bucketId: bucketId, file, - isWorkspaceUpload: !!isWorkspacesUpload, resourcesToken: resourcesToken ?? workspacesToken, workspaceId: workspaceId, ownerToken: workspacesToken, @@ -121,7 +114,7 @@ export async function uploadFile( localStorageService.clear(); navigationService.push(AppView.Login); - throw new Error('Bucket not found!'); + throw new BucketNotFoundError(); } const [promise, abort] = new Network(bridgeUser, bridgePass, encryptionKey).uploadFile( diff --git a/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts new file mode 100644 index 0000000000..6c044dd50e --- /dev/null +++ b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts @@ -0,0 +1,135 @@ +import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { processDuplicateFiles } from './processDuplicateFiles'; +import { getUniqueFilename } from './getUniqueFilename'; + +vi.mock('./getUniqueFilename', () => ({ + getUniqueFilename: vi.fn(), +})); + +const mockGetUniqueFilename = vi.mocked(getUniqueFilename); + +describe('Process duplicated files', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('When processing a single file without duplicates check, then it should add the file with original name', async () => { + const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + const parentFolderId = 'folder-123'; + + const result = await processDuplicateFiles({ + files: [file], + existingFilesToUpload: [], + parentFolderId, + disableDuplicatedNamesCheck: true, + }); + + expect(result.newFilesToUpload).toHaveLength(1); + expect(result.newFilesToUpload[0].name).toBe('document'); + expect(result.newFilesToUpload[0].size).toBe(file.size); + expect(result.newFilesToUpload[0].type).toBe('pdf'); + expect(result.newFilesToUpload[0].parentFolderId).toBe(parentFolderId); + expect(result.newFilesToUpload[0].content.name).toBe('document'); + expect(mockGetUniqueFilename).not.toHaveBeenCalled(); + }); + + test('When processing a file with duplicate check enabled and duplicates exist, then it should rename the file', async () => { + const file = new File(['content'], 'report.txt', { type: 'text/plain' }); + const parentFolderId = 'folder-456'; + const duplicatedFiles = [{ plainName: 'report', type: 'txt' }] as DriveFileData[]; + + mockGetUniqueFilename.mockResolvedValue('report (1)'); + + const result = await processDuplicateFiles({ + files: [file], + existingFilesToUpload: [], + parentFolderId, + disableDuplicatedNamesCheck: false, + duplicatedFiles, + }); + + expect(result.newFilesToUpload).toHaveLength(1); + expect(result.newFilesToUpload[0].name).toBe('report (1)'); + expect(result.newFilesToUpload[0].content.name).toBe('report (1)'); + expect(mockGetUniqueFilename).toHaveBeenCalledWith('report', 'txt', duplicatedFiles, parentFolderId); + }); + + test('When a file has no extension, then it should use the provided file type', async () => { + const file = new File(['content'], 'README', { type: 'text/plain' }); + const parentFolderId = 'folder-222'; + const fileType = 'txt'; + + const result = await processDuplicateFiles({ + files: [file], + existingFilesToUpload: [], + fileType, + parentFolderId, + disableDuplicatedNamesCheck: true, + }); + + expect(result.newFilesToUpload).toHaveLength(1); + expect(result.newFilesToUpload[0].name).toBe('README'); + expect(result.newFilesToUpload[0].type).toBe(fileType); + }); + + test('When there are no duplicated files and the check is disabled, then it should not get a unique filename', async () => { + const file = new File(['content'], 'test.js', { type: 'application/javascript' }); + const parentFolderId = 'folder-333'; + + const result = await processDuplicateFiles({ + files: [file], + existingFilesToUpload: [], + parentFolderId, + disableDuplicatedNamesCheck: false, + duplicatedFiles: undefined, + }); + + expect(result.newFilesToUpload).toHaveLength(1); + expect(result.newFilesToUpload[0].name).toBe('test'); + expect(result.newFilesToUpload[0].type).toBe('js'); + expect(mockGetUniqueFilename).not.toHaveBeenCalled(); + }); + + test('When processing files with duplicate check, then it should use an unique filename', async () => { + const file = new File(['content'], 'data.csv', { type: 'text/csv' }); + const uniqueName = 'data (3)'; + const parentFolderId = 'folder-555'; + const duplicatedFiles = [ + { plainName: 'data', type: 'csv' }, + { plainName: 'data (1)', type: 'csv' }, + { plainName: 'data (2)', type: 'csv' }, + ] as DriveFileData[]; + + mockGetUniqueFilename.mockResolvedValue(uniqueName); + + const result = await processDuplicateFiles({ + files: [file], + existingFilesToUpload: [], + parentFolderId, + disableDuplicatedNamesCheck: false, + duplicatedFiles, + }); + + expect(result.newFilesToUpload[0].name).toBe(uniqueName); + expect(result.newFilesToUpload[0].content.name).toBe(uniqueName); + }); + + test('When processing files with different extensions but same name, then each should be processed independently', async () => { + const fileTxt = new File(['text'], 'readme.txt', { type: 'text/plain' }); + const fileMd = new File(['markdown'], 'readme.md', { type: 'text/markdown' }); + const parentFolderId = 'folder-777'; + + const result = await processDuplicateFiles({ + files: [fileTxt, fileMd], + existingFilesToUpload: [], + parentFolderId, + disableDuplicatedNamesCheck: true, + }); + + expect(result.newFilesToUpload).toHaveLength(2); + const types = result.newFilesToUpload.map((f) => f.type); + expect(types).toContain('txt'); + expect(types).toContain('md'); + }); +}); From 64b720291105080d3fffe21926425dbadb0d8244 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Thu, 18 Dec 2025 11:46:38 +0100 Subject: [PATCH 06/10] fix: use correct type --- .../services/file.service/uploadFile.test.ts | 15 +++++------ .../fileUtils/processDuplicateFiles.test.ts | 26 ++++++++++++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/app/drive/services/file.service/uploadFile.test.ts b/src/app/drive/services/file.service/uploadFile.test.ts index be42ecbc9d..f2c9aed05f 100644 --- a/src/app/drive/services/file.service/uploadFile.test.ts +++ b/src/app/drive/services/file.service/uploadFile.test.ts @@ -38,6 +38,7 @@ import { SdkFactory } from 'app/core/factory/sdk'; import workspacesService from 'services/workspace.service'; import { Network, getEnvironmentConfig } from 'app/drive/services/network.service'; import { BucketNotFoundError, FileIdRequiredError } from './upload.errors'; +import { DriveFileData } from 'app/drive/types'; const mockSdkFactory = vi.mocked(SdkFactory); const mockNetwork = vi.mocked(Network); @@ -61,10 +62,8 @@ describe('Create File Entry', () => { const workspaceId = 'workspace-456'; const resourcesToken = 'resources-token'; - const expectedResponse = { id: 'created-file-id', name: file.name }; - const workspaceServiceSpy = vi - .spyOn(workspacesService, 'createFileEntry') - .mockResolvedValue(expectedResponse as never); + const expectedResponse = { id: 'created-file-id', name: file.name } as unknown as DriveFileData; + const workspaceServiceSpy = vi.spyOn(workspacesService, 'createFileEntry').mockResolvedValue(expectedResponse); const result = await createFileEntry({ bucketId, @@ -130,7 +129,7 @@ describe('Create File Entry', () => { createNewStorageClient: vi.fn(() => ({ createFileEntryByUuid: mockCreateFileEntryByUuid, })), - } as never); + } as any); const result = await createFileEntry({ bucketId, @@ -176,7 +175,7 @@ describe('Uploading a file', () => { bridgePass: 'pass', encryptionKey: 'key', bucketId, - } as never); + } as any); const expectedResponse = { id: 'empty-file-id', name: file.name, uuid: 'uuid-123', thumbnails: [] }; const mockCreateFileEntryByUuid = vi.fn().mockResolvedValue(expectedResponse); @@ -184,7 +183,7 @@ describe('Uploading a file', () => { createNewStorageClient: vi.fn(() => ({ createFileEntryByUuid: mockCreateFileEntryByUuid, })), - } as never); + } as any); const result = await uploadFile( 'user@test.com', @@ -212,7 +211,7 @@ describe('Uploading a file', () => { bridgePass: 'pass', encryptionKey: 'key', bucketId: undefined, - } as never); + } as any); await expect( uploadFile( diff --git a/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts index 6c044dd50e..d3c0d96bf2 100644 --- a/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts +++ b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts @@ -15,7 +15,9 @@ describe('Process duplicated files', () => { }); test('When processing a single file without duplicates check, then it should add the file with original name', async () => { - const file = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + const fileName = 'document'; + const fileType = 'pff'; + const file = new File(['content'], `${fileName}.${fileType}`, { type: 'application/pdf' }); const parentFolderId = 'folder-123'; const result = await processDuplicateFiles({ @@ -26,11 +28,14 @@ describe('Process duplicated files', () => { }); expect(result.newFilesToUpload).toHaveLength(1); - expect(result.newFilesToUpload[0].name).toBe('document'); - expect(result.newFilesToUpload[0].size).toBe(file.size); - expect(result.newFilesToUpload[0].type).toBe('pdf'); - expect(result.newFilesToUpload[0].parentFolderId).toBe(parentFolderId); - expect(result.newFilesToUpload[0].content.name).toBe('document'); + expect(result.newFilesToUpload[0]).toStrictEqual({ + name: fileName, + size: file.size, + type: fileType, + parentFolderId, + content: file, + }); + expect(mockGetUniqueFilename).not.toHaveBeenCalled(); }); @@ -51,7 +56,6 @@ describe('Process duplicated files', () => { expect(result.newFilesToUpload).toHaveLength(1); expect(result.newFilesToUpload[0].name).toBe('report (1)'); - expect(result.newFilesToUpload[0].content.name).toBe('report (1)'); expect(mockGetUniqueFilename).toHaveBeenCalledWith('report', 'txt', duplicatedFiles, parentFolderId); }); @@ -74,7 +78,9 @@ describe('Process duplicated files', () => { }); test('When there are no duplicated files and the check is disabled, then it should not get a unique filename', async () => { - const file = new File(['content'], 'test.js', { type: 'application/javascript' }); + const fileName = 'test'; + const fileType = 'js'; + const file = new File(['content'], `${fileName}.${fileType}`, { type: 'application/javascript' }); const parentFolderId = 'folder-333'; const result = await processDuplicateFiles({ @@ -86,8 +92,8 @@ describe('Process duplicated files', () => { }); expect(result.newFilesToUpload).toHaveLength(1); - expect(result.newFilesToUpload[0].name).toBe('test'); - expect(result.newFilesToUpload[0].type).toBe('js'); + expect(result.newFilesToUpload[0].name).toBe(fileName); + expect(result.newFilesToUpload[0].type).toBe(fileType); expect(mockGetUniqueFilename).not.toHaveBeenCalled(); }); From 161e89d837c05e94d1fcaa3cbc4675e04e67bda8 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Thu, 18 Dec 2025 11:51:34 +0100 Subject: [PATCH 07/10] fix: use isFileEmpty to check the size --- src/app/drive/services/file.service/uploadFile.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 9efd1f1f4d..e209c7ec06 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -13,6 +13,7 @@ import { FileToUpload } from './types'; import { FileEntry } from '@internxt/sdk/dist/workspaces'; import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; import { BucketNotFoundError, FileIdRequiredError, RetryableFileError } from './upload.errors'; +import { isFileEmpty } from 'utils/isFileEmpty'; export interface FileUploadOptions { isTeam: boolean; @@ -99,7 +100,7 @@ export async function uploadFile( const isWorkspacesUpload = workspaceId && workspacesToken; - if (file.size === 0 && !isWorkspacesUpload) { + if (isFileEmpty(file.content) && !isWorkspacesUpload) { return createFileEntry({ bucketId: bucketId, file, From 9c878667ad09ac053c72042e5c4ba8a47e122eac Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Fri, 16 Jan 2026 16:45:54 +0100 Subject: [PATCH 08/10] fix: use an error more descriptive --- src/app/drive/services/file.service/upload.errors.ts | 10 ---------- src/app/drive/services/file.service/uploadFile.ts | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/app/drive/services/file.service/upload.errors.ts b/src/app/drive/services/file.service/upload.errors.ts index 622fbd09bc..c60f540b9e 100644 --- a/src/app/drive/services/file.service/upload.errors.ts +++ b/src/app/drive/services/file.service/upload.errors.ts @@ -1,5 +1,3 @@ -import { FileToUpload } from './types'; - export class FileIdRequiredError extends Error { constructor() { super('File ID is required when uploading a file'); @@ -16,11 +14,3 @@ export class BucketNotFoundError extends Error { Object.setPrototypeOf(this, BucketNotFoundError.prototype); } } - -export class RetryableFileError extends Error { - constructor(public file: FileToUpload) { - super('Retryable file'); - this.name = 'RetryableFileError'; - Object.setPrototypeOf(this, RetryableFileError.prototype); - } -} diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index e209c7ec06..7c9c338c93 100644 --- a/src/app/drive/services/file.service/uploadFile.ts +++ b/src/app/drive/services/file.service/uploadFile.ts @@ -12,7 +12,7 @@ import { OwnerUserAuthenticationData } from 'app/network/types'; import { FileToUpload } from './types'; import { FileEntry } from '@internxt/sdk/dist/workspaces'; import { DriveFileData } from '@internxt/sdk/dist/drive/storage/types'; -import { BucketNotFoundError, FileIdRequiredError, RetryableFileError } from './upload.errors'; +import { BucketNotFoundError, FileIdRequiredError } from './upload.errors'; import { isFileEmpty } from 'utils/isFileEmpty'; export interface FileUploadOptions { @@ -134,7 +134,7 @@ export async function uploadFile( options.abortCallback?.(abort?.abort); const fileId = await promise; - if (fileId === undefined) throw new RetryableFileError(file); + if (fileId === undefined) throw new FileIdRequiredError(); let response = await createFileEntry({ bucketId: bucketId, From 7a22c3f022d8004989e117e78be754aee2d82ebe Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Fri, 16 Jan 2026 17:00:14 +0100 Subject: [PATCH 09/10] test: check that an error is thrown if no fileId is returned --- .../services/file.service/uploadFile.test.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/app/drive/services/file.service/uploadFile.test.ts b/src/app/drive/services/file.service/uploadFile.test.ts index f2c9aed05f..40acd43512 100644 --- a/src/app/drive/services/file.service/uploadFile.test.ts +++ b/src/app/drive/services/file.service/uploadFile.test.ts @@ -223,4 +223,40 @@ describe('Uploading a file', () => { ), ).rejects.toThrow(BucketNotFoundError); }); + + test('When a file has been uploaded and the file ID is undefined, then an error indicating so is thrown', async () => { + const file: FileToUpload = { + name: 'test-file', + size: 100, + type: 'txt', + content: new File(['some content'], 'test-file.txt'), + parentFolderId: 'folder-123', + }; + const bucketId = 'bucket-123'; + + mockGetEnvironmentConfig.mockReturnValue({ + bridgeUser: 'user', + bridgePass: 'pass', + encryptionKey: 'key', + bucketId, + } as any); + + const mockUploadFile = vi.fn().mockReturnValue([Promise.resolve(undefined), { abort: vi.fn() }]); + mockNetwork.mockImplementation( + () => + ({ + uploadFile: mockUploadFile, + }) as any, + ); + + await expect( + uploadFile( + 'user@test.com', + file, + vi.fn(), + { isTeam: false }, + { taskId: 'task-1', isPaused: false, isRetriedUpload: false }, + ), + ).rejects.toThrow(FileIdRequiredError); + }); }); From ff2264d1f183dca0883166134223b350eb983a60 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Tue, 20 Jan 2026 16:40:00 +0100 Subject: [PATCH 10/10] feat: show 0 bytes for empty files --- src/app/drive/services/size.service.ts | 2 +- src/views/Backups/components/BackupListItem.tsx | 2 +- src/views/Backups/components/DeviceListItem.tsx | 2 +- .../DriveExplorer/components/DriveExplorerListItem.tsx | 2 +- src/views/Shared/components/SharedListItem.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/drive/services/size.service.ts b/src/app/drive/services/size.service.ts index 73a5bd6b30..4d9032bede 100644 --- a/src/app/drive/services/size.service.ts +++ b/src/app/drive/services/size.service.ts @@ -7,7 +7,7 @@ export function bytesToString( decimals = 1, hideSizeString = false, ): string { - if (size > 0) { + if (size >= 0) { return PrettySize(size, removeSpace, useSingleChar, decimals, hideSizeString); } else { return ''; diff --git a/src/views/Backups/components/BackupListItem.tsx b/src/views/Backups/components/BackupListItem.tsx index 2a563f676b..17f1341ec3 100644 --- a/src/views/Backups/components/BackupListItem.tsx +++ b/src/views/Backups/components/BackupListItem.tsx @@ -55,7 +55,7 @@ export default function BackupListItem({ item, onItemClicked }: Readonly - {sizeService.bytesToString(item.size, false) === '' ? ( + {sizeService.bytesToString(item.size, false) === '' || item.isFolder ? ( ) : ( sizeService.bytesToString(item.size, false) diff --git a/src/views/Backups/components/DeviceListItem.tsx b/src/views/Backups/components/DeviceListItem.tsx index 5e50dd496d..d1faf9c2e8 100644 --- a/src/views/Backups/components/DeviceListItem.tsx +++ b/src/views/Backups/components/DeviceListItem.tsx @@ -61,6 +61,6 @@ interface DeviceSizeCellProps { } export function DeviceSizeCell({ device }: Readonly): JSX.Element { - const size = 'size' in device ? sizeService.bytesToString(device.size) : ''; + const size = 'size' in device && device.size > 0 ? sizeService.bytesToString(device.size) : '-'; return
{size}
; } diff --git a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx index 8ce573bc36..b0a8f27768 100644 --- a/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx +++ b/src/views/Drive/components/DriveExplorer/components/DriveExplorerListItem.tsx @@ -118,7 +118,7 @@ const DriveExplorerListItem = ({ item }: DriveExplorerItemProps): JSX.Element => {/* SIZE */}
- {sizeService.bytesToString(item.size, false) === '' ? ( + {sizeService.bytesToString(item.size, false) === '' || item.isFolder ? ( ) : ( sizeService.bytesToString(item.size, false) diff --git a/src/views/Shared/components/SharedListItem.tsx b/src/views/Shared/components/SharedListItem.tsx index 8cbbf4b079..6efd6950d6 100644 --- a/src/views/Shared/components/SharedListItem.tsx +++ b/src/views/Shared/components/SharedListItem.tsx @@ -109,7 +109,7 @@ export const SharedListItem = ({ {/* SIZE */}
- {sizeService.bytesToString(item.size, false) === '' ? ( + {sizeService.bytesToString(item.size, false) === '' || item.isFolder ? ( ) : ( sizeService.bytesToString(item.size, false)