diff --git a/src/public/javascripts/context_menu.js b/src/public/javascripts/context_menu.js index 36c8ebc33..fc5cc7f0f 100644 --- a/src/public/javascripts/context_menu.js +++ b/src/public/javascripts/context_menu.js @@ -85,9 +85,12 @@ const contextMenu = (function() { {title: "Paste into Ctrl+V", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, {title: "----"}, - {title: "Collapse sub-tree Alt+-", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"}, - {title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"}, - {title: "Sort alphabetically Alt+S", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} + {title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"}, + {title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"}, + {title: "----"}, + {title: "Collapse sub-tree Alt+-", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"}, + {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, + {title: "Sort alphabetically Alt+S", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} ], beforeOpen: (event, ui) => { @@ -139,13 +142,19 @@ const contextMenu = (function() { else if (ui.cmd === "delete") { treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); } - else if (ui.cmd === "collapse-sub-tree") { + else if (ui.cmd === "exportSubTree") { + exportSubTree(node.data.noteId); + } + else if (ui.cmd === "importSubTree") { + importSubTree(node.data.noteId); + } + else if (ui.cmd === "collapseSubTree") { noteTree.collapseTree(node); } - else if (ui.cmd === "force-note-sync") { + else if (ui.cmd === "forceNoteSync") { forceNoteSync(node.data.noteId); } - else if (ui.cmd === "sort-alphabetically") { + else if (ui.cmd === "sortAlphabetically") { noteTree.sortAlphabetically(node.data.noteId); } else { diff --git a/src/public/javascripts/export.js b/src/public/javascripts/export.js new file mode 100644 index 000000000..6938d49ec --- /dev/null +++ b/src/public/javascripts/export.js @@ -0,0 +1,11 @@ +"use strict"; + +function exportSubTree(noteId) { + const url = getHost() + "/api/export/" + noteId; + + download(url); +} + +function importSubTree(noteId) { + +} \ No newline at end of file diff --git a/src/public/javascripts/note_editor.js b/src/public/javascripts/note_editor.js index 616acd7ae..98bc9aa84 100644 --- a/src/public/javascripts/note_editor.js +++ b/src/public/javascripts/note_editor.js @@ -304,16 +304,7 @@ const noteEditor = (function() { } } - $attachmentDownload.click(() => { - if (isElectron()) { - const remote = require('electron').remote; - - remote.getCurrentWebContents().downloadURL(getAttachmentUrl()); - } - else { - window.location.href = getAttachmentUrl(); - } - }); + $attachmentDownload.click(() => download(getAttachmentUrl())); $attachmentOpen.click(() => { if (isElectron()) { @@ -328,13 +319,8 @@ const noteEditor = (function() { function getAttachmentUrl() { // electron needs absolute URL so we extract current host, port, protocol - const url = new URL(window.location.href); - const host = url.protocol + "//" + url.hostname + ":" + url.port; - - const downloadUrl = "/api/attachments/download/" + getCurrentNoteId() + return getHost() + "/api/attachments/download/" + getCurrentNoteId() + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId()); - - return host + downloadUrl; } $(document).ready(() => { diff --git a/src/public/javascripts/utils.js b/src/public/javascripts/utils.js index c826b7e57..81abc2147 100644 --- a/src/public/javascripts/utils.js +++ b/src/public/javascripts/utils.js @@ -189,4 +189,20 @@ async function requireCss(url) { if (!css.includes(url)) { $('head').append($('').attr('href', url)); } +} + +function getHost() { + const url = new URL(window.location.href); + return url.protocol + "//" + url.hostname + ":" + url.port; +} + +function download(url) { + if (isElectron()) { + const remote = require('electron').remote; + + remote.getCurrentWebContents().downloadURL(url); + } + else { + window.location.href = url; + } } \ No newline at end of file diff --git a/src/routes/api/export.js b/src/routes/api/export.js index 7846c168b..d719335af 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -2,56 +2,67 @@ const express = require('express'); const router = express.Router(); -const rimraf = require('rimraf'); -const fs = require('fs'); const sql = require('../../services/sql'); -const data_dir = require('../../services/data_dir'); +const attributes = require('../../services/attributes'); const html = require('html'); const auth = require('../../services/auth'); const wrap = require('express-promise-wrap').wrap; +const tar = require('tar-stream'); +const sanitize = require("sanitize-filename"); -router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => { +router.get('/:noteId/', auth.checkApiAuth, wrap(async (req, res, next) => { const noteId = req.params.noteId; - const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); - - if (!fs.existsSync(data_dir.EXPORT_DIR)) { - fs.mkdirSync(data_dir.EXPORT_DIR); - } - - const completeExportDir = data_dir.EXPORT_DIR + '/' + directory; - - if (fs.existsSync(completeExportDir)) { - rimraf.sync(completeExportDir); - } - - fs.mkdirSync(completeExportDir); const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); - await exportNote(noteTreeId, completeExportDir); + const pack = tar.pack(); - res.send({}); + const name = await exportNote(noteTreeId, '', pack); + + pack.finalize(); + + res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"'); + res.setHeader('Content-Type', 'application/tar'); + + pack.pipe(res); })); -async function exportNote(noteTreeId, dir) { +async function exportNote(noteTreeId, directory, pack) { const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]); - const pos = (noteTree.notePosition + '').padStart(4, '0'); + const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; - fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2})); + const childFileName = directory + sanitize(note.title); + + console.log(childFileName); + + pack.entry({ name: childFileName + ".dat", size: content.length }, content); + + const metadata = await getMetadata(note); + + pack.entry({ name: childFileName + ".meta", size: metadata.length }, metadata); const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); if (children.length > 0) { - const childrenDir = dir + '/' + pos + '-' + note.title; - - fs.mkdirSync(childrenDir); - for (const child of children) { - await exportNote(child.noteTreeId, childrenDir); + await exportNote(child.noteTreeId, childFileName + "/", pack); } } + + return childFileName; +} + +async function getMetadata(note) { + const meta = { + title: note.title, + type: note.type, + mime: note.mime, + attributes: await attributes.getNoteAttributeMap(note.noteId) + }; + + return JSON.stringify(meta, null, '\t') } module.exports = router; \ No newline at end of file diff --git a/src/services/data_dir.js b/src/services/data_dir.js index 0dc0f0e01..d792fe064 100644 --- a/src/services/data_dir.js +++ b/src/services/data_dir.js @@ -20,6 +20,5 @@ module.exports = { DOCUMENT_PATH, BACKUP_DIR, LOG_DIR, - EXPORT_DIR, ANONYMIZED_DB_DIR }; \ No newline at end of file diff --git a/src/views/index.ejs b/src/views/index.ejs index b039a998f..b575eb7b3 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -497,6 +497,7 @@ +