diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5427a6e71..f3656c80d 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -300,7 +300,6 @@ "EXPORT_DATA": "Export data", "SELECT_FOLDER": "Select folder", "DESTINATION": "Destination", - "TOTAL_FILE_COUNT": "Total file count", "START": "Start", "EXPORT_IN_PROGRESS": "Export in progress...", "PAUSE": "Pause", @@ -577,5 +576,8 @@ "FINISH": "Export finished", "UP_TO_DATE": "No new files to export" }, - "CONTINUOUS_EXPORT": "Sync continuously" + "CONTINUOUS_EXPORT": "Sync continuously", + "TOTAL_ITEMS": "Total items", + "PENDING_ITEMS": "Pending items", + "EXPORT_STARTING": "Export starting..." } \ No newline at end of file diff --git a/src/components/ExportFinished.tsx b/src/components/ExportFinished.tsx index 74ea25d66..c458e2b60 100644 --- a/src/components/ExportFinished.tsx +++ b/src/components/ExportFinished.tsx @@ -7,14 +7,13 @@ import { } from '@mui/material'; import React from 'react'; import { t } from 'i18next'; -import { ExportStats } from 'types/export'; import { formatDateTime } from 'utils/time/format'; import { SpaceBetweenFlex } from './Container'; interface Props { + pendingFileCount: number; onHide: () => void; lastExportTime: number; - exportStats: ExportStats; startExport: () => void; } @@ -22,8 +21,14 @@ export default function ExportFinished(props: Props) { return ( <> - - + + + + {t('PENDING_ITEMS')} + + {props.pendingFileCount} + + {t('LAST_EXPORT_TIME')} @@ -31,20 +36,6 @@ export default function ExportFinished(props: Props) { {formatDateTime(props.lastExportTime)} - - - {t('SUCCESSFULLY_EXPORTED_FILES')} - - {props.exportStats.success} - - {props.exportStats.failed > 0 && ( - - - {t('FAILED_EXPORTED_FILES')} - - {props.exportStats.failed} - - )} diff --git a/src/components/ExportInProgress.tsx b/src/components/ExportInProgress.tsx index 938515c6b..e71504bbb 100644 --- a/src/components/ExportInProgress.tsx +++ b/src/components/ExportInProgress.tsx @@ -27,28 +27,37 @@ interface Props { } export default function ExportInProgress(props: Props) { + const isLoading = props.exportProgress.total === 0; return ( <> - , - }} - values={{ - progress: props.exportProgress, - }} - /> + {isLoading ? ( + t('EXPORT_STARTING') + ) : ( + , + }} + values={{ + progress: props.exportProgress, + }} + /> + )} diff --git a/src/components/ExportModal.tsx b/src/components/ExportModal.tsx index 5e4f2485d..47a9b76b0 100644 --- a/src/components/ExportModal.tsx +++ b/src/components/ExportModal.tsx @@ -1,7 +1,7 @@ import isElectron from 'is-electron'; import React, { useEffect, useState, useContext } from 'react'; import exportService from 'services/exportService'; -import { ExportProgress, ExportSettings, ExportStats } from 'types/export'; +import { ExportProgress, ExportSettings, FileExportStats } from 'types/export'; import { Box, Button, @@ -28,9 +28,8 @@ import { OverflowMenuOption } from './OverflowMenu/option'; import { AppContext } from 'pages/_app'; import { getExportDirectoryDoesNotExistMessage } from 'utils/ui'; import { t } from 'i18next'; -import { getTotalFileCount } from 'utils/file'; -import { eventBus, Events } from 'services/events'; import LinkButton from './pages/gallery/LinkButton'; +import { CustomError } from 'utils/error'; const ExportFolderPathContainer = styled(LinkButton)` width: 262px; @@ -51,14 +50,13 @@ export default function ExportModal(props: Props) { const [exportStage, setExportStage] = useState(ExportStage.INIT); const [exportFolder, setExportFolder] = useState(''); const [continuousExport, setContinuousExport] = useState(false); - const [totalFileCount, setTotalFileCount] = useState(0); const [exportProgress, setExportProgress] = useState({ current: 0, total: 0, }); - const [exportStats, setExportStats] = useState({ - failed: 0, - success: 0, + const [fileExportStats, setFileExportStats] = useState({ + totalCount: 0, + pendingCount: 0, }); const [lastExportTime, setLastExportTime] = useState(0); @@ -73,16 +71,19 @@ export default function ExportModal(props: Props) { const exportSettings: ExportSettings = getData(LS_KEYS.EXPORT); setExportFolder(exportSettings?.folder); setContinuousExport(exportSettings?.continuousExport); - const localFileUpdateHandler = async () => { - setTotalFileCount(await getTotalFileCount()); - }; - localFileUpdateHandler(); - eventBus.on(Events.LOCAL_FILES_UPDATED, localFileUpdateHandler); + syncFileCounts(); } catch (e) { logError(e, 'error in exportModal'); } }, []); + useEffect(() => { + if (!props.show) { + return; + } + syncFileCounts(); + }, [props.show]); + useEffect(() => { try { if (continuousExport) { @@ -102,12 +103,13 @@ export default function ExportModal(props: Props) { const main = async () => { try { const exportInfo = await exportService.getExportRecord(); - setExportStage(exportInfo?.stage ?? ExportStage.INIT); - setLastExportTime(exportInfo?.lastAttemptTimestamp); - setExportStats({ - success: exportInfo?.exportedFiles?.length ?? 0, - failed: exportInfo?.failedFiles?.length ?? 0, - }); + if (exportInfo?.stage) { + setExportStage(exportInfo?.stage); + } + if (exportInfo?.lastAttemptTimestamp) { + setLastExportTime(exportInfo?.lastAttemptTimestamp); + } + await syncFileCounts(); if (exportInfo?.stage === ExportStage.INPROGRESS) { await startExport(); } @@ -155,7 +157,7 @@ export default function ExportModal(props: Props) { // ====================== // HELPER FUNCTIONS - // ========================= + // ======================= const preExportRun = async () => { const exportFolder = getData(LS_KEYS.EXPORT)?.folder; @@ -164,7 +166,7 @@ export default function ExportModal(props: Props) { appContext.setDialogMessage( getExportDirectoryDoesNotExistMessage() ); - return; + throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); } await updateExportStage(ExportStage.INPROGRESS); }; @@ -172,14 +174,16 @@ export default function ExportModal(props: Props) { const postExportRun = async () => { await updateExportStage(ExportStage.FINISHED); await updateExportTime(Date.now()); - await syncExportStatsWithRecord(); + await syncFileCounts(); }; - const syncExportStatsWithRecord = async () => { - const exportRecord = await exportService.getExportRecord(); - const failed = exportRecord?.failedFiles?.length ?? 0; - const success = exportRecord?.exportedFiles?.length ?? 0; - setExportStats({ failed, success }); + const syncFileCounts = async () => { + try { + const fileExportStats = await exportService.getFileExportStats(); + setFileExportStats(fileExportStats); + } catch (e) { + logError(e, 'error updating file counts'); + } }; // ============= @@ -205,24 +209,13 @@ export default function ExportModal(props: Props) { const startExport = async () => { try { await preExportRun(); - const exportRecord = await exportService.getExportRecord(); - const totalFileCount = await getTotalFileCount(); - const exportedFileCount = exportRecord.exportedFiles?.length ?? 0; - setExportProgress({ - current: exportedFileCount, - total: totalFileCount, - }); - - const updateExportStatsWithOffset = (current: number) => - setExportProgress({ - current: exportedFileCount + current, - total: totalFileCount, - }); - await exportService.exportFiles(updateExportStatsWithOffset); - + setExportProgress({ current: 0, total: 0 }); + await exportService.exportFiles(setExportProgress); await postExportRun(); } catch (e) { - logError(e, 'startExport failed'); + if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { + logError(e, 'startExport failed'); + } } }; @@ -235,35 +228,6 @@ export default function ExportModal(props: Props) { } }; - const ExportDynamicContent = () => { - switch (exportStage) { - case ExportStage.INIT: - return ; - - case ExportStage.INPROGRESS: - return ( - - ); - case ExportStage.FINISHED: - return ( - - ); - - default: - return <>; - } - }; - return ( @@ -276,14 +240,29 @@ export default function ExportModal(props: Props) { exportStage={exportStage} openExportDirectory={handleOpenExportDirectoryClick} /> - + + + {t('TOTAL_ITEMS')} + + + {fileExportStats.totalCount} + + - + ); } @@ -328,17 +307,6 @@ function ExportDirectory({ ); } -function TotalFileCount({ totalFileCount }) { - return ( - - - {t('TOTAL_FILE_COUNT')}{' '} - - {totalFileCount} - - ); -} - function ExportDirectoryOption({ changeExportDirectory }) { return ( + {t('CONTINUOUS_EXPORT')} @@ -374,3 +342,48 @@ function ContinuousExport({ continuousExport, toggleContinuousExport }) { ); } + +const ExportDynamicContent = ({ + exportStage, + startExport, + stopExport, + onHide, + lastExportTime, + pendingFileCount, + exportProgress, +}: { + exportStage: ExportStage; + startExport: () => void; + stopExport: () => void; + onHide: () => void; + lastExportTime: number; + pendingFileCount: number; + exportProgress: ExportProgress; +}) => { + switch (exportStage) { + case ExportStage.INIT: + return ; + + case ExportStage.INPROGRESS: + return ( + + ); + case ExportStage.FINISHED: + return ( + + ); + + default: + return <>; + } +}; diff --git a/src/services/exportService.ts b/src/services/exportService.ts index bd98aa99f..36f7a6ea2 100644 --- a/src/services/exportService.ts +++ b/src/services/exportService.ts @@ -31,6 +31,7 @@ import { decodeMotionPhoto } from './motionPhotoService'; import { generateStreamFromArrayBuffer, getFileExtension, + getPersonalFiles, mergeMetadata, } from 'utils/file'; @@ -40,8 +41,10 @@ import { Collection } from 'types/collection'; import { CollectionIDNameMap, CollectionIDPathMap, + ExportProgress, ExportRecord, ExportRecordV1, + FileExportStats, } from 'types/export'; import { User } from 'types/user'; import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file'; @@ -64,7 +67,7 @@ class ExportService { private stopExport: boolean = false; private allElectronAPIsExist: boolean = false; private fileReader: FileReader = null; - private continuousExportEventListener: () => void; + private continuousExportEventHandler: () => void; constructor() { this.electronAPIs = runningInBrowser() && window['ElectronAPIs']; @@ -93,24 +96,35 @@ class ExportService { } } - enableContinuousExport(startExport: () => void) { + enableContinuousExport(startExport: () => Promise) { try { - if (this.continuousExportEventListener) { + if (this.continuousExportEventHandler) { addLogLine('continuous export already enabled'); return; } - startExport(); - this.continuousExportEventListener = () => { - addLogLine('continuous export triggered'); - if (this.exportInProgress) { - addLogLine('export in progress, skipping'); - return; + const reRunNeeded = { current: false }; + this.continuousExportEventHandler = async () => { + try { + addLogLine('continuous export triggered'); + if (this.exportInProgress) { + addLogLine('export in progress, scheduling re-run'); + reRunNeeded.current = true; + return; + } + await startExport(); + if (reRunNeeded.current) { + reRunNeeded.current = false; + addLogLine('re-running export'); + setTimeout(this.continuousExportEventHandler, 0); + } + } catch (e) { + logError(e, 'continuous export failed'); } - startExport(); }; + this.continuousExportEventHandler(); eventBus.addListener( Events.LOCAL_FILES_UPDATED, - this.continuousExportEventListener + this.continuousExportEventHandler ); } catch (e) { logError(e, 'failed to enableContinuousExport '); @@ -120,26 +134,44 @@ class ExportService { disableContinuousExport() { try { - if (!this.continuousExportEventListener) { + if (!this.continuousExportEventHandler) { addLogLine('continuous export already disabled'); return; } eventBus.removeListener( Events.LOCAL_FILES_UPDATED, - this.continuousExportEventListener + this.continuousExportEventHandler ); - this.continuousExportEventListener = null; + this.continuousExportEventHandler = null; } catch (e) { logError(e, 'failed to disableContinuousExport'); throw e; } } + getFileExportStats = async (): Promise => { + try { + const exportRecord = await this.getExportRecord(); + const userPersonalFiles = await getPersonalFiles(); + const unExportedFiles = getUnExportedFiles( + userPersonalFiles, + exportRecord + ); + return { + totalCount: userPersonalFiles.length, + pendingCount: unExportedFiles.length, + }; + } catch (e) { + logError(e, 'getUpdateFileLists failed'); + throw e; + } + }; + stopRunningExport() { this.stopExport = true; } - async exportFiles(updateProgress: (current: number) => void) { + async exportFiles(updateProgress: (progress: ExportProgress) => void) { try { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (this.exportInProgress) { @@ -219,7 +251,7 @@ class ExportService { collectionIDNameMap: CollectionIDNameMap, renamedCollections: Collection[], collectionIDPathMap: CollectionIDPathMap, - updateProgress: (current: number) => void, + updateProgress: (progress: ExportProgress) => void, exportDir: string ): Promise { try { @@ -241,8 +273,8 @@ class ExportService { } this.stopExport = false; this.electronAPIs.sendNotification(t('EXPORT_NOTIFICATION.START')); - - for (const [index, file] of files.entries()) { + let success = 0; + for (const file of files) { if (this.stopExport) { break; } @@ -264,6 +296,8 @@ class ExportService { file, RecordType.SUCCESS ); + success++; + updateProgress({ current: success, total: files.length }); } catch (e) { logError(e, 'export failed for a file'); if ( @@ -278,7 +312,6 @@ class ExportService { RecordType.FAILED ); } - updateProgress(index + 1); } if (!this.stopExport) { this.electronAPIs.sendNotification( @@ -304,20 +337,8 @@ class ExportService { 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.failedFiles = dedupe(exportRecord.failedFiles); await this.updateExportRecord(exportRecord, folder); } catch (e) { logError(e, 'addFileExportedRecord failed'); @@ -375,20 +396,16 @@ class ExportService { folder = getData(LS_KEYS.EXPORT)?.folder; } if (!folder) { - throw Error(CustomError.NO_EXPORT_FOLDER_SELECTED); + return null; } const exportFolderExists = this.exists(folder); if (!exportFolderExists) { - throw Error(CustomError.EXPORT_FOLDER_DOES_NOT_EXIST); + return null; } const recordFile = await this.electronAPIs.getExportRecord( `${folder}/${EXPORT_RECORD_FILE_NAME}` ); - if (recordFile) { - return JSON.parse(recordFile); - } else { - return {} as ExportRecord; - } + return JSON.parse(recordFile); } catch (e) { logError(e, 'export Record JSON parsing failed '); throw e; @@ -675,6 +692,9 @@ class ExportService { if (exportRecord?.progress) { exportRecord.progress = undefined; } + if (exportRecord?.failedFiles) { + exportRecord.failedFiles = undefined; + } await this.updateExportRecord(exportRecord); } } diff --git a/src/types/export/index.ts b/src/types/export/index.ts index 724788574..b607e0c77 100644 --- a/src/types/export/index.ts +++ b/src/types/export/index.ts @@ -9,9 +9,9 @@ export interface ExportProgress { export interface ExportedCollectionPaths { [collectionID: number]: string; } -export interface ExportStats { - failed: number; - success: number; +export interface FileExportStats { + totalCount: number; + pendingCount: number; } export interface ExportRecordV1 { @@ -30,7 +30,6 @@ export interface ExportRecord { stage: ExportStage; lastAttemptTimestamp: number; exportedFiles: string[]; - failedFiles: string[]; exportedCollectionPaths: ExportedCollectionPaths; } diff --git a/src/utils/export/index.ts b/src/utils/export/index.ts index 8006d870d..14021e281 100644 --- a/src/utils/export/index.ts +++ b/src/utils/export/index.ts @@ -93,20 +93,6 @@ export const getExportedFiles = ( return exportedFiles; }; -export const getExportFailedFiles = ( - allFiles: EnteFile[], - exportRecord: ExportRecord -) => { - const failedFiles = new Set(exportRecord?.failedFiles); - const filesToExport = allFiles.filter((file) => { - if (failedFiles.has(getExportRecordFileUID(file))) { - return true; - } - return false; - }); - return filesToExport; -}; - export const dedupe = (files: string[]) => { const fileSet = new Set(files); return Array.from(fileSet); diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 49f027de9..6789b399c 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -580,19 +580,11 @@ export function getLatestVersionFiles(files: EnteFile[]) { ); } -export function getUserPersonalFiles(files: EnteFile[]) { +export async function getPersonalFiles() { + const files = await getLocalFiles(); const user: User = getData(LS_KEYS.USER); if (!user?.id) { throw Error('user missing'); } return files.filter((file) => file.ownerID === user.id); } - -export const getTotalFileCount = async () => { - try { - const userPersonalFiles = getUserPersonalFiles(await getLocalFiles()); - return userPersonalFiles.length; - } catch (e) { - logError(e, 'updateTotalFileCount failed'); - } -};