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..c60f540b9e --- /dev/null +++ b/src/app/drive/services/file.service/upload.errors.ts @@ -0,0 +1,16 @@ +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); + } +} 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..40acd43512 --- /dev/null +++ b/src/app/drive/services/file.service/uploadFile.test.ts @@ -0,0 +1,262 @@ +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'; +import { DriveFileData } from 'app/drive/types'; + +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 } as unknown as DriveFileData; + const workspaceServiceSpy = vi.spyOn(workspacesService, 'createFileEntry').mockResolvedValue(expectedResponse); + + 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 any); + + 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 any); + + 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 any); + + 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 any); + + await expect( + uploadFile( + 'user@test.com', + file, + vi.fn(), + { isTeam: false }, + { taskId: 'task-1', isPaused: false, isRetriedUpload: false }, + ), + ).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); + }); +}); diff --git a/src/app/drive/services/file.service/uploadFile.ts b/src/app/drive/services/file.service/uploadFile.ts index 3dfb92ab3b..7c9c338c93 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,10 @@ 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'; +import { BucketNotFoundError, FileIdRequiredError } from './upload.errors'; +import { isFileEmpty } from 'utils/isFileEmpty'; export interface FileUploadOptions { isTeam: boolean; @@ -20,13 +23,64 @@ export interface FileUploadOptions { isUploadedFromFolder?: boolean; } -class RetryableFileError extends Error { - constructor(public file: FileToUpload) { - super('Retryable file'); - this.name = 'RetryableFileError'; - } +interface UploadFileProps { + file: FileToUpload; + fileId?: string; + isWorkspaceUpload?: boolean; + 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) { + if (!fileId) { + throw new FileIdRequiredError(); + } + + const workspaceFileEntry: FileEntry = { + 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,13 +94,28 @@ 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 (isFileEmpty(file.content) && !isWorkspacesUpload) { + return createFileEntry({ + bucketId: bucketId, + file, + resourcesToken: resourcesToken ?? workspacesToken, + workspaceId: workspaceId, + ownerToken: workspacesToken, + }); + } if (!bucketId) { notificationsService.show({ text: 'Login again to start uploading files', type: ToastType.Warning }); localStorageService.clear(); navigationService.push(AppView.Login); - throw new Error('Bucket not found!'); + throw new BucketNotFoundError(); } const [promise, abort] = new Network(bridgeUser, bridgePass, encryptionKey).uploadFile( @@ -65,45 +134,18 @@ export async function uploadFile( options.abortCallback?.(abort?.abort); 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(), - }; + if (fileId === undefined) throw new FileIdRequiredError(); + + let response = await createFileEntry({ + bucketId: bucketId, + fileId: fileId, + file, + isWorkspaceUpload: !!isWorkspacesUpload, + resourcesToken: resourcesToken ?? workspacesToken, + workspaceId: workspaceId, + ownerToken: workspacesToken, + }); - 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, - }; - - response = await storageClient.createFileEntryByUuid(fileEntry, options.ownerUserAuthenticationData?.token); - } if (!response.thumbnails) { response = { ...response, 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/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/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/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.test.ts b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts new file mode 100644 index 0000000000..d3c0d96bf2 --- /dev/null +++ b/src/app/store/slices/storage/fileUtils/processDuplicateFiles.test.ts @@ -0,0 +1,141 @@ +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 fileName = 'document'; + const fileType = 'pff'; + const file = new File(['content'], `${fileName}.${fileType}`, { 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]).toStrictEqual({ + name: fileName, + size: file.size, + type: fileType, + parentFolderId, + content: file, + }); + + 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(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 fileName = 'test'; + const fileType = 'js'; + const file = new File(['content'], `${fileName}.${fileType}`, { 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(fileName); + expect(result.newFilesToUpload[0].type).toBe(fileType); + 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'); + }); +}); 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, 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)