ente/src/services/exportService.ts

248 lines
9 KiB
TypeScript
Raw Normal View History

2021-07-13 13:39:31 +00:00
import { retryPromise, runningInBrowser, sleep } from 'utils/common';
2021-06-12 17:14:21 +00:00
import { logError } from 'utils/sentry';
2021-07-08 06:59:26 +00:00
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { Collection, getLocalCollections } from './collectionService';
2021-03-29 05:15:08 +00:00
import downloadManager from './downloadManager';
import { File, getLocalFiles } from './fileService';
2021-03-29 05:15:08 +00:00
2021-07-13 13:39:31 +00:00
export interface ExportStats {
current: number;
total: number;
failed: number;
success?: number;
}
export interface ExportRecord {
stage: ExportStage
time: number;
stats: ExportStats;
exportedFiles: string[];
failedFiles: string[];
}
export enum ExportStage {
INIT,
INPROGRESS,
PAUSED,
FINISHED
}
enum ExportNotification {
START = 'export started',
IN_PROGRESS = 'export already in progress',
FINISH = 'export finished',
2021-07-05 12:46:20 +00:00
FAILED = 'export failed',
2021-03-31 08:34:00 +00:00
ABORT = 'export aborted',
2021-07-08 06:59:26 +00:00
PAUSE = 'export paused',
}
2021-07-12 09:15:08 +00:00
enum RecordType {
SUCCESS = 'success',
FAILED = 'failed'
}
2021-03-29 05:15:08 +00:00
class ExportService {
ElectronAPIs: any;
2021-05-29 06:27:52 +00:00
exportInProgress: Promise<void> = null;
2021-07-13 13:39:31 +00:00
recordUpdateInProgress: Promise<void> = null;
2021-07-09 03:35:33 +00:00
stopExport: boolean = false;
2021-07-08 06:59:26 +00:00
pauseExport: boolean = false;
2021-07-12 09:15:08 +00:00
constructor() {
2021-07-12 09:15:08 +00:00
this.ElectronAPIs = runningInBrowser() && window['ElectronAPIs'];
}
2021-07-07 07:45:55 +00:00
async selectExportDirectory() {
return await this.ElectronAPIs.selectRootDirectory();
}
2021-07-09 03:35:33 +00:00
stopRunningExport() {
this.stopExport = true;
2021-07-07 07:45:55 +00:00
}
2021-07-09 03:35:33 +00:00
pauseRunningExport() {
2021-07-08 06:59:26 +00:00
this.pauseExport = true;
}
2021-07-07 07:45:55 +00:00
async exportFiles(updateProgress: (stats: ExportStats) => void) {
const files = await getLocalFiles();
const collections = await getLocalCollections();
if (this.exportInProgress) {
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
return this.exportInProgress;
}
2021-07-08 05:45:20 +00:00
this.ElectronAPIs.showOnTray('starting export');
2021-07-13 13:39:31 +00:00
const dir = getData(LS_KEYS.EXPORT_FOLDER);
2021-07-12 09:15:08 +00:00
if (!dir) {
// no-export folder set
return;
}
2021-07-13 13:39:31 +00:00
const exportRecord = await this.getExportRecord(dir);
const exportedFiles = new Set(exportRecord?.exportedFiles);
console.log(dir, exportedFiles);
2021-07-12 09:15:08 +00:00
const unExportedFiles = files.filter((file) => {
if (!exportedFiles.has(`${file.id}_${file.collectionID}`)) {
return files;
}
});
this.exportInProgress = this.fileExporter(unExportedFiles, collections, updateProgress, dir);
return this.exportInProgress;
}
2021-05-29 06:27:52 +00:00
2021-07-12 09:15:08 +00:00
async retryFailedFiles(updateProgress: (stats: ExportStats) => void) {
const files = await getLocalFiles();
const collections = await getLocalCollections();
if (this.exportInProgress) {
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
return this.exportInProgress;
}
this.ElectronAPIs.showOnTray('starting export');
2021-07-13 13:39:31 +00:00
const dir = getData(LS_KEYS.EXPORT_FOLDER);
console.log(dir);
2021-07-12 09:15:08 +00:00
if (!dir) {
// no-export folder set
return;
}
2021-07-13 13:39:31 +00:00
const failedFilesIds = new Set((await this.getExportRecord()).failedFiles ?? []);
2021-07-12 09:15:08 +00:00
const failedFiles = files.filter((file) => {
if (failedFilesIds.has(`${file.id}_${file.collectionID}`)) {
return files;
}
2021-07-12 09:15:08 +00:00
});
this.exportInProgress = this.fileExporter(failedFiles, collections, updateProgress, dir);
return this.exportInProgress;
}
async fileExporter(files: File[], collections: Collection[], updateProgress: (stats: ExportStats,) => void, dir: string) {
try {
2021-07-12 12:06:21 +00:00
this.stopExport = false;
this.pauseExport = false;
2021-07-12 09:15:08 +00:00
let failedFileCount = 0;
2021-07-08 05:45:20 +00:00
this.ElectronAPIs.showOnTray({
export_progress:
`0 / ${files.length} files exported`,
});
2021-07-12 09:15:08 +00:00
updateProgress({ current: 0, total: files.length, failed: 0 });
2021-07-08 05:45:20 +00:00
this.ElectronAPIs.sendNotification(ExportNotification.START);
const collectionIDMap = new Map<number, string>();
2021-05-29 06:27:52 +00:00
for (const collection of collections) {
2021-06-07 05:04:50 +00:00
const collectionFolderPath = `${dir}/${collection.id}_${this.sanitizeName(collection.name)}`;
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
2021-05-29 06:27:52 +00:00
collectionFolderPath,
);
collectionIDMap.set(collection.id, collectionFolderPath);
}
2021-05-29 06:27:52 +00:00
for (const [index, file] of files.entries()) {
2021-07-09 03:35:33 +00:00
if (this.stopExport || this.pauseExport) {
2021-07-08 06:59:26 +00:00
if (this.pauseExport) {
this.ElectronAPIs.showOnTray({
export_progress:
`${index} / ${files.length} files exported (paused)`,
paused: true,
});
}
2021-03-31 08:34:00 +00:00
break;
}
2021-04-04 08:34:42 +00:00
const uid = `${file.id}_${this.sanitizeName(
2021-05-29 06:27:52 +00:00
file.metadata.title,
)}`;
2021-05-29 06:27:52 +00:00
const filePath = `${collectionIDMap.get(file.collectionID)}/${uid}`;
2021-07-12 09:15:08 +00:00
try {
await this.downloadAndSave(file, filePath);
2021-07-13 13:39:31 +00:00
await this.addFileExportRecord(dir, `${file.id}_${file.collectionID}`, RecordType.SUCCESS);
2021-07-12 09:15:08 +00:00
} catch (e) {
failedFileCount++;
2021-07-13 13:39:31 +00:00
await this.addFileExportRecord(dir, `${file.id}_${file.collectionID}`, RecordType.FAILED);
2021-07-12 09:15:08 +00:00
logError(e, 'download and save failed for file during export');
}
2021-07-05 12:46:20 +00:00
this.ElectronAPIs.showOnTray({
export_progress:
2021-07-08 05:45:20 +00:00
`${index + 1} / ${files.length} files exported`,
2021-07-05 12:46:20 +00:00
});
2021-07-12 09:15:08 +00:00
updateProgress({ current: index + 1, total: files.length, failed: failedFileCount });
}
2021-07-09 03:35:33 +00:00
if (this.stopExport) {
2021-07-08 06:59:26 +00:00
this.ElectronAPIs.sendNotification(
ExportNotification.ABORT,
);
2021-07-09 03:35:33 +00:00
this.ElectronAPIs.showOnTray();
2021-07-08 06:59:26 +00:00
} else if (this.pauseExport) {
this.ElectronAPIs.sendNotification(
ExportNotification.PAUSE,
);
2021-07-12 09:15:08 +00:00
} else if (failedFileCount > 0) {
2021-07-08 06:59:26 +00:00
this.ElectronAPIs.sendNotification(
ExportNotification.FAILED,
);
2021-07-05 12:46:20 +00:00
this.ElectronAPIs.showOnTray({
retry_export:
`export failed - retry export`,
});
} else {
2021-07-08 06:59:26 +00:00
this.ElectronAPIs.sendNotification(
ExportNotification.FINISH,
);
2021-07-05 12:46:20 +00:00
this.ElectronAPIs.showOnTray();
}
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e);
2021-03-31 08:34:00 +00:00
} finally {
this.exportInProgress = null;
2021-03-29 05:15:08 +00:00
}
}
2021-03-29 11:29:19 +00:00
2021-07-13 13:39:31 +00:00
async addFileExportRecord(folder: string, fileUID: string, type: RecordType) {
const exportRecord = await this.ElectronAPIs.getExportRecord(folder);
if (type === RecordType.SUCCESS) {
if (!exportRecord.exportedFiles) {
exportRecord.exportedFiles = [];
}
exportRecord.exportedFiles.push(fileUID);
} else {
if (!exportRecord.failedFiles) {
exportRecord.failedFiles = [];
}
exportRecord.failedFiles.push(fileUID);
}
await this.ElectronAPIs.setExportRecord(folder, exportRecord);
}
async updateExportRecord(newData) {
await sleep(100);
if (this.recordUpdateInProgress) {
await this.recordUpdateInProgress;
this.recordUpdateInProgress = null;
}
this.recordUpdateInProgress = (async () => {
const folder = getData(LS_KEYS.EXPORT_FOLDER);
const exportRecord = await this.getExportRecord(folder);
const newRecord = { ...exportRecord, ...newData };
console.log(newRecord, JSON.stringify(newRecord, null, 2));
this.ElectronAPIs.setExportRecord(folder, JSON.stringify(newRecord, null, 2));
})();
await this.recordUpdateInProgress;
}
async getExportRecord(folder?: string): Promise<ExportRecord> {
console.log(folder);
if (!folder) {
folder = getData(LS_KEYS.EXPORT_FOLDER);
}
const recordFile = await this.ElectronAPIs.getExportRecord(folder);
console.log(recordFile, JSON.parse(recordFile));
return JSON.parse(recordFile);
}
async downloadAndSave(file: File, path) {
const fileStream = await retryPromise(downloadManager.downloadFile(file));
2021-03-31 06:45:41 +00:00
this.ElectronAPIs.saveStreamToDisk(path, fileStream);
this.ElectronAPIs.saveFileToDisk(
`${path}.json`,
2021-05-29 06:27:52 +00:00
JSON.stringify(file.metadata, null, 2),
2021-03-31 06:45:41 +00:00
);
2021-03-29 11:29:19 +00:00
}
2021-05-29 06:27:52 +00:00
2021-04-04 08:23:00 +00:00
private sanitizeName(name) {
return name.replaceAll('/', '_').replaceAll(' ', '_');
}
2021-03-29 05:15:08 +00:00
}
export default new ExportService();