From 900d69a60d7a61a584c88e9aff3a2b33ab1e09c2 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 9 May 2023 17:31:35 +0530 Subject: [PATCH] better handle export folder doesn't exist issues --- src/components/ExportModal.tsx | 16 ++- src/pages/_app.tsx | 11 +- src/services/export/index.ts | 179 ++++++++++++++++++++++--------- src/services/export/migration.ts | 9 +- src/types/electron/index.ts | 3 +- src/utils/error/index.ts | 2 +- 6 files changed, 148 insertions(+), 72 deletions(-) diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 962045978..f1efce28f 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -113,7 +113,7 @@ export default function ExportModal(props: Props) { // ======================= const verifyExportFolderExists = () => { - if (!exportFolder || !exportService.exists(exportFolder)) { + if (!exportService.exportFolderExists(exportFolder)) { appContext.setDialogMessage( getExportDirectoryDoesNotExistMessage() ); @@ -123,17 +123,25 @@ export default function ExportModal(props: Props) { const syncExportRecord = async (exportFolder: string): Promise => { try { + if (!exportService.exportFolderExists(exportFolder)) { + const fileExportStats = await exportService.getFileExportStats( + null + ); + setFileExportStats(fileExportStats); + } const exportRecord = await exportService.getExportRecord( exportFolder ); - setExportStage(exportRecord?.stage ?? ExportStage.INIT); - setLastExportTime(exportRecord?.lastAttemptTimestamp ?? 0); + setExportStage(exportRecord.stage); + setLastExportTime(exportRecord.lastAttemptTimestamp); const fileExportStats = await exportService.getFileExportStats( exportRecord ); setFileExportStats(fileExportStats); } catch (e) { - logError(e, 'syncExportRecord failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'syncExportRecord failed'); + } } }; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index cfbed0923..fff306284 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -257,17 +257,20 @@ export default function App(props) { try { addLogLine('init export'); const exportSettings = exportService.getExportSettings(); + if (!exportService.exportFolderExists(exportSettings?.folder)) { + return; + } const exportRecord = await exportService.getExportRecord( - exportSettings?.folder + exportSettings.folder ); await exportService.runMigration( - exportSettings?.folder, + exportSettings.folder, exportRecord ); - if (exportSettings?.continuousExport) { + if (exportSettings.continuousExport) { exportService.enableContinuousExport(); } - if (exportRecord?.stage === ExportStage.INPROGRESS) { + if (exportRecord.stage === ExportStage.INPROGRESS) { addLogLine('export was in progress, resuming'); exportService.scheduleExport(); } diff --git a/src/services/export/index.ts b/src/services/export/index.ts index e81d19738..a48a0716a 100644 --- a/src/services/export/index.ts +++ b/src/services/export/index.ts @@ -65,6 +65,14 @@ const EXPORT_RECORD_FILE_NAME = 'export_status.json'; export const ENTE_EXPORT_DIRECTORY = 'ente Photos'; +export const NULL_EXPORT_RECORD: ExportRecord = { + version: 3, + lastAttemptTimestamp: null, + stage: ExportStage.INIT, + fileExportNames: {}, + collectionExportNames: {}, +}; + class ExportService { private electronAPIs: ElectronAPIs; private exportInProgress: boolean = false; @@ -115,11 +123,13 @@ class ExportService { async runMigration(exportDir: string, exportRecord: ExportRecord) { try { + addLogLine('running migration'); this.migrationInProgress = migrateExportJSON( exportDir, exportRecord ); await this.migrationInProgress; + addLogLine('migration completed'); this.migrationInProgress = null; } catch (e) { logError(e, 'migration failed'); @@ -263,7 +273,8 @@ class ExportService { } }; - async preExport() { + async preExport(exportFolder: string) { + this.verifyExportFolderExists(exportFolder); this.stopExport = false; await this.updateExportStage(ExportStage.INPROGRESS); this.updateExportProgress({ @@ -274,14 +285,24 @@ class ExportService { } async postExport() { - await this.updateExportStage(ExportStage.FINISHED); - await this.updateLastExportTime(Date.now()); + try { + const exportSettings = this.getExportSettings(); + if (!this.exportFolderExists(exportSettings?.folder)) { + this.uiUpdater.setExportStage(ExportStage.INIT); + return; + } + await this.updateExportStage(ExportStage.FINISHED); + await this.updateLastExportTime(Date.now()); - const exportSettings = this.getExportSettings(); - const exportRecord = await this.getExportRecord(exportSettings?.folder); + const exportRecord = await this.getExportRecord( + exportSettings?.folder + ); - const fileExportStats = await this.getFileExportStats(exportRecord); - this.uiUpdater.setFileExportStats(fileExportStats); + const fileExportStats = await this.getFileExportStats(exportRecord); + this.uiUpdater.setFileExportStats(fileExportStats); + } catch (e) { + logError(e, 'postExport failed'); + } } async stopRunningExport() { @@ -310,9 +331,11 @@ class ExportService { this.migrationInProgress = null; } try { - await this.preExport(); + const exportSettings = this.getExportSettings(); + const exportFolder = exportSettings?.folder; + await this.preExport(exportFolder); addLogLine('export started'); - await this.runExport(); + await this.runExport(exportFolder); addLogLine('export completed'); } finally { this.exportInProgress = false; @@ -330,12 +353,8 @@ class ExportService { } }; - private async runExport() { + private async runExport(exportFolder: string) { try { - const exportSettings = this.getExportSettings(); - if (!exportSettings?.folder) { - throw new Error(CustomError.NO_EXPORT_FOLDER_SELECTED); - } const user: User = getData(LS_KEYS.USER); const files = mergeMetadata(await getLocalFiles()); const personalFiles = getPersonalFiles(files, user); @@ -347,9 +366,7 @@ class ExportService { user ); - const exportRecord = await this.getExportRecord( - exportSettings.folder - ); + const exportRecord = await this.getExportRecord(exportFolder); const collectionIDExportNameMap = convertCollectionIDExportNameObjectToMap( exportRecord.collectionExportNames @@ -412,7 +429,7 @@ class ExportService { if (renamedCollections?.length > 0) { addLogLine(`renaming ${renamedCollections.length} collections`); this.collectionRenamer( - exportSettings.folder, + exportFolder, collectionIDExportNameMap, renamedCollections, incrementSuccess, @@ -423,7 +440,7 @@ class ExportService { if (removedFileUIDs?.length > 0) { addLogLine(`trashing ${removedFileUIDs.length} files`); await this.fileTrasher( - exportSettings.folder, + exportFolder, collectionIDExportNameMap, removedFileUIDs, incrementSuccess, @@ -436,7 +453,7 @@ class ExportService { filesToExport, collectionIDNameMap, collectionIDExportNameMap, - exportSettings.folder, + exportFolder, incrementSuccess, incrementFailed ); @@ -447,13 +464,16 @@ class ExportService { ); await this.collectionRemover( deletedExportedCollections, - exportSettings.folder, + exportFolder, incrementSuccess, incrementFailed ); } } catch (e) { - logError(e, 'runExport failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'runExport failed'); + } + throw e; } } @@ -467,6 +487,7 @@ class ExportService { try { for (const collection of renamedCollections) { try { + this.verifyExportFolderExists(exportFolder); const oldCollectionExportName = collectionIDExportNameMap.get(collection.id); const oldCollectionExportPath = getCollectionExportPath( @@ -509,14 +530,17 @@ class ExportService { logError(e, 'collectionRenamer failed a collection'); if ( e.message === - CustomError.ADD_FILE_EXPORTED_RECORD_FAILED + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST ) { throw e; } } } } catch (e) { - logError(e, 'collectionRenamer failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'collectionRenamer failed'); + } throw e; } } @@ -535,6 +559,7 @@ class ExportService { ); for (const collectionID of deletedExportedCollectionIDs) { try { + this.verifyExportFolderExists(exportFolder); addLocalLog( () => `removing collection with id ${collectionID} from export folder` @@ -571,14 +596,17 @@ class ExportService { logError(e, 'collectionRemover failed a collection'); if ( e.message === - CustomError.ADD_FILE_EXPORTED_RECORD_FAILED + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST ) { throw e; } } } } catch (e) { - logError(e, 'collectionRemover failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'collectionRemover failed'); + } throw e; } } @@ -605,6 +633,7 @@ class ExportService { break; } try { + this.verifyExportFolderExists(exportDir); let collectionExportName = collectionIDFolderNameMap.get( file.collectionID ); @@ -624,19 +653,14 @@ class ExportService { file.collectionID, collectionExportName ); - } else { - const collectionExportPath = getCollectionExportPath( - exportDir, - collectionExportName - ); - await this.electronAPIs.checkExistsAndCreateDir( - collectionExportPath - ); } const collectionExportPath = getCollectionExportPath( exportDir, collectionExportName ); + await this.electronAPIs.checkExistsAndCreateDir( + collectionExportPath + ); const fileExportName = await this.downloadAndSave( collectionExportPath, file @@ -652,14 +676,17 @@ class ExportService { logError(e, 'export failed for a file'); if ( e.message === - CustomError.ADD_FILE_EXPORTED_RECORD_FAILED + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST ) { throw e; } } } } catch (e) { - logError(e, 'fileExporter failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'fileExporter failed'); + } throw e; } } @@ -677,6 +704,7 @@ class ExportService { exportRecord.fileExportNames ); for (const fileUID of removedFileUIDs) { + this.verifyExportFolderExists(exportDir); addLocalLog(() => `trashing file with id ${fileUID}`); if (this.stopExport) { break; @@ -773,14 +801,17 @@ class ExportService { logError(e, 'trashing failed for a file'); if ( e.message === - CustomError.ADD_FILE_EXPORTED_RECORD_FAILED + CustomError.UPDATE_EXPORTED_RECORD_FAILED || + e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST ) { throw e; } } } } catch (e) { - logError(e, 'fileTrasher failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'fileTrasher failed'); + } throw e; } } @@ -802,8 +833,10 @@ class ExportService { }; await this.updateExportRecord(exportRecord, folder); } catch (e) { - logError(e, 'addFileExportedRecord failed'); - throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'addFileExportedRecord failed'); + } + throw e; } } @@ -824,8 +857,10 @@ class ExportService { await this.updateExportRecord(exportRecord, folder); } catch (e) { - logError(e, 'addCollectionExportedRecord failed'); - throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'addCollectionExportedRecord failed'); + } + throw e; } } @@ -841,8 +876,10 @@ class ExportService { await this.updateExportRecord(exportRecord, folder); } catch (e) { - logError(e, 'removeCollectionExportedRecord failed'); - throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'removeCollectionExportedRecord failed'); + } + throw e; } } @@ -856,8 +893,10 @@ class ExportService { ); await this.updateExportRecord(exportRecord, folder); } catch (e) { - logError(e, 'removeFileExportedRecord failed'); - throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'removeFileExportedRecord failed'); + } + throw e; } } @@ -878,28 +917,35 @@ class ExportService { } const exportRecord = await this.getExportRecord(folder); const newRecord: ExportRecord = { ...exportRecord, ...newData }; - await this.electronAPIs.setExportRecord( + await this.electronAPIs.saveFileToDisk( `${folder}/${EXPORT_RECORD_FILE_NAME}`, JSON.stringify(newRecord, null, 2) ); return newRecord; } catch (e) { + if (e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + throw e; + } logError(e, 'error updating Export Record'); - throw e; + throw Error(CustomError.UPDATE_EXPORTED_RECORD_FAILED); } } async getExportRecord(folder: string): Promise { try { - if (!folder || !this.exists(folder)) { - return null; + this.verifyExportFolderExists(folder); + const exportRecordJSONPath = `${folder}/${EXPORT_RECORD_FILE_NAME}`; + if (!this.exists(exportRecordJSONPath)) { + return this.createEmptyExportRecord(folder); } - const recordFile = await this.electronAPIs.getExportRecord( - `${folder}/${EXPORT_RECORD_FILE_NAME}` + const recordFile = await this.electronAPIs.readTextFile( + exportRecordJSONPath ); return JSON.parse(recordFile); } catch (e) { - logError(e, 'export Record JSON parsing failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'export Record JSON parsing failed'); + } throw e; } } @@ -909,6 +955,7 @@ class ExportService { collectionID: number, collectionIDNameMap: Map ) { + this.verifyExportFolderExists(exportFolder); const collectionName = collectionIDNameMap.get(collectionID); const collectionExportName = getUniqueCollectionExportName( exportFolder, @@ -1057,5 +1104,31 @@ class ExportService { checkExistsAndCreateDir = (path: string) => { return this.electronAPIs.checkExistsAndCreateDir(path); }; + + exportFolderExists = (exportFolder: string) => { + return exportFolder && this.exists(exportFolder); + }; + + private verifyExportFolderExists = (exportFolder: string) => { + try { + if (!this.exportFolderExists(exportFolder)) { + throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); + } + } catch (e) { + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'verifyExportFolderExists failed'); + } + throw e; + } + }; + + private createEmptyExportRecord = async (exportFolder: string) => { + const exportRecord: ExportRecord = NULL_EXPORT_RECORD; + await this.electronAPIs.saveFileToDisk( + `${exportFolder}/${EXPORT_RECORD_FILE_NAME}`, + JSON.stringify(exportRecord, null, 2) + ); + return exportRecord; + }; } export default new ExportService(); diff --git a/src/services/export/migration.ts b/src/services/export/migration.ts index b82468f1b..56e707e8c 100644 --- a/src/services/export/migration.ts +++ b/src/services/export/migration.ts @@ -38,7 +38,6 @@ import { FILE_TYPE } from 'constants/file'; import { decodeLivePhoto } from 'services/livePhotoService'; import downloadManager from 'services/downloadManager'; import { retryAsyncFunction } from 'utils/network'; -import { CustomError } from 'utils/error'; export async function migrateExportJSON( exportDir: string, @@ -67,12 +66,6 @@ async function migrateExport( exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord ) { try { - if (!exportRecord?.version) { - exportRecord = { - ...exportRecord, - version: 0, - }; - } addLogLine(`current export version: ${exportRecord.version}`); if (exportRecord.version === 0) { addLogLine('migrating export to version 1'); @@ -371,6 +364,6 @@ async function addCollectionExportedRecordV1( await exportService.updateExportRecord(exportRecord, folder); } catch (e) { logError(e, 'addCollectionExportedRecord failed'); - throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); + throw e; } } diff --git a/src/types/electron/index.ts b/src/types/electron/index.ts index 9166b05d0..ee59fe8cc 100644 --- a/src/types/electron/index.ts +++ b/src/types/electron/index.ts @@ -17,8 +17,7 @@ export interface ElectronAPIs { saveFileToDisk: (path: string, file: any) => Promise; selectRootDirectory: () => Promise; sendNotification: (content: string) => void; - getExportRecord: (filePath: string) => Promise; - setExportRecord: (filePath: string, data: string) => Promise; + readTextFile: (path: string) => Promise; showUploadFilesDialog: () => Promise; showUploadDirsDialog: () => Promise; getPendingUploads: () => Promise<{ diff --git a/src/utils/error/index.ts b/src/utils/error/index.ts index 0c3b77deb..0228797b9 100644 --- a/src/utils/error/index.ts +++ b/src/utils/error/index.ts @@ -55,7 +55,7 @@ export const CustomError = { 'Windows native image processing is not supported', NETWORK_ERROR: 'Network Error', NOT_FILE_OWNER: 'not file owner', - ADD_FILE_EXPORTED_RECORD_FAILED: 'add file exported record failed', + UPDATE_EXPORTED_RECORD_FAILED: 'update file exported record failed', NO_EXPORT_FOLDER_SELECTED: 'no export folder selected', EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist', NO_INTERNET_CONNECTION: 'no internet connection',