mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	added progress also to export
This commit is contained in:
		| @@ -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 | ||||
| }; | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|             }); | ||||
|         } | ||||
|   | ||||
| @@ -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], | ||||
|   | ||||
| @@ -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(`<outline title="${preparedTitle}" text="${preparedContent}">\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(`</body> | ||||
| </opml>`); | ||||
|     res.end(); | ||||
|  | ||||
|     exportContext.exportFinished(); | ||||
| } | ||||
|  | ||||
| function prepareText(text) { | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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 = `<a href="#root/${resourceNote.noteId}">${utils.escapeHtml(resource.title)}</a>`; | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|         }); | ||||
|   | ||||
| @@ -57,9 +57,13 @@ | ||||
|                             </label> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div id="export-progress-count-wrapper"> | ||||
|                         <strong>Note export progress count:</strong> <span id="export-progress-count"></span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button class="btn btn-primary">Export</button> | ||||
|                     <button class="btn btn-primary" id="export-button">Export</button> | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|   | ||||
| @@ -28,8 +28,8 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
|                     <div id="import-note-count-wrapper"> | ||||
|                         <strong>Imported notes:</strong> <span id="import-note-count"></span> | ||||
|                     <div id="import-progress-count-wrapper"> | ||||
|                         <strong>Note import progress count:</strong> <span id="import-progress-count"></span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user