diff --git a/src/app/core/collections.ts b/src/app/core/collections.ts index ac53c2784..76582acc7 100644 --- a/src/app/core/collections.ts +++ b/src/app/core/collections.ts @@ -1,3 +1,3 @@ export interface Iterator { - next(): Promise<{ value: T[]; done: boolean }>; + next(): Promise<{ value: T[]; done: boolean; token?: string }>; } diff --git a/src/app/drive/services/filesZip.service.test.ts b/src/app/drive/services/filesZip.service.test.ts new file mode 100644 index 000000000..609eaefd8 --- /dev/null +++ b/src/app/drive/services/filesZip.service.test.ts @@ -0,0 +1,185 @@ +import { FlatFolderZip } from '../../core/services/zip.service'; +import { addAllFilesToZip, addAllSharedFilesToZip } from './filesZip.service'; + +const mockDownloadFile = jest.fn(); + +class MockFlatFolderZip { + // zip variable public to spy with Jest + public zip: any; + private passThrough: any; + private folderName: string; + + constructor(folderName: string) { + this.folderName = folderName; + this.zip = { + addFile: jest.fn(), + addFolder: jest.fn(), + end: jest.fn(), + }; + this.passThrough = { + pipeTo: jest.fn().mockReturnValue(Promise.resolve()), + }; + } + + addFile(name: string, source: ReadableStream): void { + this.zip.addFile(name, source); + } + + addFolder(name: string): void { + this.zip.addFolder(name); + } + + async close(): Promise { + await this.zip.end(); + } +} + +describe('filesZip', () => { + const filesPage1 = [ + { name: 'file1', type: 'txt' }, + { name: 'file2', type: 'pdf' }, + { name: 'file4', type: 'pdf' }, + { name: 'file44', type: 'txt' }, + ]; + + const filesPage2 = [ + { name: 'file1', type: 'txt' }, + { name: 'file2', type: 'pdf' }, + { name: 'file4', type: 'pdf' }, + { name: 'file44', type: 'txt' }, + ]; + + const filesPage3 = [ + { name: 'file1', type: 'txt' }, + { name: 'file2', type: 'pdf' }, + { name: 'file4', type: 'pdf' }, + { name: 'file44', type: 'txt' }, + ]; + + let iterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: filesPage1, done: false }) + .mockReturnValueOnce({ value: filesPage2, done: false }) + .mockReturnValueOnce({ value: filesPage3, done: true }), + }; + + let sharedIterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: filesPage1, done: false, token: 'token' }) + .mockReturnValueOnce({ value: filesPage2, done: false, token: 'token' }) + .mockReturnValueOnce({ value: filesPage3, done: true, token: 'token' }), + }; + + const zip = new MockFlatFolderZip('folderName') as unknown as MockFlatFolderZip; + + afterEach(() => { + jest.clearAllMocks(); + iterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: filesPage1, done: false }) + .mockReturnValueOnce({ value: filesPage2, done: false }) + .mockReturnValueOnce({ value: filesPage3, done: true }), + }; + sharedIterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: filesPage1, done: false, token: 'token' }) + .mockReturnValueOnce({ value: filesPage2, done: false, token: 'token' }) + .mockReturnValueOnce({ value: filesPage3, done: true, token: 'token' }), + }; + }); + describe('addAllFilesToZip', () => { + test('should add all files to the zip correctly', async () => { + mockDownloadFile.mockResolvedValue('Mocked file stream'); + const zip = new MockFlatFolderZip('folderName'); + + const result = await addAllFilesToZip( + '/path/to/files', + mockDownloadFile, + iterator, + zip as unknown as FlatFolderZip, + ); + const addFile = jest.spyOn(zip.zip, 'addFile'); + + const allFilesLength = filesPage1.length + filesPage2.length + filesPage3.length; + const allFiles = [...filesPage1, ...filesPage2, ...filesPage3]; + expect(mockDownloadFile).toHaveBeenCalledTimes(allFilesLength); + expect(addFile).toHaveBeenCalledTimes(allFilesLength); + expect(result).toEqual(allFiles); + }); + + test('should handle empty iterator correctly', async () => { + const result = await addAllFilesToZip( + '/path/to/files', + mockDownloadFile, + { next: jest.fn().mockReturnValue({ value: [], done: true }) }, + zip as unknown as FlatFolderZip, + ); + const addFile = jest.spyOn(zip.zip, 'addFile'); + + expect(mockDownloadFile).not.toHaveBeenCalled(); + expect(addFile).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + test('should handle errors during file download', async () => { + mockDownloadFile.mockRejectedValueOnce(new Error('Download error')); + const addFile = jest.spyOn(zip.zip, 'addFile'); + + await expect( + addAllFilesToZip('/path/to/files', mockDownloadFile, iterator, zip as unknown as FlatFolderZip), + ).rejects.toThrow('Download error'); + + expect(addFile).not.toHaveBeenCalled(); + }); + }); + describe('addAllSharedFilesToZip', () => { + test('should add all shared files to the zip correctly', async () => { + mockDownloadFile.mockResolvedValue('Mocked file stream'); + const zip = new MockFlatFolderZip('folderName'); + + const result = await addAllSharedFilesToZip( + '/path/to/files', + mockDownloadFile, + sharedIterator, + zip as unknown as FlatFolderZip, + ); + const addFile = jest.spyOn(zip.zip, 'addFile'); + const allFilesLength = filesPage1.length + filesPage2.length + filesPage3.length; + const allFiles = [...filesPage1, ...filesPage2, ...filesPage3]; + expect(mockDownloadFile).toHaveBeenCalledTimes(allFilesLength); + expect(addFile).toHaveBeenCalledTimes(allFilesLength); + expect(result.files).toEqual(allFiles); + expect(result.token).toEqual('token'); + }); + + test('should handle empty shared iterator correctly', async () => { + const result = await addAllSharedFilesToZip( + '/path/to/files', + mockDownloadFile, + { next: jest.fn().mockReturnValue({ value: [], done: true, token: 'token' }) }, + zip as unknown as FlatFolderZip, + ); + const addFile = jest.spyOn(zip.zip, 'addFile'); + + expect(mockDownloadFile).not.toHaveBeenCalled(); + expect(addFile).not.toHaveBeenCalled(); + expect(result.files).toEqual([]); + expect(result.token).toEqual('token'); + }); + + test('should handle errors during shared file download', async () => { + mockDownloadFile.mockRejectedValueOnce(new Error('Download error')); + const addFile = jest.spyOn(zip.zip, 'addFile'); + + await expect( + addAllSharedFilesToZip('/path/to/files', mockDownloadFile, sharedIterator, zip as unknown as FlatFolderZip), + ).rejects.toThrow('Download error'); + + expect(addFile).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/drive/services/filesZip.service.ts b/src/app/drive/services/filesZip.service.ts new file mode 100644 index 000000000..d2fb19574 --- /dev/null +++ b/src/app/drive/services/filesZip.service.ts @@ -0,0 +1,58 @@ +import { SharedFiles } from '@internxt/sdk/dist/drive/share/types'; +import { Iterator } from 'app/core/collections'; +import { FlatFolderZip } from '../../core/services/zip.service'; +import { DriveFileData } from '../types'; + +type File = SharedFiles | DriveFileData; + +async function addFilesToZip( + currentAbsolutePath: string, + downloadFile: (file: T) => Promise, + iterator: Iterator, + zip: FlatFolderZip, +): Promise<{ files: T[]; token?: string }> { + const path = currentAbsolutePath; + const allFiles: T[] = []; + + const addFileToZip = async (file: T) => { + const fileStream = await downloadFile(file); + zip.addFile(path + '/' + file.name + (file.type ? '.' + file.type : ''), fileStream); + }; + + let pack; + let moreFiles = true; + while (moreFiles) { + pack = await iterator.next(); + + const files = pack.value; + moreFiles = !pack.done; + allFiles.push(...files); + + for (const file of files) { + await addFileToZip(file); + } + } + return { files: allFiles, token: pack?.token ?? '' }; +} + +async function addAllFilesToZip( + currentAbsolutePath: string, + downloadFile: (file: DriveFileData) => Promise, + iterator: Iterator, + zip: FlatFolderZip, +): Promise { + const { files } = await addFilesToZip(currentAbsolutePath, downloadFile, iterator, zip); + return files; +} + +async function addAllSharedFilesToZip( + currentAbsolutePath: string, + downloadFile: (file: SharedFiles) => Promise, + iterator: Iterator, + zip: FlatFolderZip, +): Promise<{ files: SharedFiles[]; token: string }> { + const { files, token } = await addFilesToZip(currentAbsolutePath, downloadFile, iterator, zip); + return { files, token: token ?? '' }; +} + +export { addAllFilesToZip, addAllSharedFilesToZip }; diff --git a/src/app/drive/services/folder.service.ts b/src/app/drive/services/folder.service.ts index b4576bcc1..86b17f8d2 100644 --- a/src/app/drive/services/folder.service.ts +++ b/src/app/drive/services/folder.service.ts @@ -1,24 +1,26 @@ -import { DriveFileData, DriveFolderData, DriveFolderMetadataPayload, DriveItemData, FolderTree } from '../types'; -import errorService from '../../core/services/error.service'; import { aes } from '@internxt/lib'; -import httpService from '../../core/services/http.service'; -import { DevicePlatform } from '../../core/types'; -import analyticsService from '../../analytics/services/analytics.service'; -import localStorageService from '../../core/services/local-storage.service'; -import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { StorageTypes } from '@internxt/sdk/dist/drive'; +import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; import { RequestCanceler } from '@internxt/sdk/dist/shared/http/types'; -import { SdkFactory } from '../../core/factory/sdk'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { Iterator } from 'app/core/collections'; +import { binaryStreamToBlob } from 'app/core/services/stream.service'; import { FlatFolderZip } from 'app/core/services/zip.service'; -import { downloadFile } from 'app/network/download'; import { LRUFilesCacheManager } from 'app/database/services/database.service/LRUFilesCacheManager'; -import { updateDatabaseFileSourceData } from './database.service'; -import { binaryStreamToBlob } from 'app/core/services/stream.service'; +import { downloadFile } from 'app/network/download'; import { checkIfCachedSourceIsOlder } from 'app/store/slices/storage/storage.thunks/downloadFileThunk'; import { t } from 'i18next'; import { TrackingPlan } from '../../analytics/TrackingPlan'; -import { SharedFiles, SharedFolders } from '@internxt/sdk/dist/drive/share/types'; +import analyticsService from '../../analytics/services/analytics.service'; +import { SdkFactory } from '../../core/factory/sdk'; +import errorService from '../../core/services/error.service'; +import httpService from '../../core/services/http.service'; +import localStorageService from '../../core/services/local-storage.service'; +import { DevicePlatform } from '../../core/types'; +import { DriveFileData, DriveFolderData, DriveFolderMetadataPayload, DriveItemData, FolderTree } from '../types'; +import { updateDatabaseFileSourceData } from './database.service'; +import { addAllFilesToZip, addAllSharedFilesToZip } from './filesZip.service'; +import { addAllFoldersToZip, addAllSharedFoldersToZip } from './foldersZip.service'; export interface IFolders { bucket: string; @@ -158,8 +160,8 @@ class DirectoryFolderIterator implements Iterator { private readonly queryValues: { directoryId: number }; constructor(queryValues: { directoryId: number }, limit?: number, offset?: number) { - this.limit = limit || 5; - this.offset = offset || 0; + this.limit = limit ?? 5; + this.offset = offset ?? 0; this.queryValues = queryValues; } @@ -186,8 +188,8 @@ class DirectoryFilesIterator implements Iterator { private readonly queryValues: { directoryId: number }; constructor(queryValues: { directoryId: number }, limit?: number, offset?: number) { - this.limit = limit || 5; - this.offset = offset || 0; + this.limit = limit ?? 5; + this.offset = offset ?? 0; this.queryValues = queryValues; } @@ -218,126 +220,6 @@ interface FolderRef { folderToken?: string; } -async function addAllFilesToZip( - currentAbsolutePath: string, - downloadFile: (file: DriveFileData) => Promise, - iterator: Iterator, - zip: FlatFolderZip, -): Promise { - let pack = await iterator.next(); - let files = pack.value; - let moreFiles = !pack.done; - - const path = currentAbsolutePath; - const allFiles: DriveFileData[] = []; - - do { - const nextChunkRequest = iterator.next(); - - allFiles.push(...files); - - for (const file of files) { - const fileStream = await downloadFile(file); - await zip.addFile(path + '/' + file.name + (file.type ? '.' + file.type : ''), fileStream); - } - - pack = await nextChunkRequest; - files = pack.value; - moreFiles = !pack.done; - } while (moreFiles); - - return allFiles; -} - -async function addAllSharedFilesToZip( - currentAbsolutePath: string, - downloadFile: (file: SharedFiles) => Promise, - iterator: Iterator, - zip: FlatFolderZip, -): Promise<{ files: SharedFiles[]; token: string }> { - let pack = await iterator.next(); - let files = pack.value; - let moreFiles = !pack.done; - - const path = currentAbsolutePath; - const allFiles: SharedFiles[] = []; - - do { - const nextChunkRequest = iterator.next(); - - allFiles.push(...files); - - for (const file of files) { - const fileStream = await downloadFile(file); - await zip.addFile(path + '/' + file.name + (file.type ? '.' + file.type : ''), fileStream); - } - - pack = await nextChunkRequest; - files = pack.value; - moreFiles = !pack.done; - } while (moreFiles); - - return { files: allFiles, token: (pack as any)?.token }; -} - -export async function addAllSharedFoldersToZip( - currentAbsolutePath: string, - iterator: Iterator, - zip: FlatFolderZip, -): Promise<{ folders: SharedFolders[]; token: string }> { - let pack = await iterator.next(); - let folders = pack.value; - let moreFolders = !pack.done; - - const path = currentAbsolutePath; - const allFolders: SharedFolders[] = []; - - do { - const nextChunkRequest = iterator.next(); - - allFolders.push(...folders); - - for (const folder of folders) { - await zip.addFolder(path + '/' + folder.name); - } - - pack = await nextChunkRequest; - folders = pack.value; - moreFolders = !pack.done; - } while (moreFolders); - - return { folders: allFolders, token: (pack as any)?.token }; -} - -export async function addAllFoldersToZip( - currentAbsolutePath: string, - iterator: Iterator, - zip: FlatFolderZip, -): Promise { - let pack = await iterator.next(); - let folders = pack.value; - let moreFolders = !pack.done; - - const path = currentAbsolutePath; - const allFolders: DriveFolderData[] = []; - - do { - const nextChunkRequest = iterator.next(); - - allFolders.push(...folders); - - for (const folder of folders) { - await zip.addFolder(path + '/' + folder.name); - } - - pack = await nextChunkRequest; - folders = pack.value; - moreFolders = !pack.done; - } while (moreFolders); - - return allFolders; -} - async function downloadSharedFolderAsZip( folderId: DriveFolderData['id'], folderName: DriveFolderData['name'], @@ -680,6 +562,7 @@ const folderService = { fetchFolderTree, downloadFolderAsZip, addAllFoldersToZip, + addAllFilesToZip, downloadSharedFolderAsZip, }; diff --git a/src/app/drive/services/foldersZip.service.test.ts b/src/app/drive/services/foldersZip.service.test.ts new file mode 100644 index 000000000..84c30ba7a --- /dev/null +++ b/src/app/drive/services/foldersZip.service.test.ts @@ -0,0 +1,103 @@ +import { FlatFolderZip } from '../../core/services/zip.service'; +import { addAllFoldersToZip, addAllSharedFoldersToZip } from './foldersZip.service'; + +class MockFlatFolderZip { + public zip: any; + + constructor() { + this.zip = { + addFolder: jest.fn(), + }; + } + + addFolder(name: string): void { + this.zip.addFolder(name); + } +} + +describe('foldersZip', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('addAllSharedFoldersToZip', () => { + const foldersPage1 = [{ name: 'folder1' }, { name: 'folder2' }]; + const foldersPage2 = [{ name: 'folder3' }, { name: 'folder4' }]; + const foldersPage3 = [{ name: 'folder5' }]; + + const iterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: foldersPage1, done: false, token: 'token1' }) + .mockReturnValueOnce({ value: foldersPage2, done: false, token: 'token2' }) + .mockReturnValueOnce({ value: foldersPage3, done: true, token: 'token3' }), + }; + + test('should add all shared folders to the zip correctly', async () => { + const zip = new MockFlatFolderZip(); + + const result = await addAllSharedFoldersToZip('/path/to/folders', iterator, zip as unknown as FlatFolderZip); + const addFolder = jest.spyOn(zip.zip, 'addFolder'); + const allFoldersLength = foldersPage1.length + foldersPage2.length + foldersPage3.length; + + expect(addFolder).toHaveBeenCalledTimes(allFoldersLength); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder1'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder2'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder3'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder4'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder5'); + expect(result.folders).toEqual([...foldersPage1, ...foldersPage2, ...foldersPage3]); + expect(result.token).toEqual('token3'); + }); + + test('should handle empty iterator correctly', async () => { + const emptyIterator = { next: jest.fn().mockReturnValue({ value: [], done: true, token: 'token' }) }; + const zip = new MockFlatFolderZip(); + const addFolder = jest.spyOn(zip.zip, 'addFolder'); + + const result = await addAllSharedFoldersToZip('/path/to/folders', emptyIterator, zip as unknown as FlatFolderZip); + + expect(addFolder).not.toHaveBeenCalled(); + expect(result.folders).toEqual([]); + expect(result.token).toEqual('token'); + }); + }); + + describe('addAllFoldersToZip', () => { + const foldersPage1 = [{ name: 'folder1' }, { name: 'folder2' }]; + const foldersPage2 = [{ name: 'folder3' }, { name: 'folder4' }]; + const foldersPage3 = []; + + const iterator = { + next: jest + .fn() + .mockReturnValueOnce({ value: foldersPage1, done: false }) + .mockReturnValueOnce({ value: foldersPage2, done: false }) + .mockReturnValueOnce({ value: foldersPage3, done: true }), + }; + const zip = new MockFlatFolderZip(); + + test('should add all folders to the zip correctly', async () => { + const result = await addAllFoldersToZip('/path/to/folders', iterator, zip as unknown as FlatFolderZip); + const addFolder = jest.spyOn(zip.zip, 'addFolder'); + const allFoldersLength = foldersPage1.length + foldersPage2.length + foldersPage3.length; + + expect(addFolder).toHaveBeenCalledTimes(allFoldersLength); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder1'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder2'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder3'); + expect(addFolder).toHaveBeenCalledWith('/path/to/folders/folder4'); + expect(result).toEqual([...foldersPage1, ...foldersPage2, ...foldersPage3]); + }); + + test('should handle empty iterator correctly', async () => { + const emptyIterator = { next: jest.fn().mockReturnValue({ value: [], done: true }) }; + const addFolder = jest.spyOn(zip.zip, 'addFolder'); + + const result = await addAllFoldersToZip('/path/to/folders', emptyIterator, zip as unknown as FlatFolderZip); + + expect(addFolder).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + }); +}); diff --git a/src/app/drive/services/foldersZip.service.ts b/src/app/drive/services/foldersZip.service.ts new file mode 100644 index 000000000..4d6fdd0c1 --- /dev/null +++ b/src/app/drive/services/foldersZip.service.ts @@ -0,0 +1,62 @@ +import { SharedFolders } from '@internxt/sdk/dist/drive/share/types'; +import { Iterator } from 'app/core/collections'; +import { FlatFolderZip } from '../../core/services/zip.service'; +import { DriveFolderData } from '../types'; + +async function addAllFoldersToZip( + currentAbsolutePath: string, + iterator: Iterator, + zip: FlatFolderZip, +): Promise { + const path = currentAbsolutePath; + const allFolders: DriveFolderData[] = []; + + const addFolderToZip = (folder: DriveFolderData) => { + zip.addFolder(path + '/' + folder.name); + }; + + let pack; + let moreFolders = true; + + while (moreFolders) { + pack = await iterator.next(); + const folders = pack.value; + moreFolders = !pack.done; + allFolders.push(...folders); + + for (const folder of folders) { + addFolderToZip(folder); + } + } + + return allFolders; +} + +async function addAllSharedFoldersToZip( + currentAbsolutePath: string, + iterator: Iterator, + zip: FlatFolderZip, +): Promise<{ folders: SharedFolders[]; token: string }> { + const path = currentAbsolutePath; + const allFolders: SharedFolders[] = []; + const addFolderToZip = (folder: DriveFolderData) => { + zip.addFolder(path + '/' + folder.name); + }; + let pack; + let moreFolders = true; + + while (moreFolders) { + pack = await iterator.next(); + const folders = pack.value; + moreFolders = !pack.done; + allFolders.push(...folders); + + for (const folder of folders) { + addFolderToZip(folder); + } + } + + return { folders: allFolders, token: pack.token ?? '' }; +} + +export { addAllFoldersToZip, addAllSharedFoldersToZip };