better handle export folder doesn't exist issues

This commit is contained in:
Abhinav 2023-05-09 17:31:35 +05:30
parent 82d48fcfd8
commit 900d69a60d
6 changed files with 148 additions and 72 deletions

View file

@ -113,7 +113,7 @@ export default function ExportModal(props: Props) {
// ======================= // =======================
const verifyExportFolderExists = () => { const verifyExportFolderExists = () => {
if (!exportFolder || !exportService.exists(exportFolder)) { if (!exportService.exportFolderExists(exportFolder)) {
appContext.setDialogMessage( appContext.setDialogMessage(
getExportDirectoryDoesNotExistMessage() getExportDirectoryDoesNotExistMessage()
); );
@ -123,17 +123,25 @@ export default function ExportModal(props: Props) {
const syncExportRecord = async (exportFolder: string): Promise<void> => { const syncExportRecord = async (exportFolder: string): Promise<void> => {
try { try {
if (!exportService.exportFolderExists(exportFolder)) {
const fileExportStats = await exportService.getFileExportStats(
null
);
setFileExportStats(fileExportStats);
}
const exportRecord = await exportService.getExportRecord( const exportRecord = await exportService.getExportRecord(
exportFolder exportFolder
); );
setExportStage(exportRecord?.stage ?? ExportStage.INIT); setExportStage(exportRecord.stage);
setLastExportTime(exportRecord?.lastAttemptTimestamp ?? 0); setLastExportTime(exportRecord.lastAttemptTimestamp);
const fileExportStats = await exportService.getFileExportStats( const fileExportStats = await exportService.getFileExportStats(
exportRecord exportRecord
); );
setFileExportStats(fileExportStats); setFileExportStats(fileExportStats);
} catch (e) { } catch (e) {
logError(e, 'syncExportRecord failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'syncExportRecord failed');
}
} }
}; };

View file

@ -257,17 +257,20 @@ export default function App(props) {
try { try {
addLogLine('init export'); addLogLine('init export');
const exportSettings = exportService.getExportSettings(); const exportSettings = exportService.getExportSettings();
if (!exportService.exportFolderExists(exportSettings?.folder)) {
return;
}
const exportRecord = await exportService.getExportRecord( const exportRecord = await exportService.getExportRecord(
exportSettings?.folder exportSettings.folder
); );
await exportService.runMigration( await exportService.runMigration(
exportSettings?.folder, exportSettings.folder,
exportRecord exportRecord
); );
if (exportSettings?.continuousExport) { if (exportSettings.continuousExport) {
exportService.enableContinuousExport(); exportService.enableContinuousExport();
} }
if (exportRecord?.stage === ExportStage.INPROGRESS) { if (exportRecord.stage === ExportStage.INPROGRESS) {
addLogLine('export was in progress, resuming'); addLogLine('export was in progress, resuming');
exportService.scheduleExport(); exportService.scheduleExport();
} }

View file

@ -65,6 +65,14 @@ const EXPORT_RECORD_FILE_NAME = 'export_status.json';
export const ENTE_EXPORT_DIRECTORY = 'ente Photos'; export const ENTE_EXPORT_DIRECTORY = 'ente Photos';
export const NULL_EXPORT_RECORD: ExportRecord = {
version: 3,
lastAttemptTimestamp: null,
stage: ExportStage.INIT,
fileExportNames: {},
collectionExportNames: {},
};
class ExportService { class ExportService {
private electronAPIs: ElectronAPIs; private electronAPIs: ElectronAPIs;
private exportInProgress: boolean = false; private exportInProgress: boolean = false;
@ -115,11 +123,13 @@ class ExportService {
async runMigration(exportDir: string, exportRecord: ExportRecord) { async runMigration(exportDir: string, exportRecord: ExportRecord) {
try { try {
addLogLine('running migration');
this.migrationInProgress = migrateExportJSON( this.migrationInProgress = migrateExportJSON(
exportDir, exportDir,
exportRecord exportRecord
); );
await this.migrationInProgress; await this.migrationInProgress;
addLogLine('migration completed');
this.migrationInProgress = null; this.migrationInProgress = null;
} catch (e) { } catch (e) {
logError(e, 'migration failed'); logError(e, 'migration failed');
@ -263,7 +273,8 @@ class ExportService {
} }
}; };
async preExport() { async preExport(exportFolder: string) {
this.verifyExportFolderExists(exportFolder);
this.stopExport = false; this.stopExport = false;
await this.updateExportStage(ExportStage.INPROGRESS); await this.updateExportStage(ExportStage.INPROGRESS);
this.updateExportProgress({ this.updateExportProgress({
@ -274,14 +285,24 @@ class ExportService {
} }
async postExport() { async postExport() {
await this.updateExportStage(ExportStage.FINISHED); try {
await this.updateLastExportTime(Date.now()); 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(
const exportRecord = await this.getExportRecord(exportSettings?.folder); exportSettings?.folder
);
const fileExportStats = await this.getFileExportStats(exportRecord); const fileExportStats = await this.getFileExportStats(exportRecord);
this.uiUpdater.setFileExportStats(fileExportStats); this.uiUpdater.setFileExportStats(fileExportStats);
} catch (e) {
logError(e, 'postExport failed');
}
} }
async stopRunningExport() { async stopRunningExport() {
@ -310,9 +331,11 @@ class ExportService {
this.migrationInProgress = null; this.migrationInProgress = null;
} }
try { try {
await this.preExport(); const exportSettings = this.getExportSettings();
const exportFolder = exportSettings?.folder;
await this.preExport(exportFolder);
addLogLine('export started'); addLogLine('export started');
await this.runExport(); await this.runExport(exportFolder);
addLogLine('export completed'); addLogLine('export completed');
} finally { } finally {
this.exportInProgress = false; this.exportInProgress = false;
@ -330,12 +353,8 @@ class ExportService {
} }
}; };
private async runExport() { private async runExport(exportFolder: string) {
try { try {
const exportSettings = this.getExportSettings();
if (!exportSettings?.folder) {
throw new Error(CustomError.NO_EXPORT_FOLDER_SELECTED);
}
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
const files = mergeMetadata(await getLocalFiles()); const files = mergeMetadata(await getLocalFiles());
const personalFiles = getPersonalFiles(files, user); const personalFiles = getPersonalFiles(files, user);
@ -347,9 +366,7 @@ class ExportService {
user user
); );
const exportRecord = await this.getExportRecord( const exportRecord = await this.getExportRecord(exportFolder);
exportSettings.folder
);
const collectionIDExportNameMap = const collectionIDExportNameMap =
convertCollectionIDExportNameObjectToMap( convertCollectionIDExportNameObjectToMap(
exportRecord.collectionExportNames exportRecord.collectionExportNames
@ -412,7 +429,7 @@ class ExportService {
if (renamedCollections?.length > 0) { if (renamedCollections?.length > 0) {
addLogLine(`renaming ${renamedCollections.length} collections`); addLogLine(`renaming ${renamedCollections.length} collections`);
this.collectionRenamer( this.collectionRenamer(
exportSettings.folder, exportFolder,
collectionIDExportNameMap, collectionIDExportNameMap,
renamedCollections, renamedCollections,
incrementSuccess, incrementSuccess,
@ -423,7 +440,7 @@ class ExportService {
if (removedFileUIDs?.length > 0) { if (removedFileUIDs?.length > 0) {
addLogLine(`trashing ${removedFileUIDs.length} files`); addLogLine(`trashing ${removedFileUIDs.length} files`);
await this.fileTrasher( await this.fileTrasher(
exportSettings.folder, exportFolder,
collectionIDExportNameMap, collectionIDExportNameMap,
removedFileUIDs, removedFileUIDs,
incrementSuccess, incrementSuccess,
@ -436,7 +453,7 @@ class ExportService {
filesToExport, filesToExport,
collectionIDNameMap, collectionIDNameMap,
collectionIDExportNameMap, collectionIDExportNameMap,
exportSettings.folder, exportFolder,
incrementSuccess, incrementSuccess,
incrementFailed incrementFailed
); );
@ -447,13 +464,16 @@ class ExportService {
); );
await this.collectionRemover( await this.collectionRemover(
deletedExportedCollections, deletedExportedCollections,
exportSettings.folder, exportFolder,
incrementSuccess, incrementSuccess,
incrementFailed incrementFailed
); );
} }
} catch (e) { } 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 { try {
for (const collection of renamedCollections) { for (const collection of renamedCollections) {
try { try {
this.verifyExportFolderExists(exportFolder);
const oldCollectionExportName = const oldCollectionExportName =
collectionIDExportNameMap.get(collection.id); collectionIDExportNameMap.get(collection.id);
const oldCollectionExportPath = getCollectionExportPath( const oldCollectionExportPath = getCollectionExportPath(
@ -509,14 +530,17 @@ class ExportService {
logError(e, 'collectionRenamer failed a collection'); logError(e, 'collectionRenamer failed a collection');
if ( if (
e.message === e.message ===
CustomError.ADD_FILE_EXPORTED_RECORD_FAILED CustomError.UPDATE_EXPORTED_RECORD_FAILED ||
e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST
) { ) {
throw e; throw e;
} }
} }
} }
} catch (e) { } catch (e) {
logError(e, 'collectionRenamer failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'collectionRenamer failed');
}
throw e; throw e;
} }
} }
@ -535,6 +559,7 @@ class ExportService {
); );
for (const collectionID of deletedExportedCollectionIDs) { for (const collectionID of deletedExportedCollectionIDs) {
try { try {
this.verifyExportFolderExists(exportFolder);
addLocalLog( addLocalLog(
() => () =>
`removing collection with id ${collectionID} from export folder` `removing collection with id ${collectionID} from export folder`
@ -571,14 +596,17 @@ class ExportService {
logError(e, 'collectionRemover failed a collection'); logError(e, 'collectionRemover failed a collection');
if ( if (
e.message === e.message ===
CustomError.ADD_FILE_EXPORTED_RECORD_FAILED CustomError.UPDATE_EXPORTED_RECORD_FAILED ||
e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST
) { ) {
throw e; throw e;
} }
} }
} }
} catch (e) { } catch (e) {
logError(e, 'collectionRemover failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'collectionRemover failed');
}
throw e; throw e;
} }
} }
@ -605,6 +633,7 @@ class ExportService {
break; break;
} }
try { try {
this.verifyExportFolderExists(exportDir);
let collectionExportName = collectionIDFolderNameMap.get( let collectionExportName = collectionIDFolderNameMap.get(
file.collectionID file.collectionID
); );
@ -624,19 +653,14 @@ class ExportService {
file.collectionID, file.collectionID,
collectionExportName collectionExportName
); );
} else {
const collectionExportPath = getCollectionExportPath(
exportDir,
collectionExportName
);
await this.electronAPIs.checkExistsAndCreateDir(
collectionExportPath
);
} }
const collectionExportPath = getCollectionExportPath( const collectionExportPath = getCollectionExportPath(
exportDir, exportDir,
collectionExportName collectionExportName
); );
await this.electronAPIs.checkExistsAndCreateDir(
collectionExportPath
);
const fileExportName = await this.downloadAndSave( const fileExportName = await this.downloadAndSave(
collectionExportPath, collectionExportPath,
file file
@ -652,14 +676,17 @@ class ExportService {
logError(e, 'export failed for a file'); logError(e, 'export failed for a file');
if ( if (
e.message === e.message ===
CustomError.ADD_FILE_EXPORTED_RECORD_FAILED CustomError.UPDATE_EXPORTED_RECORD_FAILED ||
e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST
) { ) {
throw e; throw e;
} }
} }
} }
} catch (e) { } catch (e) {
logError(e, 'fileExporter failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'fileExporter failed');
}
throw e; throw e;
} }
} }
@ -677,6 +704,7 @@ class ExportService {
exportRecord.fileExportNames exportRecord.fileExportNames
); );
for (const fileUID of removedFileUIDs) { for (const fileUID of removedFileUIDs) {
this.verifyExportFolderExists(exportDir);
addLocalLog(() => `trashing file with id ${fileUID}`); addLocalLog(() => `trashing file with id ${fileUID}`);
if (this.stopExport) { if (this.stopExport) {
break; break;
@ -773,14 +801,17 @@ class ExportService {
logError(e, 'trashing failed for a file'); logError(e, 'trashing failed for a file');
if ( if (
e.message === e.message ===
CustomError.ADD_FILE_EXPORTED_RECORD_FAILED CustomError.UPDATE_EXPORTED_RECORD_FAILED ||
e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST
) { ) {
throw e; throw e;
} }
} }
} }
} catch (e) { } catch (e) {
logError(e, 'fileTrasher failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
logError(e, 'fileTrasher failed');
}
throw e; throw e;
} }
} }
@ -802,8 +833,10 @@ class ExportService {
}; };
await this.updateExportRecord(exportRecord, folder); await this.updateExportRecord(exportRecord, folder);
} catch (e) { } catch (e) {
logError(e, 'addFileExportedRecord failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); logError(e, 'addFileExportedRecord failed');
}
throw e;
} }
} }
@ -824,8 +857,10 @@ class ExportService {
await this.updateExportRecord(exportRecord, folder); await this.updateExportRecord(exportRecord, folder);
} catch (e) { } catch (e) {
logError(e, 'addCollectionExportedRecord failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); logError(e, 'addCollectionExportedRecord failed');
}
throw e;
} }
} }
@ -841,8 +876,10 @@ class ExportService {
await this.updateExportRecord(exportRecord, folder); await this.updateExportRecord(exportRecord, folder);
} catch (e) { } catch (e) {
logError(e, 'removeCollectionExportedRecord failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); logError(e, 'removeCollectionExportedRecord failed');
}
throw e;
} }
} }
@ -856,8 +893,10 @@ class ExportService {
); );
await this.updateExportRecord(exportRecord, folder); await this.updateExportRecord(exportRecord, folder);
} catch (e) { } catch (e) {
logError(e, 'removeFileExportedRecord failed'); if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); logError(e, 'removeFileExportedRecord failed');
}
throw e;
} }
} }
@ -878,28 +917,35 @@ class ExportService {
} }
const exportRecord = await this.getExportRecord(folder); const exportRecord = await this.getExportRecord(folder);
const newRecord: ExportRecord = { ...exportRecord, ...newData }; const newRecord: ExportRecord = { ...exportRecord, ...newData };
await this.electronAPIs.setExportRecord( await this.electronAPIs.saveFileToDisk(
`${folder}/${EXPORT_RECORD_FILE_NAME}`, `${folder}/${EXPORT_RECORD_FILE_NAME}`,
JSON.stringify(newRecord, null, 2) JSON.stringify(newRecord, null, 2)
); );
return newRecord; return newRecord;
} catch (e) { } catch (e) {
if (e.message === CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
throw e;
}
logError(e, 'error updating Export Record'); logError(e, 'error updating Export Record');
throw e; throw Error(CustomError.UPDATE_EXPORTED_RECORD_FAILED);
} }
} }
async getExportRecord(folder: string): Promise<ExportRecord> { async getExportRecord(folder: string): Promise<ExportRecord> {
try { try {
if (!folder || !this.exists(folder)) { this.verifyExportFolderExists(folder);
return null; const exportRecordJSONPath = `${folder}/${EXPORT_RECORD_FILE_NAME}`;
if (!this.exists(exportRecordJSONPath)) {
return this.createEmptyExportRecord(folder);
} }
const recordFile = await this.electronAPIs.getExportRecord( const recordFile = await this.electronAPIs.readTextFile(
`${folder}/${EXPORT_RECORD_FILE_NAME}` exportRecordJSONPath
); );
return JSON.parse(recordFile); return JSON.parse(recordFile);
} catch (e) { } 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; throw e;
} }
} }
@ -909,6 +955,7 @@ class ExportService {
collectionID: number, collectionID: number,
collectionIDNameMap: Map<number, string> collectionIDNameMap: Map<number, string>
) { ) {
this.verifyExportFolderExists(exportFolder);
const collectionName = collectionIDNameMap.get(collectionID); const collectionName = collectionIDNameMap.get(collectionID);
const collectionExportName = getUniqueCollectionExportName( const collectionExportName = getUniqueCollectionExportName(
exportFolder, exportFolder,
@ -1057,5 +1104,31 @@ class ExportService {
checkExistsAndCreateDir = (path: string) => { checkExistsAndCreateDir = (path: string) => {
return this.electronAPIs.checkExistsAndCreateDir(path); 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(); export default new ExportService();

View file

@ -38,7 +38,6 @@ import { FILE_TYPE } from 'constants/file';
import { decodeLivePhoto } from 'services/livePhotoService'; import { decodeLivePhoto } from 'services/livePhotoService';
import downloadManager from 'services/downloadManager'; import downloadManager from 'services/downloadManager';
import { retryAsyncFunction } from 'utils/network'; import { retryAsyncFunction } from 'utils/network';
import { CustomError } from 'utils/error';
export async function migrateExportJSON( export async function migrateExportJSON(
exportDir: string, exportDir: string,
@ -67,12 +66,6 @@ async function migrateExport(
exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord
) { ) {
try { try {
if (!exportRecord?.version) {
exportRecord = {
...exportRecord,
version: 0,
};
}
addLogLine(`current export version: ${exportRecord.version}`); addLogLine(`current export version: ${exportRecord.version}`);
if (exportRecord.version === 0) { if (exportRecord.version === 0) {
addLogLine('migrating export to version 1'); addLogLine('migrating export to version 1');
@ -371,6 +364,6 @@ async function addCollectionExportedRecordV1(
await exportService.updateExportRecord(exportRecord, folder); await exportService.updateExportRecord(exportRecord, folder);
} catch (e) { } catch (e) {
logError(e, 'addCollectionExportedRecord failed'); logError(e, 'addCollectionExportedRecord failed');
throw Error(CustomError.ADD_FILE_EXPORTED_RECORD_FAILED); throw e;
} }
} }

View file

@ -17,8 +17,7 @@ export interface ElectronAPIs {
saveFileToDisk: (path: string, file: any) => Promise<void>; saveFileToDisk: (path: string, file: any) => Promise<void>;
selectRootDirectory: () => Promise<string>; selectRootDirectory: () => Promise<string>;
sendNotification: (content: string) => void; sendNotification: (content: string) => void;
getExportRecord: (filePath: string) => Promise<string>; readTextFile: (path: string) => Promise<string>;
setExportRecord: (filePath: string, data: string) => Promise<void>;
showUploadFilesDialog: () => Promise<ElectronFile[]>; showUploadFilesDialog: () => Promise<ElectronFile[]>;
showUploadDirsDialog: () => Promise<ElectronFile[]>; showUploadDirsDialog: () => Promise<ElectronFile[]>;
getPendingUploads: () => Promise<{ getPendingUploads: () => Promise<{

View file

@ -55,7 +55,7 @@ export const CustomError = {
'Windows native image processing is not supported', 'Windows native image processing is not supported',
NETWORK_ERROR: 'Network Error', NETWORK_ERROR: 'Network Error',
NOT_FILE_OWNER: 'not file owner', 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', NO_EXPORT_FOLDER_SELECTED: 'no export folder selected',
EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist', EXPORT_FOLDER_DOES_NOT_EXIST: 'export folder does not exist',
NO_INTERNET_CONNECTION: 'no internet connection', NO_INTERNET_CONNECTION: 'no internet connection',