diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 0450b43fe..ef336534d 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -70,14 +70,24 @@ export default function ExportModal(props: Props) { if (!isElectron()) { return; } - setExportFolder(getData(LS_KEYS.EXPORT)?.folder); + try { + setExportFolder(getData(LS_KEYS.EXPORT)?.folder); - exportService.electronAPIs.registerStopExportListener(stopExport); - exportService.electronAPIs.registerPauseExportListener(pauseExport); - exportService.electronAPIs.registerResumeExportListener(resumeExport); - exportService.electronAPIs.registerRetryFailedExportListener( - retryFailedExport - ); + exportService.electronAPIs.registerStopExportListener( + stopExportHandler + ); + exportService.electronAPIs.registerPauseExportListener( + pauseExportHandler + ); + exportService.electronAPIs.registerResumeExportListener( + resumeExportHandler + ); + exportService.electronAPIs.registerRetryFailedExportListener( + retryFailedExportHandler + ); + } catch (e) { + logError(e, 'error in exportModal'); + } }, []); useEffect(() => { @@ -85,19 +95,25 @@ export default function ExportModal(props: Props) { return; } const main = async () => { - const exportInfo = await exportService.getExportRecord(); - setExportStage(exportInfo?.stage ?? ExportStage.INIT); - setLastExportTime(exportInfo?.lastAttemptTimestamp); - setExportProgress(exportInfo?.progress ?? { current: 0, total: 0 }); - setExportStats({ - success: exportInfo?.exportedFiles?.length ?? 0, - failed: exportInfo?.failedFiles?.length ?? 0, - }); - if (exportInfo?.stage === ExportStage.INPROGRESS) { - resumeExport(); + try { + const exportInfo = await exportService.getExportRecord(); + setExportStage(exportInfo?.stage ?? ExportStage.INIT); + setLastExportTime(exportInfo?.lastAttemptTimestamp); + setExportProgress( + exportInfo?.progress ?? { current: 0, total: 0 } + ); + setExportStats({ + success: exportInfo?.exportedFiles?.length ?? 0, + failed: exportInfo?.failedFiles?.length ?? 0, + }); + if (exportInfo?.stage === ExportStage.INPROGRESS) { + await resumeExport(); + } + } catch (e) { + logError(e, 'error handling exportFolder change'); } }; - main(); + void main(); }, [exportFolder]); useEffect(() => { @@ -117,7 +133,7 @@ export default function ExportModal(props: Props) { const failedFilesCnt = exportRecord.failedFiles?.length; const syncedFilesCnt = userPersonalFiles.length; if (syncedFilesCnt > exportedFileCnt + failedFilesCnt) { - updateExportProgress({ + await updateExportProgress({ current: exportedFileCnt + failedFilesCnt, total: syncedFilesCnt, }); @@ -131,11 +147,11 @@ export default function ExportModal(props: Props) { getExportRecordFileUID(file) ) ); - exportService.addFilesQueuedRecord( + await exportService.addFilesQueuedRecord( exportFolder, unExportedFiles ); - updateExportStage(ExportStage.PAUSED); + await updateExportStage(ExportStage.PAUSED); } } catch (e) { setExportStage(ExportStage.INIT); @@ -143,7 +159,7 @@ export default function ExportModal(props: Props) { } } }; - main(); + void main(); }, [props.show]); useEffect(() => { @@ -158,19 +174,21 @@ export default function ExportModal(props: Props) { setData(LS_KEYS.EXPORT, { folder: newFolder }); }; - const updateExportStage = (newStage: ExportStage) => { + const updateExportStage = async (newStage: ExportStage) => { setExportStage(newStage); - exportService.updateExportRecord({ stage: newStage }); + await exportService.updateExportRecord({ stage: newStage }); }; - const updateExportTime = (newTime: number) => { + const updateExportTime = async (newTime: number) => { setLastExportTime(newTime); - exportService.updateExportRecord({ lastAttemptTimestamp: newTime }); + await exportService.updateExportRecord({ + lastAttemptTimestamp: newTime, + }); }; - const updateExportProgress = (newProgress: ExportProgress) => { + const updateExportProgress = async (newProgress: ExportProgress) => { setExportProgress(newProgress); - exportService.updateExportRecord({ progress: newProgress }); + await exportService.updateExportRecord({ progress: newProgress }); }; // ====================== @@ -182,15 +200,15 @@ export default function ExportModal(props: Props) { if (!exportFolder) { await selectExportDirectory(); } - updateExportStage(ExportStage.INPROGRESS); + await updateExportStage(ExportStage.INPROGRESS); await sleep(100); }; const postExportRun = async (exportResult?: { paused?: boolean }) => { if (!exportResult?.paused) { - updateExportStage(ExportStage.FINISHED); + await updateExportStage(ExportStage.FINISHED); await sleep(100); - updateExportTime(Date.now()); - syncExportStatsWithRecord(); + await updateExportTime(Date.now()); + await syncExportStatsWithRecord(); } }; @@ -217,7 +235,7 @@ export default function ExportModal(props: Props) { const startExport = async () => { try { await preExportRun(); - updateExportProgress({ current: 0, total: 0 }); + await updateExportProgress({ current: 0, total: 0 }); const exportResult = await exportService.exportFiles( updateExportProgress, ExportType.NEW @@ -233,7 +251,7 @@ export default function ExportModal(props: Props) { const stopExport = async () => { try { exportService.stopRunningExport(); - postExportRun(); + await postExportRun(); } catch (e) { if (e.message !== CustomError.REQUEST_CANCELLED) { logError(e, 'stopExport failed'); @@ -241,11 +259,11 @@ export default function ExportModal(props: Props) { } }; - const pauseExport = () => { + const pauseExport = async () => { try { - updateExportStage(ExportStage.PAUSED); + await updateExportStage(ExportStage.PAUSED); exportService.pauseRunningExport(); - postExportRun({ paused: true }); + await postExportRun({ paused: true }); } catch (e) { if (e.message !== CustomError.REQUEST_CANCELLED) { logError(e, 'pauseExport failed'); @@ -282,7 +300,10 @@ export default function ExportModal(props: Props) { const retryFailedExport = async () => { try { await preExportRun(); - updateExportProgress({ current: 0, total: exportStats.failed }); + await updateExportProgress({ + current: 0, + total: exportStats.failed, + }); const exportResult = await exportService.exportFiles( updateExportProgress, @@ -296,10 +317,26 @@ export default function ExportModal(props: Props) { } }; + const startExportHandler = () => { + void startExport(); + }; + const stopExportHandler = () => { + void stopExport(); + }; + const pauseExportHandler = () => { + void pauseExport(); + }; + const resumeExportHandler = () => { + void resumeExport(); + }; + const retryFailedExportHandler = () => { + void retryFailedExport(); + }; + const ExportDynamicContent = () => { switch (exportStage) { case ExportStage.INIT: - return ; + return ; case ExportStage.INPROGRESS: case ExportStage.PAUSED: @@ -307,9 +344,9 @@ export default function ExportModal(props: Props) { ); case ExportStage.FINISHED: @@ -318,8 +355,8 @@ export default function ExportModal(props: Props) { onHide={props.onHide} lastExportTime={lastExportTime} exportStats={exportStats} - exportFiles={startExport} - retryFailed={retryFailedExport} + exportFiles={startExportHandler} + retryFailed={retryFailedExportHandler} /> ); diff --git a/src/services/exportService.ts b/src/services/exportService.ts index 68a905dc6..87b6339a3 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -49,6 +49,7 @@ import { User } from 'types/user'; import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file'; import { ExportType, ExportNotification, RecordType } from 'constants/export'; import { ElectronAPIs } from 'types/electron'; +import { CustomError } from 'utils/error'; const LATEST_EXPORT_VERSION = 1; const EXPORT_RECORD_FILE_NAME = 'export_status.json'; @@ -83,10 +84,11 @@ class ExportService { this.pauseExport = true; } async exportFiles( - updateProgress: (progress: ExportProgress) => void, + updateProgress: (progress: ExportProgress) => Promise, exportType: ExportType ) { try { + // eslint-disable-next-line @typescript-eslint/no-misused-promises if (this.exportInProgress) { this.electronAPIs.sendNotification( ExportNotification.IN_PROGRESS @@ -177,7 +179,7 @@ class ExportService { newCollections: Collection[], renamedCollections: Collection[], collectionIDPathMap: CollectionIDPathMap, - updateProgress: (progress: ExportProgress) => void, + updateProgress: (progress: ExportProgress) => Promise, exportDir: string ): Promise<{ paused: boolean }> { try { @@ -212,7 +214,7 @@ class ExportService { this.electronAPIs.showOnTray({ export_progress: `0 / ${files.length} files exported`, }); - updateProgress({ + await updateProgress({ current: 0, total: files.length, }); @@ -239,23 +241,28 @@ class ExportService { RecordType.SUCCESS ); } catch (e) { + logError(e, 'export failed for a file'); + if ( + e.message === + CustomError.ADD_FILE_EXPORTED_RECORD_FAILED + ) { + throw e; + } await this.addFileExportedRecord( exportDir, file, RecordType.FAILED ); - - logError( - e, - 'download and save failed for file during export' - ); } this.electronAPIs.showOnTray({ export_progress: `${index + 1} / ${ files.length } files exported`, }); - updateProgress({ current: index + 1, total: files.length }); + await updateProgress({ + current: index + 1, + total: files.length, + }); } if (this.stopExport) { this.electronAPIs.sendNotification(ExportNotification.ABORT); @@ -266,7 +273,7 @@ class ExportService { } else if (failedFileCount > 0) { this.electronAPIs.sendNotification(ExportNotification.FAILED); this.electronAPIs.showOnTray({ - retry_export: `export failed - retry export`, + retry_export: `Retry failed exports`, }); } else { this.electronAPIs.sendNotification(ExportNotification.FINISH); @@ -289,32 +296,37 @@ class ExportService { file: EnteFile, type: RecordType ) { - const fileUID = getExportRecordFileUID(file); - const exportRecord = await this.getExportRecord(folder); - exportRecord.queuedFiles = exportRecord.queuedFiles.filter( - (queuedFilesUID) => queuedFilesUID !== fileUID - ); - if (type === RecordType.SUCCESS) { - if (!exportRecord.exportedFiles) { - exportRecord.exportedFiles = []; - } - exportRecord.exportedFiles.push(fileUID); - exportRecord.failedFiles && - (exportRecord.failedFiles = exportRecord.failedFiles.filter( - (FailedFileUID) => FailedFileUID !== fileUID - )); - } else { - if (!exportRecord.failedFiles) { - exportRecord.failedFiles = []; - } - if (!exportRecord.failedFiles.find((x) => x === fileUID)) { - exportRecord.failedFiles.push(fileUID); + try { + const fileUID = getExportRecordFileUID(file); + const exportRecord = await this.getExportRecord(folder); + exportRecord.queuedFiles = exportRecord.queuedFiles.filter( + (queuedFilesUID) => queuedFilesUID !== fileUID + ); + if (type === RecordType.SUCCESS) { + if (!exportRecord.exportedFiles) { + exportRecord.exportedFiles = []; + } + exportRecord.exportedFiles.push(fileUID); + exportRecord.failedFiles && + (exportRecord.failedFiles = exportRecord.failedFiles.filter( + (FailedFileUID) => FailedFileUID !== fileUID + )); + } else { + if (!exportRecord.failedFiles) { + exportRecord.failedFiles = []; + } + if (!exportRecord.failedFiles.find((x) => x === fileUID)) { + exportRecord.failedFiles.push(fileUID); + } } + exportRecord.exportedFiles = dedupe(exportRecord.exportedFiles); + exportRecord.queuedFiles = dedupe(exportRecord.queuedFiles); + exportRecord.failedFiles = dedupe(exportRecord.failedFiles); + await this.updateExportRecord(exportRecord, folder); + } catch (e) { + logError(e, 'addFileExportedRecord failed'); + throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); } - exportRecord.exportedFiles = dedupe(exportRecord.exportedFiles); - exportRecord.queuedFiles = dedupe(exportRecord.queuedFiles); - exportRecord.failedFiles = dedupe(exportRecord.failedFiles); - await this.updateExportRecord(exportRecord, folder); } async addCollectionExportedRecord( @@ -354,6 +366,7 @@ class ExportService { ); } catch (e) { logError(e, 'error updating Export Record'); + throw e; } } @@ -372,6 +385,7 @@ class ExportService { } } catch (e) { logError(e, 'export Record JSON parsing failed '); + throw e; } } @@ -428,36 +442,45 @@ class ExportService { } async downloadAndSave(file: EnteFile, collectionPath: string) { - file.metadata = mergeMetadata([file])[0].metadata; - const fileSaveName = getUniqueFileSaveName( - collectionPath, - file.metadata.title, - file.id - ); - let fileStream = await retryAsyncFunction(() => - downloadManager.downloadFile(file) - ); - const fileType = getFileExtension(file.metadata.title); - if ( - file.pubMagicMetadata?.data.editedTime && - (fileType === TYPE_JPEG || fileType === TYPE_JPG) - ) { - const fileBlob = await new Response(fileStream).blob(); - if (!this.fileReader) { - this.fileReader = new FileReader(); - } - const updatedFileBlob = await updateFileCreationDateInEXIF( - this.fileReader, - fileBlob, - new Date(file.pubMagicMetadata.data.editedTime / 1000) + try { + file.metadata = mergeMetadata([file])[0].metadata; + const fileSaveName = getUniqueFileSaveName( + collectionPath, + file.metadata.title, + file.id ); - fileStream = updatedFileBlob.stream(); - } - if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - await this.exportMotionPhoto(fileStream, file, collectionPath); - } else { - await this.saveMediaFile(collectionPath, fileSaveName, fileStream); - await this.saveMetadataFile(collectionPath, fileSaveName, file); + let fileStream = await retryAsyncFunction(() => + downloadManager.downloadFile(file) + ); + const fileType = getFileExtension(file.metadata.title); + if ( + file.pubMagicMetadata?.data.editedTime && + (fileType === TYPE_JPEG || fileType === TYPE_JPG) + ) { + const fileBlob = await new Response(fileStream).blob(); + if (!this.fileReader) { + this.fileReader = new FileReader(); + } + const updatedFileBlob = await updateFileCreationDateInEXIF( + this.fileReader, + fileBlob, + new Date(file.pubMagicMetadata.data.editedTime / 1000) + ); + fileStream = updatedFileBlob.stream(); + } + if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { + await this.exportMotionPhoto(fileStream, file, collectionPath); + } else { + await this.saveMediaFile( + collectionPath, + fileSaveName, + fileStream + ); + await this.saveMetadataFile(collectionPath, fileSaveName, file); + } + } catch (e) { + logError(e, 'download and save failed'); + throw e; } } diff --git a/src/utils/error/index.ts b/src/utils/error/index.ts index 557d7c7f2..a4bf9a7c2 100644 --- a/src/utils/error/index.ts +++ b/src/utils/error/index.ts @@ -55,6 +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', }; export function parseUploadErrorCodes(error) {