diff --git a/package.json b/package.json index 78aa18f79..46790b53e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "internxt-drive", - "version": "2.0.10", + "version": "2.1.0", "author": "Internxt ", "description": "Internxt Drive client UI", "license": "AGPL-3.0", @@ -95,8 +95,7 @@ "win": { "target": [ "nsis" - ], - "certificateSubjectName": "Internxt Universal Technologies SL" + ] }, "linux": { "target": [ diff --git a/release/app/package.json b/release/app/package.json index 788b83eac..33bb5678a 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "internxt-drive", - "version": "2.0.10", + "version": "2.1.0", "description": "Internxt Drive client UI", "main": "./dist/main/main.js", "author": "Internxt ", diff --git a/src/apps/main/analytics/drive-handlers.ts b/src/apps/main/analytics/drive-handlers.ts index f0ed61122..e681d42a9 100644 --- a/src/apps/main/analytics/drive-handlers.ts +++ b/src/apps/main/analytics/drive-handlers.ts @@ -35,6 +35,16 @@ ipcMainDrive.on('FILE_DOWNLOADED', (_, payload) => { }); }); +ipcMainDrive.on('FILE_DOWNLOAD_CANCEL', (_, payload) => { + const { name, extension, size } = payload; + + trackEvent('Download Aborted', { + file_name: name, + file_extension: extension, + file_size: size, + }); +}); + ipcMainDrive.on('FILE_CLONNED', (_, payload) => { const { name, extension, size, processInfo } = payload; diff --git a/src/apps/main/auth/refresh-token.ts b/src/apps/main/auth/refresh-token.ts index bc4632857..50a94a251 100644 --- a/src/apps/main/auth/refresh-token.ts +++ b/src/apps/main/auth/refresh-token.ts @@ -12,8 +12,9 @@ const authorizedClient = getClient(); async function obtainTokens() { try { + Logger.debug('[TOKEN] Obtaining new tokens'); const res = await authorizedClient.get( - `${process.env.API_URL}/api/user/refresh` + `${process.env.API_URL}/user/refresh` ); return res.data; diff --git a/src/apps/main/background-processes/sync-engine.ts b/src/apps/main/background-processes/sync-engine.ts index 82548666e..319be48d1 100644 --- a/src/apps/main/background-processes/sync-engine.ts +++ b/src/apps/main/background-processes/sync-engine.ts @@ -31,7 +31,7 @@ async function healthCheck() { resolve(); }); - const millisecondsToWait = 8_000; + const millisecondsToWait = 5_000; setTimeout(() => { reject( @@ -55,7 +55,7 @@ function scheduleHeathCheck() { const relaunchOnFail = () => healthCheck() .then(() => { - // Logger.debug('Health check succeeded'); + Logger.debug('Health check succeeded'); }) .catch(() => { const warning = 'Health check failed, relaunching the worker'; @@ -71,7 +71,7 @@ function scheduleHeathCheck() { spawnSyncEngineWorker(); }); - healthCheckSchedule = nodeSchedule.scheduleJob('*/30 * * * * *', async () => { + healthCheckSchedule = nodeSchedule.scheduleJob('*/20 * * * * *', async () => { const workerIsPending = checkSyncEngineInProcess(15_000); Logger.debug( 'Health check', @@ -166,7 +166,7 @@ export async function stopSyncEngineWatcher() { }); try { - worker?.webContents.send('STOP_SYNC_ENGINE_PROCESS'); + worker?.webContents?.send('STOP_SYNC_ENGINE_PROCESS'); await stopPromise; } catch (err) { @@ -219,7 +219,7 @@ async function stopAndClearSyncEngineWatcher() { }); try { - worker?.webContents.send('STOP_AND_CLEAR_SYNC_ENGINE_PROCESS'); + worker?.webContents?.send('STOP_AND_CLEAR_SYNC_ENGINE_PROCESS'); await response; } catch (err) { @@ -235,8 +235,13 @@ async function stopAndClearSyncEngineWatcher() { export function updateSyncEngine() { try { - if (worker?.webContents && !worker?.isDestroyed()) { - worker?.webContents.send('UPDATE_SYNC_ENGINE_PROCESS'); + if ( + worker && + !worker.isDestroyed() && + worker.webContents && + !worker.webContents.isDestroyed() + ) { + worker.webContents?.send('UPDATE_SYNC_ENGINE_PROCESS'); } } catch (err) { // TODO: handle error @@ -247,8 +252,13 @@ export function updateSyncEngine() { export function fallbackSyncEngine() { try { - if (worker?.webContents && !worker?.isDestroyed()) { - worker?.webContents.send('FALLBACK_SYNC_ENGINE_PROCESS'); + if ( + worker && + !worker.isDestroyed() && + worker.webContents && + !worker.webContents.isDestroyed() + ) { + worker?.webContents?.send('FALLBACK_SYNC_ENGINE_PROCESS'); } } catch (err) { Logger.error(err); @@ -256,8 +266,13 @@ export function fallbackSyncEngine() { } export async function sendUpdateFilesInSyncPending(): Promise { try { - if (worker?.webContents && !worker?.isDestroyed()) { - worker?.webContents.send('UPDATE_UNSYNC_FILE_IN_SYNC_ENGINE_PROCESS'); + if ( + worker && + !worker.isDestroyed() && + worker.webContents && + !worker.webContents.isDestroyed() + ) { + worker?.webContents?.send('UPDATE_UNSYNC_FILE_IN_SYNC_ENGINE_PROCESS'); } return []; } catch (err) { diff --git a/src/apps/main/device/service.ts b/src/apps/main/device/service.ts index 0cbf41836..8780b8b37 100644 --- a/src/apps/main/device/service.ts +++ b/src/apps/main/device/service.ts @@ -35,7 +35,7 @@ export const addUnknownDeviceIssue = (error: Error) => { }; function createDevice(deviceName: string) { - return fetch(`${process.env.API_URL}/api/backup/deviceAsFolder`, { + return fetch(`${process.env.API_URL}/backup/deviceAsFolder`, { method: 'POST', headers: getHeaders(true), body: JSON.stringify({ deviceName }), @@ -76,7 +76,7 @@ export async function getOrCreateDevice() { if (deviceIsDefined) { const res = await fetch( - `${process.env.API_URL}/api/backup/deviceAsFolder/${savedDeviceId}`, + `${process.env.API_URL}/backup/deviceAsFolder/${savedDeviceId}`, { method: 'GET', headers: getHeaders(), @@ -110,7 +110,7 @@ export async function renameDevice(deviceName: string): Promise { const deviceId = getDeviceId(); const res = await fetch( - `${process.env.API_URL}/api/backup/deviceAsFolder/${deviceId}`, + `${process.env.API_URL}/backup/deviceAsFolder/${deviceId}`, { method: 'PATCH', headers: getHeaders(true), @@ -221,7 +221,7 @@ export async function addBackup(): Promise { async function fetchFolder(folderId: number) { const res = await fetch( - `${process.env.API_URL}/api/storage/v2/folder/${folderId}`, + `${process.env.API_URL}/storage/v2/folder/${folderId}`, { method: 'GET', headers: getHeaders(true), @@ -236,7 +236,7 @@ async function fetchFolder(folderId: number) { export async function deleteBackup(backup: Backup): Promise { const res = await fetch( - `${process.env.API_URL}/api/storage/folder/${backup.id}`, + `${process.env.API_URL}/storage/folder/${backup.id}`, { method: 'DELETE', headers: getHeaders(true), @@ -286,7 +286,7 @@ export async function changeBackupPath(currentPath: string): Promise { } const res = await fetch( - `${process.env.API_URL}/api/storage/folder/${existingBackup.folderId}/meta`, + `${process.env.API_URL}/storage/folder/${existingBackup.folderId}/meta`, { method: 'POST', headers: getHeaders(true), diff --git a/src/apps/main/fordwardToWindows.ts b/src/apps/main/fordwardToWindows.ts index 1a898c7d1..82019fc37 100644 --- a/src/apps/main/fordwardToWindows.ts +++ b/src/apps/main/fordwardToWindows.ts @@ -23,6 +23,14 @@ ipcMainDrive.on('FILE_DOWNLOADING', (_, payload) => { }); }); +ipcMainDrive.on('SYNCING', () => { + setIsProcessing(true); +}); + +ipcMainDrive.on('SYNCED', () => { + setIsProcessing(false); +}); + ipcMainDrive.on('FILE_PREPARING', (_, payload) => { const { nameWithExtension, processInfo } = payload; setIsProcessing(true); @@ -34,12 +42,21 @@ ipcMainDrive.on('FILE_PREPARING', (_, payload) => { }); ipcMainDrive.on('FILE_DOWNLOADED', (_, payload) => { + setIsProcessing(false); const { nameWithExtension } = payload; broadcastToWindows('sync-info-update', { action: 'DOWNLOADED', name: nameWithExtension, }); }); +ipcMainDrive.on('FILE_DOWNLOAD_CANCEL', (_, payload) => { + setIsProcessing(false); + const { nameWithExtension } = payload; + broadcastToWindows('sync-info-update', { + action: 'DOWNLOAD_CANCEL', + name: nameWithExtension, + }); +}); ipcMainDrive.on('FILE_MOVED', (_, payload) => { const { nameWithExtension } = payload; @@ -97,6 +114,15 @@ ipcMainDrive.on('FILE_UPLOADING', (_, payload) => { }); }); +ipcMainDrive.on('FILE_UPLOADED', (_, payload) => { + const { nameWithExtension } = payload; + setIsProcessing(false); + broadcastToWindows('sync-info-update', { + action: 'UPLOADED', + name: nameWithExtension, + }); +}); + ipcMainDrive.on('FILE_CREATED', (_, payload) => { const { nameWithExtension } = payload; setIsProcessing(false); diff --git a/src/apps/main/remote-sync/RemoteSyncManager.ts b/src/apps/main/remote-sync/RemoteSyncManager.ts index 33c0d9808..797987e23 100644 --- a/src/apps/main/remote-sync/RemoteSyncManager.ts +++ b/src/apps/main/remote-sync/RemoteSyncManager.ts @@ -4,8 +4,7 @@ import { RemoteSyncedFolder, RemoteSyncedFile, SyncConfig, - SYNC_OFFSET_MS, - SIX_HOURS_IN_MILLISECONDS, + FIVETEEN_MINUTES_IN_MILLISECONDS, rewind, WAITING_AFTER_SYNCING_DEFAULT, } from './helpers'; @@ -109,15 +108,18 @@ export class RemoteSyncManager { * * Throws an error if there's a sync in progress for this class instance */ - async startRemoteSync() { + async startRemoteSync(folderId?: number) { // const start = Date.now(); Logger.info('Starting remote to local sync'); Logger.info('Checking if we are in a valid state to start the sync'); const testPassed = this.smokeTest(); - if (!testPassed) { - return; + if (!testPassed && !folderId) { + return { + files: [], + folders: [], + }; } this.totalFilesSynced = 0; this.totalFilesUnsynced = []; @@ -128,20 +130,24 @@ export class RemoteSyncManager { Logger.info('Starting RemoteSyncManager'); this.changeStatus('SYNCING'); try { - await Promise.all([ - this.config.syncFiles - ? this.syncRemoteFiles({ - retry: 1, - maxRetries: 3, - }) - : Promise.resolve(), - this.config.syncFolders - ? this.syncRemoteFolders({ - retry: 1, - maxRetries: 3, - }) - : Promise.resolve(), + const syncOptions = { + retry: 1, + maxRetries: 3, + }; + + const syncFilesPromise = folderId + ? this.syncRemoteFilesByFolder(syncOptions, folderId) + : this.syncRemoteFiles(syncOptions); + + const syncFoldersPromise = folderId + ? this.syncRemoteFoldersByFolder(syncOptions, folderId) + : this.syncRemoteFolders(syncOptions); + + const [_files, folders] = await Promise.all([ + await syncFilesPromise, + await syncFoldersPromise, ]); + return { files: _files, folders }; } catch (error) { this.changeStatus('SYNC_FAILED'); reportError(error as Error); @@ -150,6 +156,10 @@ export class RemoteSyncManager { Logger.info('Total unsynced files: ', this.totalFilesUnsynced); Logger.info('Total synced folders: ', this.totalFoldersSynced); } + return { + files: [], + folders: [], + }; } /** @@ -168,11 +178,7 @@ export class RemoteSyncManager { } set isProcessRunning(value: boolean) { - if (value) { - this.changeStatus('SYNCING'); - } else { - this.checkRemoteSyncStatus(); - } + this.changeStatus(value ? 'SYNCING' : 'SYNCED'); this._isProcessRunning = value; } @@ -237,7 +243,7 @@ export class RemoteSyncManager { } } - private async getFileCheckpoint(): Promise> { + async getFileCheckpoint(): Promise> { const { success, result } = await this.db.files.getLastUpdated(); if (!success) return undefined; @@ -246,7 +252,7 @@ export class RemoteSyncManager { const updatedAt = new Date(result.updatedAt); - return rewind(updatedAt, SIX_HOURS_IN_MILLISECONDS); + return rewind(updatedAt, FIVETEEN_MINUTES_IN_MILLISECONDS); } /** @@ -254,42 +260,117 @@ export class RemoteSyncManager { * @param syncConfig Config to execute the sync with * @returns */ - private async syncRemoteFiles(syncConfig: SyncConfig, from?: Date) { + + private async syncRemoteFiles( + syncConfig: SyncConfig, + from?: Date + ): Promise { const fileCheckpoint = from ?? (await this.getFileCheckpoint()); + let offset = 0; + let hasMore = true; + const allResults: RemoteSyncedFile[] = []; + try { Logger.info( `Syncing files updated from ${ fileCheckpoint ?? '(no last date provided)' }` ); - const { hasMore, result } = await this.fetchFilesFromRemote( - fileCheckpoint - ); - let lastFileSynced = null; + while (hasMore) { + const { hasMore: newHasMore, result } = await this.fetchFilesFromRemote( + fileCheckpoint, + offset + ); + + for (const remoteFile of result) { + // eslint-disable-next-line no-await-in-loop + await this.createOrUpdateSyncedFileEntry(remoteFile); + this.totalFilesSynced++; + } + + allResults.push(...result); + hasMore = newHasMore; + offset += this.config.fetchFilesLimitPerRequest; + + if (hasMore) { + Logger.info('Retrieving more files for sync'); + } + } - for (const remoteFile of result) { - // eslint-disable-next-line no-await-in-loop - await this.createOrUpdateSyncedFileEntry(remoteFile); + Logger.info('Remote files sync finished'); + return allResults; + } catch (error) { + Logger.error('Remote files sync failed with error: ', error); - this.totalFilesSynced++; - lastFileSynced = remoteFile; - } + reportError(error as Error, { + lastFilesSyncAt: fileCheckpoint + ? fileCheckpoint.toISOString() + : 'INITIAL_FILES_SYNC', + }); - if (!hasMore) { - Logger.info('Remote files sync finished'); - this.filesSyncStatus = 'SYNCED'; + if (syncConfig.retry >= syncConfig.maxRetries) { + // No more retries allowed, + this.filesSyncStatus = 'SYNC_FAILED'; this.checkRemoteSyncStatus(); return; } - Logger.info('Retrieving more files for sync'); - await this.syncRemoteFiles( + + return await this.syncRemoteFiles( { - retry: 1, + retry: syncConfig.retry + 1, maxRetries: syncConfig.maxRetries, }, - lastFileSynced ? new Date(lastFileSynced.updatedAt) : undefined + from + ); + } + } + + private async syncRemoteFilesByFolder( + syncConfig: SyncConfig, + folderId: number, + from?: Date + ): Promise { + const fileCheckpoint = from ?? (await this.getFileCheckpoint()); + let offset = 0; + let hasMore = true; + const allResults: RemoteSyncedFile[] = []; + + try { + Logger.info( + `Syncing files updated from ${ + fileCheckpoint + ? fileCheckpoint.toISOString() + : '(no last date provided)' + }` ); + + while (hasMore) { + const { hasMore: newHasMore, result } = + await this.fetchFilesByFolderFromRemote( + folderId, + fileCheckpoint, + offset + ); + + for (const remoteFile of result) { + // eslint-disable-next-line no-await-in-loop + await this.createOrUpdateSyncedFileEntry(remoteFile); + this.totalFilesSynced++; + } + + allResults.push(...result); + hasMore = newHasMore; + offset += this.config.fetchFilesLimitPerRequest; + + if (hasMore) { + Logger.info('Retrieving more files for sync'); + } + } + + Logger.info('Remote files sync finished'); + + return allResults; } catch (error) { Logger.error('Remote files sync failed with error: ', error); @@ -298,6 +379,7 @@ export class RemoteSyncManager { ? fileCheckpoint.toISOString() : 'INITIAL_FILES_SYNC', }); + if (syncConfig.retry >= syncConfig.maxRetries) { // No more retries allowed, this.filesSyncStatus = 'SYNC_FAILED'; @@ -305,10 +387,14 @@ export class RemoteSyncManager { return; } - await this.syncRemoteFiles({ - retry: syncConfig.retry + 1, - maxRetries: syncConfig.maxRetries, - }); + return await this.syncRemoteFilesByFolder( + { + retry: syncConfig.retry + 1, + maxRetries: syncConfig.maxRetries, + }, + folderId, + from + ); } } @@ -321,50 +407,113 @@ export class RemoteSyncManager { const updatedAt = new Date(result.updatedAt); - return rewind(updatedAt, SIX_HOURS_IN_MILLISECONDS); + return rewind(updatedAt, FIVETEEN_MINUTES_IN_MILLISECONDS); } - /** - * Syncs all the remote folders and saves them into the local db - * @param syncConfig Config to execute the sync with - * @returns - */ - private async syncRemoteFolders(syncConfig: SyncConfig, from?: Date) { + private async syncRemoteFolders( + syncConfig: SyncConfig, + from?: Date + ): Promise { const lastFolderSyncAt = from ?? (await this.getLastFolderSyncAt()); + let offset = 0; + let hasMore = true; + const allResults: RemoteSyncedFolder[] = []; + try { Logger.info( `Syncing folders updated from ${ - lastFolderSyncAt ?? '(no last date provided)' + lastFolderSyncAt + ? lastFolderSyncAt.toISOString() + : '(no last date provided)' }` ); - const { hasMore, result } = await this.fetchFoldersFromRemote( - lastFolderSyncAt - ); - let lastFolderSynced = null; + while (hasMore) { + const { hasMore: newHasMore, result } = + await this.fetchFoldersFromRemote(lastFolderSyncAt, offset); - for (const remoteFolder of result) { - // eslint-disable-next-line no-await-in-loop - await this.createOrUpdateSyncedFolderEntry(remoteFolder); + for (const remoteFolder of result) { + // eslint-disable-next-line no-await-in-loop + await this.createOrUpdateSyncedFolderEntry(remoteFolder); + this.totalFoldersSynced++; + } - this.totalFoldersSynced++; - lastFolderSynced = remoteFolder; + allResults.push(...result); + hasMore = newHasMore; + offset += this.config.fetchFilesLimitPerRequest; + + if (hasMore) { + Logger.info('Retrieving more folders for sync'); + } } + return allResults; + } catch (error) { + Logger.error('Remote folders sync failed with error: ', error); + reportError(error as Error, { + lastFoldersSyncAt: lastFolderSyncAt + ? lastFolderSyncAt.toISOString() + : 'INITIAL_FOLDERS_SYNC', + }); - if (!hasMore) { - this.foldersSyncStatus = 'SYNCED'; + if (syncConfig.retry >= syncConfig.maxRetries) { + // No more retries allowed, + this.foldersSyncStatus = 'SYNC_FAILED'; this.checkRemoteSyncStatus(); return; } - Logger.info('Retrieving more folders for sync'); - await this.syncRemoteFolders( + return await this.syncRemoteFolders( { - retry: 1, + retry: syncConfig.retry + 1, maxRetries: syncConfig.maxRetries, }, - lastFolderSynced ? new Date(lastFolderSynced.updatedAt) : undefined + from + ); + } + } + + private async syncRemoteFoldersByFolder( + syncConfig: SyncConfig, + folderId: number, + from?: Date + ): Promise { + const lastFolderSyncAt = from ?? (await this.getLastFolderSyncAt()); + let offset = 0; + let hasMore = true; + const allResults: RemoteSyncedFolder[] = []; + + try { + Logger.info( + `Syncing folders updated from ${ + lastFolderSyncAt + ? lastFolderSyncAt.toISOString() + : '(no last date provided)' + }` ); + + while (hasMore) { + const { hasMore: newHasMore, result } = + await this.fetchFoldersByFolderFromRemote( + folderId, + lastFolderSyncAt, + offset + ); + + for (const remoteFolder of result) { + // eslint-disable-next-line no-await-in-loop + await this.createOrUpdateSyncedFolderEntry(remoteFolder); + this.totalFoldersSynced++; + } + + allResults.push(...result); + hasMore = newHasMore; + offset += this.config.fetchFilesLimitPerRequest; + + if (hasMore) { + Logger.info('Retrieving more folders for sync'); + } + } + return allResults; } catch (error) { Logger.error('Remote folders sync failed with error: ', error); reportError(error as Error, { @@ -372,6 +521,7 @@ export class RemoteSyncManager { ? lastFolderSyncAt.toISOString() : 'INITIAL_FOLDERS_SYNC', }); + if (syncConfig.retry >= syncConfig.maxRetries) { // No more retries allowed, this.foldersSyncStatus = 'SYNC_FAILED'; @@ -379,10 +529,14 @@ export class RemoteSyncManager { return; } - await this.syncRemoteFolders({ - retry: syncConfig.retry + 1, - maxRetries: syncConfig.maxRetries, - }); + return await this.syncRemoteFoldersByFolder( + { + retry: syncConfig.retry + 1, + maxRetries: syncConfig.maxRetries, + }, + folderId, + from + ); } } @@ -391,17 +545,18 @@ export class RemoteSyncManager { * * @param updatedAtCheckpoint Retrieve files that were updated after this date */ - private async fetchFilesFromRemote(updatedAtCheckpoint?: Date): Promise<{ + private async fetchFilesFromRemote( + updatedAtCheckpoint?: Date, + offset = 0 + ): Promise<{ hasMore: boolean; result: RemoteSyncedFile[]; }> { const params = { limit: this.config.fetchFilesLimitPerRequest, - offset: 0, + offset, status: 'ALL', - updatedAt: updatedAtCheckpoint - ? updatedAtCheckpoint.toISOString() - : undefined, + updatedAt: updatedAtCheckpoint ?? undefined, }; const allFilesResponse = await this.fetchItems(params, 'files'); if (allFilesResponse.status > 299) { @@ -440,19 +595,117 @@ export class RemoteSyncManager { }; } + /** + * Fetch the files that were updated after the given date + * + * @param updatedAtCheckpoint Retrieve files that were updated after this date + */ + private async fetchFilesByFolderFromRemote( + folderId: number, + updatedAtCheckpoint?: Date, + offset = 0 + ): Promise<{ + hasMore: boolean; + result: RemoteSyncedFile[]; + }> { + const params = { + limit: this.config.fetchFilesLimitPerRequest, + offset, + status: 'ALL', + updatedAt: updatedAtCheckpoint + ? updatedAtCheckpoint.toISOString() + : undefined, + }; + const allFilesResponse = await this.fetchItemsByFolder( + params, + folderId, + 'files' + ); + if (allFilesResponse.status > 299) { + throw new Error( + `Fetch files response not ok with body ${JSON.stringify( + allFilesResponse.data, + null, + 2 + )} and status ${allFilesResponse.status}` + ); + } + + if (Array.isArray(allFilesResponse.data.result)) { + Logger.info( + `Received ${allFilesResponse.data.result.length} fetched files` + ); + } else { + // Logger.info( + // `Expected to receive an array of files, but instead received ${JSON.stringify( + // allFilesResponse, + // null, + // 2 + // )}` + // ); + + throw new Error('Did not receive an array of files'); + } + + const hasMore = + allFilesResponse.data.result.length === + this.config.fetchFilesLimitPerRequest; + + return { + hasMore, + result: + allFilesResponse.data.result && + Array.isArray(allFilesResponse.data.result) + ? allFilesResponse.data.result.map(this.patchDriveFileResponseItem) + : [], + }; + } + private fetchItems = async ( params: { limit: number; offset: number; status: string; - updatedAt: string | undefined; + updatedAt: string | undefined | Date; }, type: 'files' | 'folders' ) => { - return await this.config.httpClient.get( + const response = await this.config.httpClient.get( `${process.env.NEW_DRIVE_URL}/drive/${type}`, { params } ); + Logger.info( + `Fetching item ${type} response: ${JSON.stringify( + response.data[0], + null, + 2 + )}` + ); + return response; + }; + + private fetchItemsByFolder = async ( + params: { + limit: number; + offset: number; + status: string; + updatedAt: string | undefined; + }, + folderId: number, + type: 'files' | 'folders' + ) => { + const response = await this.config.httpClient.get( + `${process.env.NEW_DRIVE_URL}/drive/folders/${folderId}/${type}`, + { params: { ...params, sort: 'ASC' } } + ); + Logger.info( + `Fetching by folder ${type} by folder response: ${JSON.stringify( + response.data.result[0], + null, + 2 + )}` + ); + return response; }; /** @@ -460,13 +713,16 @@ export class RemoteSyncManager { * * @param updatedAtCheckpoint Retrieve folders that were updated after this date */ - private async fetchFoldersFromRemote(updatedAtCheckpoint?: Date): Promise<{ + private async fetchFoldersFromRemote( + updatedAtCheckpoint?: Date, + offset = 0 + ): Promise<{ hasMore: boolean; result: RemoteSyncedFolder[]; }> { const params = { limit: this.config.fetchFilesLimitPerRequest, - offset: 0, + offset, status: 'ALL', updatedAt: updatedAtCheckpoint ? updatedAtCheckpoint.toISOString() @@ -510,6 +766,68 @@ export class RemoteSyncManager { : [], }; } + private async fetchFoldersByFolderFromRemote( + folderId: number, + updatedAtCheckpoint?: Date, + offset = 0 + ): Promise<{ + hasMore: boolean; + result: RemoteSyncedFolder[]; + }> { + const params = { + limit: this.config.fetchFilesLimitPerRequest, + offset, + status: 'ALL', + updatedAt: undefined, + }; + + const allFoldersResponse = await this.fetchItemsByFolder( + params, + folderId, + 'folders' + ); + + if (allFoldersResponse.status > 299) { + throw new Error( + `Fetch files response not ok with body ${JSON.stringify( + allFoldersResponse.data.result, + null, + 2 + )} and status ${allFoldersResponse.status}` + ); + } + + if (Array.isArray(allFoldersResponse.data.result)) { + Logger.info( + `Received ${allFoldersResponse.data.result.length} fetched files` + ); + } else { + Logger.info( + `Expected to receive an array of files, but instead received ${JSON.stringify( + allFoldersResponse.status, + null, + 2 + )}` + ); + + throw new Error('Did not receive an array of files'); + } + + const hasMore = + allFoldersResponse.data.result.length === + this.config.fetchFilesLimitPerRequest; + + return { + hasMore, + result: + allFoldersResponse.data.result && + Array.isArray(allFoldersResponse.data.result) + ? allFoldersResponse.data.result.map( + this.patchDriveFolderResponseItem + ) + : [], + }; + } private patchDriveFolderResponseItem = (payload: any): RemoteSyncedFolder => { // We will assume that we received an status diff --git a/src/apps/main/remote-sync/handlers.ts b/src/apps/main/remote-sync/handlers.ts index 85c2fdc34..9874957eb 100644 --- a/src/apps/main/remote-sync/handlers.ts +++ b/src/apps/main/remote-sync/handlers.ts @@ -15,10 +15,11 @@ import { sendUpdateFilesInSyncPending, } from '../background-processes/sync-engine'; import { debounce } from 'lodash'; +import configStore from '../config'; +import { setTrayStatus } from '../tray/tray'; const SYNC_DEBOUNCE_DELAY = 3_000; - let initialSyncReady = false; const driveFilesCollection = new DriveFilesCollection(); const driveFoldersCollection = new DriveFoldersCollection(); @@ -36,6 +37,17 @@ const remoteSyncManager = new RemoteSyncManager( } ); +export function setIsProcessing(isProcessing: boolean) { + remoteSyncManager.isProcessRunning = isProcessing; +} + +export function checkSyncEngineInProcess(milliSeconds: number) { + const syncingStatus: RemoteSyncStatus = 'SYNCING'; + const isSyncing = remoteSyncManager.getSyncStatus() === syncingStatus; + const recentlySyncing = remoteSyncManager.recentlyWasSyncing(milliSeconds); + return isSyncing || recentlySyncing; // syncing or recently was syncing +} + export async function getUpdatedRemoteItems() { try { const [allDriveFiles, allDriveFolders] = await Promise.all([ @@ -66,11 +78,45 @@ ipcMain.handle('GET_UPDATED_REMOTE_ITEMS', async () => { return getUpdatedRemoteItems(); }); -export function startRemoteSync(): Promise { - return remoteSyncManager.startRemoteSync(); +export async function startRemoteSync(folderId?: number): Promise { + try { + Logger.info('Starting remote sync function'); + Logger.info('Folder id', folderId); + + const { files, folders } = await remoteSyncManager.startRemoteSync( + folderId + ); + Logger.info('Remote sync started', folders?.length, 'folders'); + Logger.info('Remote sync started', files?.length, 'files'); + + if (folderId && folders && folders.length > 0) { + await Promise.all( + folders.map(async (folder) => { + if (!folder.id) return; + await sleep(400); + await startRemoteSync(folder.id); + }) + ); + } + Logger.info('Remote sync finished'); + } catch (error) { + if (error instanceof Error) { + Logger.error('Error during remote sync', error); + reportError(error); + } + } } + ipcMain.handle('START_REMOTE_SYNC', async () => { + Logger.info('Received start remote sync event'); + const isSyncing = await checkSyncEngineInProcess(5_000); + if (isSyncing) { + Logger.info('Remote sync is already running'); + return; + } + setIsProcessing(true); await startRemoteSync(); + setIsProcessing(false); }); remoteSyncManager.onStatusChange((newStatus) => { @@ -81,6 +127,15 @@ remoteSyncManager.onStatusChange((newStatus) => { broadcastToWindows('remote-sync-status-change', newStatus); }); +remoteSyncManager.onStatusChange((newStatus) => { + if (newStatus === 'SYNCING') { + setTrayStatus('SYNCING'); + } + if (newStatus === 'SYNCED') { + setTrayStatus('IDLE'); + } +}); + ipcMain.handle('get-remote-sync-status', () => remoteSyncManager.getSyncStatus() ); @@ -89,28 +144,29 @@ export async function updateRemoteSync(): Promise { // Wait before checking for updates, could be possible // that we received the notification, but if we check // for new data we don't receive it - await sleep(2_000); - await startRemoteSync(); + Logger.info('Updating remote sync'); + const isSyncing = await checkSyncEngineInProcess(5_000); + if (isSyncing) { + Logger.info('Remote sync is already running'); + return; + } + const userData = configStore.get('userData'); + const lastFilesSyncAt = await remoteSyncManager.getFileCheckpoint(); + Logger.info('Last files sync at', lastFilesSyncAt); + const folderId = lastFilesSyncAt ? undefined : userData?.root_folder_id; + await startRemoteSync(folderId); updateSyncEngine(); } export async function fallbackRemoteSync(): Promise { await sleep(2_000); + Logger.info('Fallback remote sync'); fallbackSyncEngine(); } -export function checkSyncEngineInProcess (milliSeconds: number) { - const syncingStatus: RemoteSyncStatus = 'SYNCING'; - const isSyncing = remoteSyncManager.getSyncStatus() === syncingStatus; - const recentlySyncing = remoteSyncManager.recentlyWasSyncing(milliSeconds); - return isSyncing || recentlySyncing; // syncing or recently was syncing -}; - -export function setIsProcessing(isProcessing: boolean) { - remoteSyncManager.isProcessRunning = isProcessing; -} - ipcMain.handle('SYNC_MANUALLY', async () => { Logger.info('[Manual Sync] Received manual sync event'); + const isSyncing = await checkSyncEngineInProcess(5_000); + if (isSyncing) return; await updateRemoteSync(); await fallbackRemoteSync(); }); @@ -142,16 +198,29 @@ eventBus.on('RECEIVED_REMOTE_CHANGES', async () => { // Wait before checking for updates, could be possible // that we received the notification, but if we check // for new data we don't receive it + Logger.info('Received remote changes event'); + const isSyncing = await checkSyncEngineInProcess(5_000); + if (isSyncing) { + Logger.info('Remote sync is already running'); + return; + } debouncedSynchronization(); }); eventBus.on('USER_LOGGED_IN', async () => { Logger.info('Received user logged in event'); - - await remoteSyncManager.startRemoteSync().catch((error) => { + try { + remoteSyncManager.isProcessRunning = true; + const userData = configStore.get('userData'); + const lastFilesSyncAt = await remoteSyncManager.getFileCheckpoint(); + Logger.info('Last files sync at', lastFilesSyncAt); + const folderId = lastFilesSyncAt ? undefined : userData?.root_folder_id; + await startRemoteSync(folderId); + remoteSyncManager.isProcessRunning = false; + } catch (error) { Logger.error('Error starting remote sync manager', error); - reportError(error); - }); + if (error instanceof Error) reportError(error); + } }); eventBus.on('USER_LOGGED_OUT', () => { @@ -180,4 +249,3 @@ export async function checkSyncInProgress(milliSeconds: number) { ipcMain.handle('CHECK_SYNC_IN_PROGRESS', async (_, milliSeconds: number) => { return await checkSyncInProgress(milliSeconds); }); - diff --git a/src/apps/main/remote-sync/helpers.ts b/src/apps/main/remote-sync/helpers.ts index c5b79fb4e..773e36884 100644 --- a/src/apps/main/remote-sync/helpers.ts +++ b/src/apps/main/remote-sync/helpers.ts @@ -1,5 +1,6 @@ export const WAITING_AFTER_SYNCING = 1000 * 60 * 3; // 5 minutes export const SIX_HOURS_IN_MILLISECONDS = 6 * 60 * 60 * 1000; +export const FIVETEEN_MINUTES_IN_MILLISECONDS = 15 * 60 * 1000; export type RemoteSyncedFile = { id: number; diff --git a/src/apps/main/tray/handlers.ts b/src/apps/main/tray/handlers.ts index f1ac01773..0a6102554 100644 --- a/src/apps/main/tray/handlers.ts +++ b/src/apps/main/tray/handlers.ts @@ -28,6 +28,14 @@ ipcMainDrive.on('FILE_DOWNLOADING', () => { setTrayStatus('SYNCING'); }); +ipcMainDrive.on('SYNCING', () => { + setTrayStatus('SYNCING'); +}); + +ipcMainDrive.on('SYNCED', () => { + setTrayStatus('IDLE'); +}); + ipcMainDrive.on('FILE_PREPARING', () => { setTrayStatus('SYNCING'); }); @@ -36,6 +44,10 @@ ipcMainDrive.on('FILE_DOWNLOADED', () => { setTrayStatus('IDLE'); }); +ipcMainDrive.on('FILE_DOWNLOAD_CANCEL', () => { + setTrayStatus('IDLE'); +}); + ipcMainDrive.on('FILE_MOVED', () => { setTrayStatus('IDLE'); }); diff --git a/src/apps/main/usage/service.ts b/src/apps/main/usage/service.ts index 71ec87e5c..4fd6a2fef 100644 --- a/src/apps/main/usage/service.ts +++ b/src/apps/main/usage/service.ts @@ -28,7 +28,7 @@ export class UserUsageService { } async calculateUsage(): Promise { - const [driveUsage, photosUsage, limitInBytes] = await Promise.all([ + const [driveUsage, photosUsage, limitInBytes] = await Promise.all([ this.getDriveUsage(), this.getPhotosUsage(), this.getLimit(), diff --git a/src/apps/main/usage/serviceBuilder.ts b/src/apps/main/usage/serviceBuilder.ts index c15e758a1..95f209813 100644 --- a/src/apps/main/usage/serviceBuilder.ts +++ b/src/apps/main/usage/serviceBuilder.ts @@ -7,7 +7,7 @@ import { UserUsageService } from './service'; import { onUserUnauthorized } from '../auth/handlers'; export function buildUsageService() { - const driveUrl = `${process.env.API_URL}/api`; + const driveUrl = `${process.env.API_URL}`; const photosUrl = process.env.PHOTOS_URL; const { name: clientName, version: clientVersion } = appInfo; diff --git a/src/apps/renderer/hooks/useInterval.tsx b/src/apps/renderer/hooks/useInterval.tsx new file mode 100644 index 000000000..88f6c6687 --- /dev/null +++ b/src/apps/renderer/hooks/useInterval.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; + +export function useInterval(callback: () => void, interval: number) { + const [intervalId, setIntervalId] = useState(null); + + useEffect(() => { + const intervalCallback = () => { + callback(); + }; + + const id = setInterval(intervalCallback, interval); + setIntervalId(id); + + return () => { + if (intervalId) { + clearInterval(intervalId); + } + }; + }, []); + return intervalId; +} diff --git a/src/apps/renderer/localize/locales/en.json b/src/apps/renderer/localize/locales/en.json index 07c45b995..b9ef42028 100644 --- a/src/apps/renderer/localize/locales/en.json +++ b/src/apps/renderer/localize/locales/en.json @@ -161,6 +161,7 @@ "uploading": "Uploading", "encrypting": "Encrypting", "downloaded": "Downloaded", + "cancel_downloaded": "Downloaded Cancel", "uploaded": "Uploaded", "deleting": "Moving to trash", "deleted": "Moved to trash", diff --git a/src/apps/renderer/localize/locales/es.json b/src/apps/renderer/localize/locales/es.json index 8eb843381..2bc673769 100644 --- a/src/apps/renderer/localize/locales/es.json +++ b/src/apps/renderer/localize/locales/es.json @@ -161,6 +161,7 @@ "uploading": "Subiendo", "encrypting": "Encriptando", "downloaded": "Descargado", + "cancel_downloaded": "Descarga Cancelada", "uploaded": "Subido", "deleting": "Moviendo a la papelera", "deleted": "Movido a la papelera", diff --git a/src/apps/renderer/localize/locales/fr.json b/src/apps/renderer/localize/locales/fr.json index 6c04cce82..17e2d32fa 100644 --- a/src/apps/renderer/localize/locales/fr.json +++ b/src/apps/renderer/localize/locales/fr.json @@ -132,6 +132,7 @@ "preferences": "Préférences", "sync": "Synchroniser", "issues": "Liste d'erreurs", + "send-feedback": "Envoyer des commentaires", "support": "Aide", "logout": "Déconnecter", "quit": "Fermer", @@ -160,6 +161,7 @@ "uploading": "téléchargement", "encrypting": "Encryptage", "downloaded": "Téléchargé", + "cancel_downloaded": "Télécharger Annulé", "uploaded": "Téléchargé", "deleting": "Déplacement vers les poubelles", "deleted": "Déplacé vers la poubelle", diff --git a/src/apps/renderer/pages/Login/service.ts b/src/apps/renderer/pages/Login/service.ts index 7ea74ab21..a9d691566 100644 --- a/src/apps/renderer/pages/Login/service.ts +++ b/src/apps/renderer/pages/Login/service.ts @@ -43,7 +43,7 @@ export async function accessRequest( let accessRes; try { - accessRes = await fetch(`${process.env.API_URL}/api/access`, { + accessRes = await fetch(`${process.env.API_URL}/access`, { method: 'POST', body: JSON.stringify({ email: email.toLowerCase(), @@ -84,7 +84,7 @@ export async function loginRequest(email: string): Promise<{ let loginRes; try { - loginRes = await fetch(`${process.env.API_URL}/api/login`, { + loginRes = await fetch(`${process.env.API_URL}/login`, { method: 'POST', body: JSON.stringify({ email }), headers: { diff --git a/src/apps/renderer/pages/Widget/Item.tsx b/src/apps/renderer/pages/Widget/Item.tsx index 1d2fca384..187de04d0 100644 --- a/src/apps/renderer/pages/Widget/Item.tsx +++ b/src/apps/renderer/pages/Widget/Item.tsx @@ -35,6 +35,8 @@ export function Item({ : translate('widget.body.activity.operation.encrypting'); } else if (action === 'DOWNLOADED') { description = translate('widget.body.activity.operation.downloaded'); + } else if (action === 'DOWNLOAD_CANCEL') { + description = translate('widget.body.activity.operation.cancel_downloaded'); } else if (action === 'UPLOADED') { description = translate('widget.body.activity.operation.uploaded'); } else if (action === 'DELETING') { @@ -117,6 +119,7 @@ export function Item({ {action && (action === 'DELETED' || action === 'DOWNLOADED' || + action === 'DOWNLOAD_CANCEL' || action === 'UPLOADED' || action === 'RENAMED') && ( diff --git a/src/apps/renderer/pages/Widget/SyncInfo.tsx b/src/apps/renderer/pages/Widget/SyncInfo.tsx index 15216c1c5..8fd086444 100644 --- a/src/apps/renderer/pages/Widget/SyncInfo.tsx +++ b/src/apps/renderer/pages/Widget/SyncInfo.tsx @@ -5,6 +5,7 @@ import { useSyncInfoSubscriber } from '../../hooks/useSyncInfoSubscriber'; import { AnimationWrapper } from './AnimationWrapper'; import { Item } from './Item'; import { NoInfoToShow } from './NoInfoToShow'; +import { useInterval } from '../../hooks/useInterval'; export default function SyncInfo() { const { processInfoUpdatedPayload, clearItems, removeOnProgressItems } = @@ -13,6 +14,7 @@ export default function SyncInfo() { useOnSyncStopped(removeOnProgressItems); useOnSyncRunning(clearItems); + useInterval(clearItems, 15000); return (
{processInfoUpdatedPayload.length === 0 && } diff --git a/src/apps/renderer/pages/Widget/index.tsx b/src/apps/renderer/pages/Widget/index.tsx index 3f9bcd3d5..afae28a81 100644 --- a/src/apps/renderer/pages/Widget/index.tsx +++ b/src/apps/renderer/pages/Widget/index.tsx @@ -12,9 +12,9 @@ export default function Widget() { const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); const handleRetrySync = () => { - window.electron.startRemoteSync().catch((err) => { - reportError(err); - }); + // window.electron.startRemoteSync().catch((err) => { + // reportError(err); + // }); }; const displayErrorInWidget = syncStatus && syncStatus === 'FAILED'; diff --git a/src/apps/shared/IPC/events/drive.ts b/src/apps/shared/IPC/events/drive.ts index e271e602b..efb1cf6f8 100644 --- a/src/apps/shared/IPC/events/drive.ts +++ b/src/apps/shared/IPC/events/drive.ts @@ -37,6 +37,7 @@ type DownloadEvents = { FILE_DOWNLOADING: (payload: FileProgressInfo) => void; FILE_PREPARING: (payload: FileProgressInfo) => void; FILE_DOWNLOADED: (payload: FileProgressInfo) => void; + FILE_DOWNLOAD_CANCEL: (payload: Partial) => void; FILE_DOWNLOAD_ERROR: (payload: FileErrorInfo) => void; }; @@ -69,6 +70,11 @@ type MoveEvents = { }) => void; }; +type SyncEvents = { + SYNCING: () => void; + SYNCED: () => void; +}; + type CloneEvents = { FILE_CLONNED: (payload: FileProgressInfo) => void; }; @@ -79,6 +85,7 @@ type FileEvents = UploadEvents & RenameEvents & OverwriteEvents & MoveEvents & - CloneEvents; + CloneEvents & + SyncEvents; export type DriveEvents = FolderEvents & FileEvents; diff --git a/src/apps/shared/IPC/events/sync-engine.ts b/src/apps/shared/IPC/events/sync-engine.ts index 0a3dbd60e..28f4f0203 100644 --- a/src/apps/shared/IPC/events/sync-engine.ts +++ b/src/apps/shared/IPC/events/sync-engine.ts @@ -68,6 +68,7 @@ export type FilesEvents = { FILE_DOWNLOADING: (payload: FileUpdatePayload) => void; FILE_PREPARING: (payload: FileUpdatePayload) => void; FILE_DOWNLOADED: (payload: FileUpdatePayload) => void; + FILE_DOWNLOAD_CANCEL: (payload: Partial) => void; FILE_UPLOAD_ERROR: (payload: { name: string; extension: string; @@ -123,6 +124,10 @@ export type SyncEngineInvocableFunctions = { files: DriveFile[]; folders: DriveFolder[]; }>; + GET_UPDATED_REMOTE_ITEMS_BY_FOLDER: (folderId: number) => Promise<{ + files: DriveFile[]; + folders: DriveFolder[]; + }>; START_REMOTE_SYNC: () => Promise; }; @@ -133,6 +138,8 @@ export type ProcessInfoUpdate = { key: string; additionalData: Record; }) => void; + SYNCING: () => void; + SYNCED: () => void; }; export type FromProcess = FilesEvents & diff --git a/src/apps/shared/types.ts b/src/apps/shared/types.ts index c15e316c5..7ccc3d40d 100644 --- a/src/apps/shared/types.ts +++ b/src/apps/shared/types.ts @@ -236,7 +236,12 @@ export type ProcessInfoUpdatePayload = progress: number; } | { - action: 'UPLOADED' | 'DOWNLOADED' | 'RENAMED' | 'DELETED'; + action: + | 'UPLOADED' + | 'DOWNLOADED' + | 'RENAMED' + | 'DELETED' + | 'DOWNLOAD_CANCEL'; } )) | ProcessIssue; diff --git a/src/apps/sync-engine/BindingManager.ts b/src/apps/sync-engine/BindingManager.ts index 550051262..0e79ac04a 100644 --- a/src/apps/sync-engine/BindingManager.ts +++ b/src/apps/sync-engine/BindingManager.ts @@ -33,8 +33,6 @@ export class BindingsManager { private static readonly PROVIDER_NAME = 'Internxt'; private progressBuffer = 0; private controllers: IControllers; - private processingResolve?: (unknown?: unknown) => void; - private processingReject?: (unknown?: unknown) => void; constructor( private readonly container: DependencyContainer, @@ -60,20 +58,18 @@ export class BindingsManager { ]); const tree = await this.container.existingItemsTreeBuilder.run(); - - await this.container.folderRepositoryInitiator.run(tree.folders); - await this.container.foldersPlaceholderCreator.run(tree.folders); - - await this.container.repositoryPopulator.run(tree.files); - await this.container.filesPlaceholderCreator.run(tree.files); - - await this.container?.filesPlaceholderDeleter?.run(tree.trashedFilesList); - await this.container?.folderPlaceholderDeleter?.run( - tree.trashedFoldersList - ); + await Promise.all([ + this.container.folderRepositoryInitiator.run(tree.folders), + this.container.foldersPlaceholderCreator.run(tree.folders), + this.container.repositoryPopulator.run(tree.files), + this.container.filesPlaceholderCreator.run(tree.files), + this.container?.filesPlaceholderDeleter?.run(tree.trashedFilesList), + this.container?.folderPlaceholderDeleter?.run(tree.trashedFoldersList), + ]); } async start(version: string, providerId: string) { + ipcRendererSyncEngine.send('SYNCING'); await this.stop(); await this.pollingStart(); @@ -102,15 +98,29 @@ export class BindingsManager { contentsId: string, callback: (response: boolean) => void ) => { - const fn = executeControllerWithFallback({ - handler: this.controllers.renameOrMove.execute.bind( - this.controllers.renameOrMove - ), - fallback: this.controllers.offline.renameOrMove.execute.bind( - this.controllers.offline.renameOrMove - ), - }); - fn(absolutePath, contentsId, callback); + try { + Logger.debug('Path received from rename callback', absolutePath); + + const fn = executeControllerWithFallback({ + handler: this.controllers.renameOrMove.execute.bind( + this.controllers.renameOrMove + ), + fallback: this.controllers.offline.renameOrMove.execute.bind( + this.controllers.offline.renameOrMove + ), + }); + fn(absolutePath, contentsId, callback); + const isFolder = fs.lstatSync(absolutePath).isDirectory(); + + this.container.virtualDrive.updateSyncStatus( + absolutePath, + isFolder, + true + ); + } catch (error) { + Logger.error('Error during rename or move operation', error); + } + ipcRendererSyncEngine.send('SYNCED'); ipcRenderer.send('CHECK_SYNC'); }, notifyFileAddedCallback: async ( @@ -172,6 +182,9 @@ export class BindingsManager { }); } this.progressBuffer = 0; + await this.controllers.notifyPlaceholderHydrationFinished.execute( + contentsId + ); } catch (error) { Logger.error('notify: ', error); Sentry.captureException(error); @@ -179,25 +192,16 @@ export class BindingsManager { fs.unlinkSync(path); Logger.debug('[Fetch Data Callback] Finish...', path); - - if (this.processingResolve) this.processingResolve(); return; } - if (!this.processingResolve) { - await this.controllers.notifyPlaceholderHydrationFinished.execute( - contentsId - ); - } + fs.unlinkSync(path); Logger.debug('[Fetch Data Callback] Finish...', path); - - if (this.processingResolve) this.processingResolve(); } catch (error) { Logger.error(error); Sentry.captureException(error); await callback(false, ''); await this.container.virtualDrive.closeDownloadMutex(); - if (this.processingResolve) this.processingResolve(); } }, notifyMessageCallback: ( @@ -225,9 +229,8 @@ export class BindingsManager { validateDataCallback: () => { Logger.debug('validateDataCallback'); }, - cancelFetchDataCallback: () => { - // TODO: clean up temp file, free up space of placeholder - if (this.processingResolve) this.processingResolve(); + cancelFetchDataCallback: async () => { + await this.controllers.downloadFile.cancel(); Logger.debug('cancelFetchDataCallback'); }, fetchPlaceholdersCallback: () => { @@ -267,9 +270,10 @@ export class BindingsManager { await this.container.virtualDrive.connectSyncRoot(); await runner([this.load.bind(this), this.polling.bind(this)]); + ipcRendererSyncEngine.send('SYNCED'); } - watch() { + async watch() { const queueManager = new QueueManager({ handleAdd: async (task: QueueItem) => { try { @@ -299,17 +303,19 @@ export class BindingsManager { try { Logger.debug('[Handle Hydrate Callback] Preparing begins', task.path); - // Crear una promesa que será resuelta por fetchDataCallback - const processingPromise = new Promise((resolve, reject) => { - this.processingResolve = resolve; - this.processingReject = reject; - }); + const atributtes = + await this.container.virtualDrive.getPlaceholderAttribute( + task.path + ); + Logger.debug('atributtes', atributtes); - await this.container.virtualDrive.hydrateFile(task.path); + const status = await this.container.virtualDrive.getPlaceholderState( + task.path + ); - // Esperar hasta que fetchDataCallback resuelva o rechace la promesa - await processingPromise; + Logger.debug('status', status); + await this.container.virtualDrive.hydrateFile(task.path); ipcRenderer.send('CHECK_SYNC'); Logger.debug('[Handle Hydrate Callback] Finish begins', task.path); @@ -319,7 +325,6 @@ export class BindingsManager { Sentry.captureException(error); } }, - handleDehydrate: async (task: QueueItem) => { try { Logger.debug('Dehydrate', task); @@ -347,7 +352,7 @@ export class BindingsManager { queueManager, logWatcherPath ); - queueManager.processAll(); + await queueManager.processAll(); } async stop() { @@ -399,6 +404,7 @@ export class BindingsManager { async update() { Logger.info('[SYNC ENGINE]: Updating placeholders'); + ipcRendererSyncEngine.send('SYNCING'); try { const tree = await this.container.existingItemsTreeBuilder.run(); @@ -412,6 +418,8 @@ export class BindingsManager { // Create all the placeholders that are in the tree await this.container.folderPlaceholderUpdater.run(tree.folders); await this.container.filesPlaceholderUpdater.run(tree.files); + ipcRendererSyncEngine.send('SYNCED'); + ipcRenderer.send('CHECK_SYNC'); } catch (error) { Logger.error('[SYNC ENGINE] ', error); Sentry.captureException(error); @@ -426,12 +434,13 @@ export class BindingsManager { async polling(): Promise { try { Logger.info('[SYNC ENGINE] Monitoring polling...'); - + ipcRendererSyncEngine.send('SYNCING'); const fileInPendingPaths = (await this.container.virtualDrive.getPlaceholderWithStatePending()) as Array; Logger.info('[SYNC ENGINE] fileInPendingPaths', fileInPendingPaths); await this.container.fileSyncOrchestrator.run(fileInPendingPaths); + ipcRendererSyncEngine.send('SYNCED'); ipcRenderer.send('CHECK_SYNC'); } catch (error) { Logger.error('[SYNC ENGINE] Polling', error); diff --git a/src/apps/sync-engine/callbacks-controllers/controllers/DownloadFileController.ts b/src/apps/sync-engine/callbacks-controllers/controllers/DownloadFileController.ts index 477307da4..f7eae188c 100644 --- a/src/apps/sync-engine/callbacks-controllers/controllers/DownloadFileController.ts +++ b/src/apps/sync-engine/callbacks-controllers/controllers/DownloadFileController.ts @@ -13,7 +13,10 @@ export class DownloadFileController extends CallbackController { super(); } - private async action(id: string, callback?: CallbackDownload): Promise { + private async action( + id: string, + callback: CallbackDownload + ): Promise { const file = this.fileFinder.run(id); Logger.info('[Begin] Download: ', file.path); return await this.downloader.run(file, callback); @@ -23,7 +26,10 @@ export class DownloadFileController extends CallbackController { return this.fileFinder.run(contentsId); } - async execute(filePlaceholderId: FilePlaceholderId, callback?: CallbackDownload): Promise { + async execute( + filePlaceholderId: FilePlaceholderId, + callback: CallbackDownload + ): Promise { const trimmedId = this.trim(filePlaceholderId); try { @@ -48,4 +54,8 @@ export class DownloadFileController extends CallbackController { }); } } + + async cancel() { + await this.downloader.stop(); + } } diff --git a/src/apps/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts b/src/apps/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts index 62c589f38..6c36de374 100644 --- a/src/apps/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts +++ b/src/apps/sync-engine/callbacks-controllers/controllers/RenameOrMoveController.ts @@ -38,12 +38,23 @@ export class RenameOrMoveController extends CallbackController { if (this.isFilePlaceholder(trimmedId)) { const [_, contentsId] = trimmedId.split(':'); + Logger.debug('[RUN File Path Updater]', contentsId, posixRelativePath); await this.filePathUpdater.run(contentsId, posixRelativePath); + Logger.debug( + '[FINISH File Path Updater]', + contentsId, + posixRelativePath + ); return callback(true); } if (this.isFolderPlaceholder(trimmedId)) { const [_, folderUuid] = trimmedId.split(':'); + Logger.debug( + '[RUN Folder Path Updater]', + contentsId, + posixRelativePath + ); await this.folderPathUpdater.run(folderUuid, posixRelativePath); return callback(true); } @@ -51,7 +62,7 @@ export class RenameOrMoveController extends CallbackController { Logger.error('Unidentified placeholder id: ', trimmedId); callback(false); } catch (error: unknown) { - Logger.error(error); + Logger.error('[ERROR Rename or move]', error); Sentry.captureException(error); callback(false); } diff --git a/src/apps/sync-engine/dependency-injection/common/QueueManager.ts b/src/apps/sync-engine/dependency-injection/common/QueueManager.ts index af6ec8606..6ee17fd10 100644 --- a/src/apps/sync-engine/dependency-injection/common/QueueManager.ts +++ b/src/apps/sync-engine/dependency-injection/common/QueueManager.ts @@ -15,13 +15,22 @@ export type QueueHandler = { handleChangeSize: HandleAction; }; -// const queueFilePath = path.join(__dirname, 'queue.json'); export class QueueManager implements IQueueManager { - private _queue: QueueItem[] = []; - - private isProcessing = false; - - // private queueFilePath = queueFilePath; + private queues: { [key: string]: QueueItem[] } = { + add: [], + hydrate: [], + dehydrate: [], + change: [], + changeSize: [], + }; + + private isProcessing: { [key: string]: boolean } = { + add: false, + hydrate: false, + dehydrate: false, + change: false, + changeSize: false, + }; actions: HandleActions; @@ -33,49 +42,26 @@ export class QueueManager implements IQueueManager { changeSize: handlers.handleChangeSize, change: handlers.handleChange || (() => Promise.resolve()), }; - - // this.loadQueueFromFile(); } - // private saveQueueToFile(): void { - // const queue = this._queue.filter((item) => item.type !== 'hydrate'); - // fs.writeFileSync(this.queueFilePath, JSON.stringify(queue, null, 2)); - // Logger.debug('Queue saved to file.'); - // } - - // private loadQueueFromFile(): void { - // try { - // if (fs.existsSync(this.queueFilePath)) { - // const data = fs.readFileSync(this.queueFilePath, 'utf-8'); - // this._queue = JSON.parse(data); - // Logger.debug('Queue loaded from file.'); - // } - // } catch (error) { - // Logger.error('Failed to load queue from file:', error); - // } - // } - public enqueue(task: QueueItem): void { Logger.debug(`Task enqueued: ${JSON.stringify(task)}`); - // const existingTask = this._queue.find( - // (item) => item.path === task.path && item.type === task.type - // ); - // if (existingTask) { - // Logger.debug('Task already exists in queue. Skipping.'); - // this.processAll(); - - // return; - // } - this._queue.push(task); - this.sortQueue(); - // this.saveQueueToFile(); - if (!this.isProcessing) { - this.processAll(); + const existingTask = this.queues[task.type].find( + (item) => item.path === task.path && item.type === task.type + ); + if (existingTask) { + Logger.debug('Task already exists in queue. Skipping.'); + return; + } + this.queues[task.type].push(task); + this.sortQueue(task.type); + if (!this.isProcessing[task.type]) { + this.processQueue(task.type); } } - private sortQueue(): void { - this._queue.sort((a, b) => { + private sortQueue(type: string): void { + this.queues[type].sort((a, b) => { if (a.isFolder && b.isFolder) { return 0; } @@ -89,52 +75,28 @@ export class QueueManager implements IQueueManager { }); } - public async processNext(): Promise { - if (this._queue.length === 0) { - Logger.debug('No tasks in queue.'); + private async processQueue(type: string): Promise { + if (this.isProcessing[type]) { return; } - const task = this._queue.shift(); - if (!task) return; - - // this.saveQueueToFile(); - - Logger.debug(`Processing task: ${JSON.stringify(task)}`); - - try { - switch (task.type) { - case 'add': - await this.actions.add(task); - break; - case 'hydrate': - await this.actions.hydrate(task); - break; - case 'dehydrate': - await this.actions.dehydrate(task); - break; - case 'change': - await this.actions.change(task); - break; - case 'changeSize': - await this.actions.changeSize(task); - break; - default: - Logger.debug('Unknown task type.'); - break; + this.isProcessing[type] = true; + while (this.queues[type].length > 0) { + const task = this.queues[type].shift(); + if (task) { + Logger.debug(`Processing ${type} task: ${JSON.stringify(task)}`); + try { + await this.actions[task.type](task); + } catch (error) { + Logger.error(`Failed to process ${type} task:`, task, error); + } } - } catch (error) { - Logger.error('Failed to process task:', task); } + this.isProcessing[type] = false; } public async processAll(): Promise { - this.isProcessing = true; - while (this._queue.length > 0) { - await sleep(200); - Logger.debug('Processing all tasks. Queue length:', this._queue.length); - await this.processNext(); - } - this.isProcessing = false; + const taskTypes = Object.keys(this.queues); + await Promise.all(taskTypes.map((type) => this.processQueue(type))); } } diff --git a/src/apps/sync-engine/dependency-injection/common/sdk.ts b/src/apps/sync-engine/dependency-injection/common/sdk.ts index 84437b858..b64a9dc50 100644 --- a/src/apps/sync-engine/dependency-injection/common/sdk.ts +++ b/src/apps/sync-engine/dependency-injection/common/sdk.ts @@ -11,7 +11,7 @@ export class DependencyInjectionStorageSdk { return DependencyInjectionStorageSdk.sdk; } - const url = `${process.env.API_URL}/api`; + const url = `${process.env.API_URL}`; const { name: clientName, version: clientVersion } = packageJson; const token = await ipcRenderer.invoke('get-token'); diff --git a/src/apps/sync-engine/index.ts b/src/apps/sync-engine/index.ts index 2e616c041..ab80a7702 100644 --- a/src/apps/sync-engine/index.ts +++ b/src/apps/sync-engine/index.ts @@ -62,9 +62,7 @@ async function setUp() { ipcRenderer.on('UPDATE_SYNC_ENGINE_PROCESS', async () => { Logger.info('[SYNC ENGINE] Updating sync engine'); - await bindings.update(); - Logger.info('[SYNC ENGINE] sync engine updated successfully'); }); @@ -80,7 +78,7 @@ async function setUp() { Logger.info('[SYNC ENGINE] updating file unsync'); const filesPending = await bindings.getFileInSyncPending(); - + event.sender.send('UPDATE_UNSYNC_FILE_IN_SYNC_ENGINE', filesPending); }); @@ -111,7 +109,7 @@ async function setUp() { '{E9D7EB38-B229-5DC5-9396-017C449D59CD}' ); - bindings.watch(); + await bindings.watch(); ipcRenderer.send('CHECK_SYNC'); } diff --git a/src/apps/utils/date.ts b/src/apps/utils/date.ts index 8a107e75f..c9f36654e 100644 --- a/src/apps/utils/date.ts +++ b/src/apps/utils/date.ts @@ -5,3 +5,35 @@ export function getDateFromSeconds(seconds: number): Date { export function getSecondsFromDateString(dateString: string): number { return Math.trunc(new Date(dateString).valueOf() / 1000); } + +export function convertUTCToSpain(dateString: string) { + const date = new Date(dateString); // Crear un objeto Date a partir del string UTC + + // Opciones de formato para España (CET/CEST) + const options: Intl.DateTimeFormatOptions = { + timeZone: 'Europe/Madrid', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, // Incluye milisegundos + hour12: false, // Formato de 24 horas + }; + + // Formatear la fecha a la franja horaria de España + const formatter = new Intl.DateTimeFormat('es-ES', options); + const parts = formatter.formatToParts(date); + + // Construir la fecha en el formato deseado + const formattedDate = `${ + parts.find((part) => part?.type === 'year')?.value + }-${parts.find((part) => part.type === 'month')?.value}-${ + parts.find((part) => part.type === 'day')?.value + }T${parts.find((part) => part.type === 'hour')?.value}:${ + parts.find((part) => part.type === 'minute')?.value + }:${parts.find((part) => part.type === 'second')?.value}.000Z`; + + return formattedDate; +} diff --git a/src/context/virtual-drive/contents/application/ContentsDownloader.ts b/src/context/virtual-drive/contents/application/ContentsDownloader.ts index d1ac68d96..f13e2a663 100644 --- a/src/context/virtual-drive/contents/application/ContentsDownloader.ts +++ b/src/context/virtual-drive/contents/application/ContentsDownloader.ts @@ -15,7 +15,7 @@ import { CallbackDownload } from '../../../../apps/sync-engine/BindingManager'; export class ContentsDownloader { private readableDownloader: Readable | null; - private WAIT_TO_SEND_PROGRESS = 20000; + private WAIT_TO_SEND_PROGRESS = 1000; private progressAt: Date | null = null; constructor( private readonly managerFactory: ContentsManagersFactory, @@ -27,10 +27,14 @@ export class ContentsDownloader { this.readableDownloader = null; } + private downloaderIntance: ContentFileDownloader | null = null; + private downloaderIntanceCB: CallbackDownload | null = null; + private downloaderFile: File | null = null; + private async registerEvents( downloader: ContentFileDownloader, file: File, - callback?: CallbackDownload + callback: CallbackDownload ) { const location = await this.temporalFolderProvider(); ensureFolderExists(location); @@ -55,7 +59,7 @@ export class ContentsDownloader { const fileSizeInBytes = stats.size; const progress = fileSizeInBytes / file.size; - await this.waitToCb(filePath, callback); + await callback(true, filePath); this.ipc.send('FILE_DOWNLOADING', { name: file.name, @@ -86,22 +90,12 @@ export class ContentsDownloader { }); } - private async waitToCb(filePath: string, callback?: CallbackDownload) { - if ( - this.progressAt && - new Date().getTime() - this.progressAt.getTime() > - this.WAIT_TO_SEND_PROGRESS - ) { - if (callback) { - await callback(true, filePath); - } - this.progressAt = new Date(); - } - } - - async run(file: File, callback?: CallbackDownload): Promise { + async run(file: File, callback: CallbackDownload): Promise { const downloader = this.managerFactory.downloader(); + this.downloaderIntance = downloader; + this.downloaderIntanceCB = callback; + this.downloaderFile = file; await this.registerEvents(downloader, file, callback); const readable = await downloader.download(file); @@ -118,14 +112,39 @@ export class ContentsDownloader { const events = localContents.pullDomainEvents(); await this.eventBus.publish(events); - this.ipc.send('FILE_DOWNLOADED', { - name: file.name, - extension: file.type, - nameWithExtension: file.nameWithExtension, - size: file.size, - processInfo: { elapsedTime: downloader.elapsedTime() }, - }); + // this.ipc.send('FILE_DOWNLOADED', { + // name: file.name, + // extension: file.type, + // nameWithExtension: file.nameWithExtension, + // size: file.size, + // processInfo: { elapsedTime: downloader.elapsedTime() }, + // }); return write; } + + async stop() { + Logger.info('[Server] Stopping download 1'); + if ( + !this.downloaderIntance || + !this.downloaderIntanceCB || + !this.downloaderFile + ) + return; + + Logger.info('[Server] Stopping download 2'); + this.downloaderIntance.forceStop(); + this.downloaderIntanceCB(false, ''); + + this.ipc.send('FILE_DOWNLOAD_CANCEL', { + name: this.downloaderFile.name, + extension: this.downloaderFile.type, + nameWithExtension: this.downloaderFile.nameWithExtension, + size: this.downloaderFile.size, + }); + + this.downloaderIntanceCB = null; + this.downloaderIntance = null; + this.downloaderFile = null; + } } diff --git a/src/context/virtual-drive/contents/application/ContentsUploader.ts b/src/context/virtual-drive/contents/application/ContentsUploader.ts index 8cdb5f33d..2e23fad03 100644 --- a/src/context/virtual-drive/contents/application/ContentsUploader.ts +++ b/src/context/virtual-drive/contents/application/ContentsUploader.ts @@ -7,7 +7,7 @@ import { PlatformPathConverter } from '../../shared/application/PlatformPathConv import { RelativePathToAbsoluteConverter } from '../../shared/application/RelativePathToAbsoluteConverter'; import { SyncEngineIpc } from '../../../../apps/sync-engine/ipcRendererSyncEngine'; import { ipcRenderer } from 'electron'; - +import Logger from 'electron-log'; export class ContentsUploader { constructor( private readonly remoteContentsManagersFactory: ContentsManagersFactory, @@ -38,7 +38,6 @@ export class ContentsUploader { size: localFileContents.size, processInfo: { elapsedTime: uploader.elapsedTime(), progress }, }); - ipcRenderer.send('CHECK_SYNC'); }); uploader.on('error', (error: Error) => { @@ -64,27 +63,35 @@ export class ContentsUploader { } async run(posixRelativePath: string): Promise { - const win32RelativePath = - PlatformPathConverter.posixToWin(posixRelativePath); + try { + const win32RelativePath = + PlatformPathConverter.posixToWin(posixRelativePath); + + const absolutePath = + this.relativePathToAbsoluteConverter.run(win32RelativePath); - const absolutePath = - this.relativePathToAbsoluteConverter.run(win32RelativePath); + Logger.debug('[DEBUG UPLOAD]:', posixRelativePath, absolutePath); - const { contents, abortSignal } = await this.contentProvider.provide( - absolutePath - ); + const { contents, abortSignal } = await this.contentProvider.provide( + absolutePath + ); + Logger.debug('[DEBUG UPLOAD STEEP 1]: '); - const uploader = this.remoteContentsManagersFactory.uploader( - contents, - abortSignal - ); + const uploader = this.remoteContentsManagersFactory.uploader( + contents, + abortSignal + ); - this.registerEvents(uploader, contents); + this.registerEvents(uploader, contents); - const contentsId = await uploader.upload(contents.stream, contents.size); + const contentsId = await uploader.upload(contents.stream, contents.size); - const fileContents = RemoteFileContents.create(contentsId, contents.size); + const fileContents = RemoteFileContents.create(contentsId, contents.size); - return fileContents; + return fileContents; + } catch (error: unknown) { + Logger.error('[ERROR DEBUG]', error); + throw error; + } } } diff --git a/src/context/virtual-drive/files/application/FileCreator.ts b/src/context/virtual-drive/files/application/FileCreator.ts index 9c179a85d..341d5693d 100644 --- a/src/context/virtual-drive/files/application/FileCreator.ts +++ b/src/context/virtual-drive/files/application/FileCreator.ts @@ -12,6 +12,7 @@ import { OfflineFile } from '../domain/OfflineFile'; import { SyncEngineIpc } from '../../../../apps/sync-engine/ipcRendererSyncEngine'; import { FileStatuses } from '../domain/FileStatus'; import { ipcRenderer } from 'electron'; +import Logger from 'electron-log'; export class FileCreator { constructor( @@ -39,18 +40,30 @@ export class FileCreator { await this.fileDeleter.run(existingFile.contentsId); } } - + Logger.debug('[DEBUG IN FILECREATOR STEEP 1]' + filePath.value); const size = new FileSize(contents.size); const folder = this.folderFinder.findFromFilePath(filePath); + Logger.debug('[DEBUG IN FILECREATOR STEEP 2]' + filePath.value); + const offline = OfflineFile.create(contents.id, folder, size, filePath); + Logger.debug('[DEBUG IN FILECREATOR STEEP 3]' + filePath.value); + + const existingPersistedFile = await this.remote.checkStatusFile(contents.id); + + Logger.debug('[DEBUG IN FILECREATOR STEEP 3.1]' + existingPersistedFile); + const persistedAttributes = await this.remote.persist(offline); + + Logger.debug('[DEBUG IN FILECREATOR STEEP 4]' + filePath.value); const file = File.from(persistedAttributes); + Logger.debug('[DEBUG IN FILECREATOR STEEP 5]' + filePath.value); await this.repository.add(file); + Logger.debug('[DEBUG IN FILECREATOR STEEP 6]' + filePath.value); await this.eventBus.publish(offline.pullDomainEvents()); this.ipc.send('FILE_CREATED', { name: file.name, @@ -59,10 +72,13 @@ export class FileCreator { }); ipcRenderer.send('CHECK_SYNC'); + Logger.debug('[DEBUG IN FILECREATOR STEEP 7]' + filePath.value); return file; } catch (error: unknown) { const message = error instanceof Error ? error.message : 'unknown error'; + Logger.debug('[DEBUG ERROR IN FILECREATOR]' + filePath.value, error); + this.ipc.send('FILE_UPLOAD_ERROR', { name: filePath.name(), extension: filePath.extension(), diff --git a/src/context/virtual-drive/files/application/FilePathUpdater.ts b/src/context/virtual-drive/files/application/FilePathUpdater.ts index 06896bed7..a0fdf3186 100644 --- a/src/context/virtual-drive/files/application/FilePathUpdater.ts +++ b/src/context/virtual-drive/files/application/FilePathUpdater.ts @@ -9,6 +9,7 @@ import { FileRepository } from '../domain/FileRepository'; import { RemoteFileSystem } from '../domain/file-systems/RemoteFileSystem'; import { LocalFileSystem } from '../domain/file-systems/LocalFileSystem'; import { SyncEngineIpc } from '../../../../apps/sync-engine/ipcRendererSyncEngine'; +import Logger from 'electron-log'; export class FilePathUpdater { constructor( @@ -32,13 +33,20 @@ export class FilePathUpdater { } private async move(file: File, destination: FilePath) { - const trackerId = await this.local.getLocalFileId(file); - + Logger.debug('[MOVE]', file.name, destination.value); const destinationFolder = this.folderFinder.run(destination.dirname()); - file.moveTo(destinationFolder, trackerId); + Logger.debug('[MOVE TO]', file.path, destinationFolder.name); + try { + const trackerId = await this.local.getFileIdentity(file.path); + file.moveTo(destinationFolder, trackerId); + } catch (error: any) { + Logger.warn(`Error in FilePathUpdater.move: ${error?.message}`); + } + Logger.debug('[REMOTE MOVE]', file.name, destinationFolder.name); await this.remote.move(file); + Logger.debug('[REPOSITORY MOVE]', file.name, destinationFolder.name); await this.repository.update(file); const events = file.pullDomainEvents(); @@ -46,44 +54,57 @@ export class FilePathUpdater { } async run(contentsId: string, posixRelativePath: string) { - const destination = new FilePath(posixRelativePath); - const file = this.fileFinderByContentsId.run(contentsId); + try { + const destination = new FilePath(posixRelativePath); + const file = this.fileFinderByContentsId.run(contentsId); - if (file.dirname !== destination.dirname()) { - if (file.nameWithExtension !== destination.nameWithExtension()) { - throw new ActionNotPermittedError('rename and change folder'); + if (file.dirname !== destination.dirname()) { + if (file.nameWithExtension !== destination.nameWithExtension()) { + throw new ActionNotPermittedError('rename and change folder'); + } + Logger.error('[RUN MOVE]', file.name, destination.value); + await this.move(file, destination); + return; } - await this.move(file, destination); - return; - } - const destinationFile = this.repository.searchByPartial({ - path: destination.value, - }); - - if (destinationFile) { - this.ipc.send('FILE_RENAME_ERROR', { - name: file.name, - extension: file.type, - nameWithExtension: file.nameWithExtension, - error: 'Renaming error: file already exists', + const destinationFile = this.repository.searchByPartial({ + path: destination.value, }); - throw new FileAlreadyExistsError(destination.name()); - } - if (destination.extensionMatch(file.type)) { - this.ipc.send('FILE_RENAMING', { - oldName: file.name, - nameWithExtension: destination.nameWithExtension(), - }); - await this.rename(file, destination); - this.ipc.send('FILE_RENAMED', { - oldName: file.name, - nameWithExtension: destination.nameWithExtension(), - }); - return; - } + if (destinationFile) { + this.ipc.send('FILE_RENAME_ERROR', { + name: file.name, + extension: file.type, + nameWithExtension: file.nameWithExtension, + error: 'Renaming error: file already exists', + }); + throw new FileAlreadyExistsError(destination.name()); + } - throw new Error('Cannot reupload files atm'); + Logger.debug('[RUN RENAME]', file.name, destination.value); + Logger.debug('[RUN RENAME]', file.name, destination.nameWithExtension()); + Logger.debug( + '[RUN RENAME]', + file.nameWithExtension, + destination.extensionMatch(file.type) + ); + if (destination.extensionMatch(file.type)) { + this.ipc.send('FILE_RENAMING', { + oldName: file.name, + nameWithExtension: destination.nameWithExtension(), + }); + await this.rename(file, destination); + this.ipc.send('FILE_RENAMED', { + oldName: file.name, + nameWithExtension: destination.nameWithExtension(), + }); + return; + } + + throw new Error('Cannot reupload files atm'); + } catch (error: any) { + Logger.error(`Error in FilePathUpdater.run: ${error.message}`); + throw error; + } } } diff --git a/src/context/virtual-drive/files/domain/file-systems/LocalFileSystem.ts b/src/context/virtual-drive/files/domain/file-systems/LocalFileSystem.ts index 84a201134..5090349a2 100644 --- a/src/context/virtual-drive/files/domain/file-systems/LocalFileSystem.ts +++ b/src/context/virtual-drive/files/domain/file-systems/LocalFileSystem.ts @@ -4,6 +4,8 @@ import { PlaceholderState } from '../PlaceholderState'; export interface LocalFileSystem { createPlaceHolder(file: File): Promise; + fileExists(filePath: string): Promise; + getLocalFileId(file: File): Promise<`${string}-${string}`>; updateSyncStatus(file: File): Promise; @@ -20,8 +22,5 @@ export interface LocalFileSystem { relativePath: string ): Promise; - updateFileIdentity( - path: File['path'], - newIdentity: string - ): Promise; + updateFileIdentity(path: File['path'], newIdentity: string): Promise; } diff --git a/src/context/virtual-drive/files/infrastructure/NodeWinLocalFileSystem.ts b/src/context/virtual-drive/files/infrastructure/NodeWinLocalFileSystem.ts index 0f722dcf2..3c3c51863 100644 --- a/src/context/virtual-drive/files/infrastructure/NodeWinLocalFileSystem.ts +++ b/src/context/virtual-drive/files/infrastructure/NodeWinLocalFileSystem.ts @@ -13,10 +13,21 @@ export class NodeWinLocalFileSystem implements LocalFileSystem { private readonly relativePathToAbsoluteConverter: RelativePathToAbsoluteConverter ) {} + async fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } + async getLocalFileId(file: File): Promise<`${string}-${string}`> { const win32AbsolutePath = this.relativePathToAbsoluteConverter.run( file.path - ); + ); + + Logger.info('[getLocalFileId]: ', win32AbsolutePath); const { ino, dev } = await fs.stat(win32AbsolutePath); @@ -74,9 +85,16 @@ export class NodeWinLocalFileSystem implements LocalFileSystem { ) as Promise; } - async updateFileIdentity(path: string, newIdentity: `${string}-${string}`): Promise { - Logger.info('[updateFileIdentity]: ', path, newIdentity); - const isNotDirectory = true; - return this.virtualDrive.updateFileIdentity(path, newIdentity, isNotDirectory); + async updateFileIdentity( + path: string, + newIdentity: `${string}-${string}` + ): Promise { + Logger.info('[updateFileIdentity]: ', path, newIdentity); + const isNotDirectory = true; + return this.virtualDrive.updateFileIdentity( + path, + newIdentity, + isNotDirectory + ); } } diff --git a/src/context/virtual-drive/folders/application/FolderPathUpdater.ts b/src/context/virtual-drive/folders/application/FolderPathUpdater.ts index f80062723..a28d65966 100644 --- a/src/context/virtual-drive/folders/application/FolderPathUpdater.ts +++ b/src/context/virtual-drive/folders/application/FolderPathUpdater.ts @@ -42,6 +42,7 @@ export class FolderPathUpdater { return await this.folderRenamer.run(folder, desiredPath); } - throw new Error('No path change detected for folder path update'); + // throw new Error('No path change detected for folder path update'); + Logger.warn('No path change detected for folder path update'); } } diff --git a/src/context/virtual-drive/folders/infrastructure/HttpRemoteFileSystem.ts b/src/context/virtual-drive/folders/infrastructure/HttpRemoteFileSystem.ts index 2113a94c6..6cd732d9d 100644 --- a/src/context/virtual-drive/folders/infrastructure/HttpRemoteFileSystem.ts +++ b/src/context/virtual-drive/folders/infrastructure/HttpRemoteFileSystem.ts @@ -31,7 +31,7 @@ export class HttpRemoteFileSystem implements RemoteFileSystem { try { const response = await this.driveClient.post( - `${process.env.API_URL}/api/storage/folder`, + `${process.env.API_URL}/storage/folder`, body ); if (response.status !== 201) { @@ -106,7 +106,7 @@ export class HttpRemoteFileSystem implements RemoteFileSystem { } async rename(folder: Folder): Promise { - const url = `${process.env.API_URL}/api/storage/folder/${folder.id}/meta`; + const url = `${process.env.API_URL}/storage/folder/${folder.id}/meta`; const body: UpdateFolderNameDTO = { metadata: { itemName: folder.name }, @@ -123,7 +123,7 @@ export class HttpRemoteFileSystem implements RemoteFileSystem { } async move(folder: Folder): Promise { - const url = `${process.env.API_URL}/api/storage/move/folder`; + const url = `${process.env.API_URL}/storage/move/folder`; const body = { destination: folder.parentId, folderId: folder.id }; diff --git a/src/context/virtual-drive/items/application/RemoteItemsGenerator.ts b/src/context/virtual-drive/items/application/RemoteItemsGenerator.ts index e4f2510e6..00d1712e2 100644 --- a/src/context/virtual-drive/items/application/RemoteItemsGenerator.ts +++ b/src/context/virtual-drive/items/application/RemoteItemsGenerator.ts @@ -1,3 +1,5 @@ +import { DriveFile } from '../../../../apps/main/database/entities/DriveFile'; +import { DriveFolder } from '../../../../apps/main/database/entities/DriveFolder'; import { SyncEngineIpc } from '../../../../apps/sync-engine/ipcRendererSyncEngine'; import { ServerFile, @@ -11,45 +13,66 @@ import { export class RemoteItemsGenerator { constructor(private readonly ipc: SyncEngineIpc) {} + private mapFile(updatedFile: DriveFile): ServerFile { + return { + bucket: updatedFile.bucket, + createdAt: updatedFile.createdAt, + encrypt_version: '03-aes', + fileId: updatedFile.fileId, + folderId: updatedFile.folderId, + id: updatedFile.id, + modificationTime: updatedFile.modificationTime, + name: updatedFile.name, + plainName: updatedFile.plainName, + size: updatedFile.size, + type: updatedFile.type ?? null, + updatedAt: updatedFile.updatedAt, + userId: updatedFile.userId, + status: updatedFile.status as ServerFileStatus, + uuid: updatedFile.uuid, + }; + } + + private mapFolder(updatedFolder: DriveFolder): ServerFolder { + return { + bucket: updatedFolder.bucket ?? null, + createdAt: updatedFolder.createdAt, + id: updatedFolder.id, + name: updatedFolder.name, + parentId: updatedFolder.parentId ?? null, + updatedAt: updatedFolder.updatedAt, + plain_name: updatedFolder.plainName ?? null, + status: updatedFolder.status as ServerFolderStatus, + uuid: updatedFolder.uuid, + }; + } + async getAll(): Promise<{ files: ServerFile[]; folders: ServerFolder[] }> { const updatedRemoteItems = await this.ipc.invoke( 'GET_UPDATED_REMOTE_ITEMS' ); - const files = updatedRemoteItems.files.map((updatedFile) => { - return { - bucket: updatedFile.bucket, - createdAt: updatedFile.createdAt, - encrypt_version: '03-aes', - fileId: updatedFile.fileId, - folderId: updatedFile.folderId, - id: updatedFile.id, - modificationTime: updatedFile.modificationTime, - name: updatedFile.name, - plainName: updatedFile.plainName, - size: updatedFile.size, - type: updatedFile.type ?? null, - updatedAt: updatedFile.updatedAt, - userId: updatedFile.userId, - status: updatedFile.status as ServerFileStatus, - uuid: updatedFile.uuid, - }; - }); + const files = updatedRemoteItems.files.map(this.mapFile); + + const folders = updatedRemoteItems.folders.map( + this.mapFolder + ); + + return { files, folders }; + } + + async getFolderItems( + folderId: number + ): Promise<{ files: ServerFile[]; folders: ServerFolder[] }> { + const updatedRemoteItems = await this.ipc.invoke( + 'GET_UPDATED_REMOTE_ITEMS_BY_FOLDER', + folderId + ); + + const files = updatedRemoteItems.files.map(this.mapFile); const folders = updatedRemoteItems.folders.map( - (updatedFolder) => { - return { - bucket: updatedFolder.bucket ?? null, - createdAt: updatedFolder.createdAt, - id: updatedFolder.id, - name: updatedFolder.name, - parentId: updatedFolder.parentId ?? null, - updatedAt: updatedFolder.updatedAt, - plain_name: updatedFolder.plainName ?? null, - status: updatedFolder.status as ServerFolderStatus, - uuid: updatedFolder.uuid, - }; - } + this.mapFolder ); return { files, folders }; diff --git a/src/context/virtual-drive/items/application/TreeProgresiveBuilder.ts b/src/context/virtual-drive/items/application/TreeProgresiveBuilder.ts new file mode 100644 index 000000000..7cbae72d3 --- /dev/null +++ b/src/context/virtual-drive/items/application/TreeProgresiveBuilder.ts @@ -0,0 +1,32 @@ +import { ServerFileStatus } from '../../../shared/domain/ServerFile'; +import { ServerFolderStatus } from '../../../shared/domain/ServerFolder'; +import { Tree } from '../domain/Tree'; +import { RemoteItemsGenerator } from './RemoteItemsGenerator'; +import { Traverser } from './Traverser'; + +export class TreeProgresiveBuilder { + constructor( + private readonly remoteItemsGenerator: RemoteItemsGenerator, + private readonly traverser: Traverser + ) {} + + public setFilterStatusesToFilter(statuses: Array): void { + this.traverser.setFileStatusesToFilter(statuses); + } + + public setFolderStatusesToFilter(statuses: Array): void { + this.traverser.setFolderStatusesToFilter(statuses); + } + // hacer un recorrido progresivo empezar por el root y luego ir a los folders hijos y ejecutar el traverser + + async run(): Promise { + // obtener los items del root + + // ejecutar el traverser + + // ejecutar el recursiveTraverse para cada folder hijo + const items = await this.remoteItemsGenerator.getAll(); + + return this.traverser.run(items); + } +} diff --git a/src/context/virtual-drive/userUsage/infrastrucutre/CachedHttpUserUsageRepository.ts b/src/context/virtual-drive/userUsage/infrastrucutre/CachedHttpUserUsageRepository.ts index 9e3b57083..7f5d1f222 100644 --- a/src/context/virtual-drive/userUsage/infrastrucutre/CachedHttpUserUsageRepository.ts +++ b/src/context/virtual-drive/userUsage/infrastrucutre/CachedHttpUserUsageRepository.ts @@ -14,7 +14,7 @@ export class CachedHttpUserUsageRepository implements UserUsageRepository { private async getDriveUsage(): Promise { const response = await this.driveClient.get( - `${process.env.API_URL}/api/usage` + `${process.env.API_URL}/usage` ); if (response.status !== 200) { @@ -26,7 +26,7 @@ export class CachedHttpUserUsageRepository implements UserUsageRepository { private async getLimit(): Promise { const response = await this.driveClient.get( - `${process.env.API_URL}/api/limit` + `${process.env.API_URL}/limit` ); if (response.status !== 200) {