diff --git a/src/public/javascripts/dialogs/export.js b/src/public/javascripts/dialogs/export.js index 6f15cee85..14d872df8 100644 --- a/src/public/javascripts/dialogs/export.js +++ b/src/public/javascripts/dialogs/export.js @@ -2,6 +2,8 @@ import treeService from '../services/tree.js'; import treeUtils from "../services/tree_utils.js"; import utils from "../services/utils.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; +import messagingService from "../services/messaging.js"; +import infoService from "../services/info.js"; const $dialog = $("#export-dialog"); const $form = $("#export-form"); @@ -10,8 +12,18 @@ const $subtreeFormats = $("#export-subtree-formats"); const $singleFormats = $("#export-single-formats"); const $subtreeType = $("#export-type-subtree"); const $singleType = $("#export-type-single"); +const $exportNoteCountWrapper = $("#export-progress-count-wrapper"); +const $exportNoteCount = $("#export-progress-count"); +const $exportButton = $("#export-button"); + +let exportId = ''; async function showDialog(defaultType) { + // each opening of the dialog resets the exportId so we don't associate it with previous exports anymore + exportId = ''; + $exportNoteCountWrapper.hide(); + $exportNoteCount.text('0'); + if (defaultType === 'subtree') { $subtreeType.prop("checked", true).change(); } @@ -33,6 +45,8 @@ async function showDialog(defaultType) { } $form.submit(() => { + $exportButton.attr("disabled", "disabled"); + const exportType = $dialog.find("input[name='export-type']:checked").val(); if (!exportType) { @@ -49,13 +63,13 @@ $form.submit(() => { exportBranch(currentNode.data.branchId, exportType, exportFormat); - $dialog.modal('hide'); - return false; }); function exportBranch(branchId, type, format) { - const url = utils.getHost() + `/api/notes/${branchId}/export/${type}/${format}?protectedSessionId=` + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); + exportId = utils.randomString(10); + + const url = utils.getHost() + `/api/notes/${branchId}/export/${type}/${format}/${exportId}?protectedSessionId=` + encodeURIComponent(protectedSessionHolder.getProtectedSessionId()); utils.download(url); } @@ -79,6 +93,30 @@ $('input[name=export-type]').change(function () { } }); +messagingService.subscribeToMessages(async message => { + if (message.type === 'export-error') { + infoService.showError(message.message); + $dialog.modal('hide'); + return; + } + + if (!message.exportId || message.exportId !== exportId) { + // incoming messages must correspond to this export instance + return; + } + + if (message.type === 'export-progress-count') { + $exportNoteCountWrapper.show(); + + $exportNoteCount.text(message.progressCount); + } + else if (message.type === 'export-finished') { + $dialog.modal('hide'); + + infoService.showMessage("Export finished successfully."); + } +}); + export default { showDialog }; \ No newline at end of file diff --git a/src/public/javascripts/dialogs/import.js b/src/public/javascripts/dialogs/import.js index f482d104e..905d178e3 100644 --- a/src/public/javascripts/dialogs/import.js +++ b/src/public/javascripts/dialogs/import.js @@ -9,8 +9,8 @@ const $dialog = $("#import-dialog"); const $form = $("#import-form"); const $noteTitle = $dialog.find(".note-title"); const $fileUploadInput = $("#import-file-upload-input"); -const $importNoteCountWrapper = $("#import-note-count-wrapper"); -const $importNoteCount = $("#import-note-count"); +const $importNoteCountWrapper = $("#import-progress-count-wrapper"); +const $importNoteCount = $("#import-progress-count"); const $importButton = $("#import-button"); let importId; @@ -74,10 +74,10 @@ messagingService.subscribeToMessages(async message => { return; } - if (message.type === 'import-note-count') { + if (message.type === 'import-progress-count') { $importNoteCountWrapper.show(); - $importNoteCount.text(message.count); + $importNoteCount.text(message.progressCount); } else if (message.type === 'import-finished') { $dialog.modal('hide'); diff --git a/src/routes/api/export.js b/src/routes/api/export.js index cd590cbd7..304c5b920 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -4,22 +4,76 @@ const tarExportService = require('../../services/export/tar'); const singleExportService = require('../../services/export/single'); const opmlExportService = require('../../services/export/opml'); const repository = require("../../services/repository"); +const messagingService = require("../../services/messaging"); +const log = require("../../services/log"); + +class ExportContext { + constructor(exportId) { + // exportId is to distinguish between different export events - it is possible (though not recommended) + // to have multiple exports going at the same time + this.exportId = exportId; + // count is mean to represent count of exported notes where practical, otherwise it's just some measure of progress + this.progressCount = 0; + this.lastSentCountTs = Date.now(); + } + + async increaseProgressCount() { + this.progressCount++; + + if (Date.now() - this.lastSentCountTs >= 200) { + this.lastSentCountTs = Date.now(); + + await messagingService.sendMessageToAllClients({ + exportId: this.exportId, + type: 'export-progress-count', + progressCount: this.progressCount + }); + } + } + + async exportFinished() { + await messagingService.sendMessageToAllClients({ + exportId: this.exportId, + type: 'export-finished' + }); + } + + // must remaing static + async reportError(message) { + await messagingService.sendMessageToAllClients({ + type: 'export-error', + message: message + }); + } +} async function exportBranch(req, res) { - const {branchId, type, format} = req.params; + const {branchId, type, format, exportId} = req.params; const branch = await repository.getBranch(branchId); - if (type === 'subtree' && (format === 'html' || format === 'markdown')) { - await tarExportService.exportToTar(branch, format, res); + const exportContext = new ExportContext(exportId); + + try { + if (type === 'subtree' && (format === 'html' || format === 'markdown')) { + await tarExportService.exportToTar(exportContext, branch, format, res); + } + else if (type === 'single') { + await singleExportService.exportSingleNote(exportContext, branch, format, res); + } + else if (format === 'opml') { + await opmlExportService.exportToOpml(exportContext, branch, res); + } + else { + return [404, "Unrecognized export format " + format]; + } } - else if (type === 'single') { - await singleExportService.exportSingleNote(branch, format, res); - } - else if (format === 'opml') { - await opmlExportService.exportToOpml(branch, res); - } - else { - return [404, "Unrecognized export format " + format]; + catch (e) { + const message = "Export failed with following error: '" + e.message + "'. More details might be in the logs."; + exportContext.reportError(message); + + log.error(message + e.stack); + + res.status(500).send(message); } } diff --git a/src/routes/api/import.js b/src/routes/api/import.js index 1b50c4a8c..ec63abd8f 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.js @@ -16,19 +16,20 @@ class ImportContext { // importId is to distinguish between different import events - it is possible (though not recommended) // to have multiple imports going at the same time this.importId = importId; + // // count is mean to represent count of exported notes where practical, otherwise it's just some measure of progress this.count = 0; this.lastSentCountTs = Date.now(); } - async increaseCount() { + async increaseProgressCount() { this.count++; - if (Date.now() - this.lastSentCountTs >= 1000) { + if (Date.now() - this.lastSentCountTs >= 200) { this.lastSentCountTs = Date.now(); await messagingService.sendMessageToAllClients({ importId: this.importId, - type: 'import-note-count', + type: 'import-progress-count', count: this.count }); } diff --git a/src/routes/routes.js b/src/routes/routes.js index 1dc887eca..4de545612 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -128,7 +128,7 @@ function register(app) { apiRoute(PUT, '/api/notes/:noteId/clone-to/:parentNoteId', cloningApiRoute.cloneNoteToParent); apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); - route(GET, '/api/notes/:branchId/export/:type/:format', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); + route(GET, '/api/notes/:branchId/export/:type/:format/:exportId', [auth.checkApiAuthOrElectron], exportRoute.exportBranch); route(POST, '/api/notes/:parentNoteId/import/:importId', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler); route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], diff --git a/src/services/export/opml.js b/src/services/export/opml.js index 5a9f164ed..9aef5033b 100644 --- a/src/services/export/opml.js +++ b/src/services/export/opml.js @@ -3,7 +3,7 @@ const repository = require("../repository"); const utils = require('../utils'); -async function exportToOpml(branch, res) { +async function exportToOpml(exportContext, branch, res) { const note = await branch.getNote(); async function exportNoteInner(branchId) { @@ -21,6 +21,8 @@ async function exportToOpml(branch, res) { res.write(`\n`); + exportContext.increaseProgressCount(); + for (const child of await note.getChildBranches()) { await exportNoteInner(child.branchId); } @@ -45,6 +47,8 @@ async function exportToOpml(branch, res) { res.write(` `); res.end(); + + exportContext.exportFinished(); } function prepareText(text) { diff --git a/src/services/export/single.js b/src/services/export/single.js index e5f2b833e..0e7c4dca9 100644 --- a/src/services/export/single.js +++ b/src/services/export/single.js @@ -5,7 +5,7 @@ const mimeTypes = require('mime-types'); const html = require('html'); const utils = require('../utils'); -async function exportSingleNote(branch, format, res) { +async function exportSingleNote(exportContext, branch, format, res) { const note = await branch.getNote(); if (note.type === 'image' || note.type === 'file') { @@ -54,6 +54,9 @@ async function exportSingleNote(branch, format, res) { res.setHeader('Content-Type', mime + '; charset=UTF-8'); res.send(payload); + + exportContext.increaseProgressCount(); + exportContext.exportFinished(); } module.exports = { diff --git a/src/services/export/tar.js b/src/services/export/tar.js index 7634f3cbb..407bfe5fd 100644 --- a/src/services/export/tar.js +++ b/src/services/export/tar.js @@ -11,9 +11,11 @@ const utils = require('../utils'); const sanitize = require("sanitize-filename"); /** - * @param format - 'html' or 'markdown' + * @param {ExportContext} exportContext + * @param {Branch} branch + * @param {string} format - 'html' or 'markdown' */ -async function exportToTar(branch, format, res) { +async function exportToTar(exportContext, branch, format, res) { let turndownService = format === 'markdown' ? new TurndownService() : null; const pack = tar.pack(); @@ -114,6 +116,8 @@ async function exportToTar(branch, format, res) { }) }; + exportContext.increaseProgressCount(); + if (note.type === 'text') { meta.format = format; } @@ -186,6 +190,8 @@ async function exportToTar(branch, format, res) { pack.entry({name: path + noteMeta.dataFileName, size: content.length}, content); } + exportContext.increaseProgressCount(); + if (noteMeta.children && noteMeta.children.length > 0) { const directoryPath = path + noteMeta.dirFileName; @@ -231,6 +237,8 @@ async function exportToTar(branch, format, res) { res.setHeader('Content-Type', 'application/tar'); pack.pipe(res); + + exportContext.exportFinished(); } module.exports = { diff --git a/src/services/import/enex.js b/src/services/import/enex.js index 0caa89b7f..fbfff05b1 100644 --- a/src/services/import/enex.js +++ b/src/services/import/enex.js @@ -218,7 +218,7 @@ async function importEnex(importContext, file, parentNote) { mime: 'text/html' })).note; - importContext.increaseCount(); + importContext.increaseProgressCount(); const noteContent = await noteEntity.getNoteContent(); @@ -240,7 +240,7 @@ async function importEnex(importContext, file, parentNote) { mime: resource.mime })).note; - importContext.increaseCount(); + importContext.increaseProgressCount(); const resourceLink = `${utils.escapeHtml(resource.title)}`; diff --git a/src/services/import/opml.js b/src/services/import/opml.js index 7630d3d83..72ce597d5 100644 --- a/src/services/import/opml.js +++ b/src/services/import/opml.js @@ -50,7 +50,7 @@ function toHtml(text) { async function importOutline(importContext, outline, parentNoteId) { const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text)); - importContext.increaseCount(); + importContext.increaseProgressCount(); for (const childOutline of (outline.outline || [])) { await importOutline(childOutline, note.noteId); diff --git a/src/services/import/single.js b/src/services/import/single.js index 8e378d1f8..e874f00cf 100644 --- a/src/services/import/single.js +++ b/src/services/import/single.js @@ -20,7 +20,7 @@ async function importMarkdown(importContext, file, parentNote) { mime: 'text/html' }); - importContext.increaseCount(); + importContext.increaseProgressCount(); importContext.importFinished(note.noteId); return note; @@ -35,7 +35,7 @@ async function importHtml(importContext, file, parentNote) { mime: 'text/html' }); - importContext.increaseCount(); + importContext.increaseProgressCount(); importContext.importFinished(note.noteId); return note; diff --git a/src/services/import/tar.js b/src/services/import/tar.js index eb18f3c7c..98483e322 100644 --- a/src/services/import/tar.js +++ b/src/services/import/tar.js @@ -342,7 +342,7 @@ async function importTar(importContext, fileBuffer, importRootNote) { log.info("Ignoring tar import entry with type " + header.type); } - importContext.increaseCount(); + importContext.increaseProgressCount(); next(); // ready for next entry }); diff --git a/src/views/dialogs/export.ejs b/src/views/dialogs/export.ejs index 8c060ed13..9f31007a6 100644 --- a/src/views/dialogs/export.ejs +++ b/src/views/dialogs/export.ejs @@ -57,9 +57,13 @@ + +
+ Note export progress count: +
diff --git a/src/views/dialogs/import.ejs b/src/views/dialogs/import.ejs index 02bdf4d0f..0f84b1a69 100644 --- a/src/views/dialogs/import.ejs +++ b/src/views/dialogs/import.ejs @@ -28,8 +28,8 @@ -
- Imported notes: +
+ Note import progress count: