diff --git a/package-lock.json b/package-lock.json index c13d6e9cb..38ba38a64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "trilium", - "version": "0.12.0", + "version": "0.13.0-beta", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -13618,9 +13618,9 @@ }, "dependencies": { "xmlbuilder": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", - "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=" + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } }, diff --git a/package.json b/package.json index 959b4cd8f..67e990070 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "sqlite": "^2.9.2", "tar-stream": "^1.6.1", "unescape": "^1.0.1", - "ws": "^5.2.0" + "ws": "^5.2.0", + "xml2js": "^0.4.19" }, "devDependencies": { "electron": "^2.0.1", diff --git a/src/public/javascripts/services/context_menu.js b/src/public/javascripts/services/context_menu.js index 0fbde248e..2c19561c4 100644 --- a/src/public/javascripts/services/context_menu.js +++ b/src/public/javascripts/services/context_menu.js @@ -98,7 +98,7 @@ const contextMenuOptions = { {title: "Native Tar", cmd: "exportBranchToTar"}, {title: "OPML", cmd: "exportBranchToOpml"} ]}, - {title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"}, + {title: "Import into branch (tar, opml)", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"}, {title: "----"}, {title: "Collapse branch Alt+-", cmd: "collapseBranch", uiIcon: "ui-icon-minus"}, {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, diff --git a/src/public/javascripts/services/export.js b/src/public/javascripts/services/export.js index de4c795f7..9f92a67c7 100644 --- a/src/public/javascripts/services/export.js +++ b/src/public/javascripts/services/export.js @@ -29,7 +29,7 @@ $("#import-upload").change(async function() { type: 'POST', contentType: false, // NEEDED, DON'T OMIT THIS processData: false, // NEEDED, DON'T OMIT THIS - }); + }).fail((xhr, status, error) => alert('Import error: ' + xhr.responseText)); await treeService.reload(); }); diff --git a/src/routes/api/import.js b/src/routes/api/import.js index f89ffabe0..f37276502 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.js @@ -7,6 +7,75 @@ const Branch = require('../../entities/branch'); const tar = require('tar-stream'); const stream = require('stream'); const path = require('path'); +const parseString = require('xml2js').parseString; + +async function importToBranch(req) { + const parentNoteId = req.params.parentNoteId; + const file = req.file; + + const parentNote = await repository.getNote(parentNoteId); + + if (!parentNote) { + return [404, `Note ${parentNoteId} doesn't exist.`]; + } + + const extension = path.extname(file.originalname).toLowerCase(); + + if (extension === '.tar') { + await importTar(file, parentNoteId); + } + else if (extension === '.opml') { + return await importOpml(file, parentNoteId); + } + else { + return [400, `Unrecognized extension ${extension}, must be .tar or .opml`]; + } +} + +function toHtml(text) { + return '

' + text.replace(/(?:\r\n|\r|\n)/g, '

') + '

'; +} + +async function importOutline(outline, parentNoteId) { + const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text)); + + for (const childOutline of (outline.outline || [])) { + await importOutline(childOutline, note.noteId); + } +} + +async function importOpml(file, parentNoteId) { + const xml = await new Promise(function(resolve, reject) + { + parseString(file.buffer, function (err, result) { + if (err) { + reject(err); + } + else { + resolve(result); + } + }); + }); + + if (xml.opml.$.version !== '1.0' && xml.opml.$.version !== '1.1') { + return [400, 'Unsupported OPML version ' + xml.opml.$.version + ', 1.0 or 1.1 expected instead.']; + } + + const outlines = xml.opml.body[0].outline || []; + + for (const outline of outlines) { + await importOutline(outline, parentNoteId); + } +} + +async function importTar(file, parentNoteId) { + const files = await parseImportFile(file); + + // maps from original noteId (in tar file) to newly generated noteId + const noteIdMap = {}; + + await importNotes(files, parentNoteId, noteIdMap); +} function getFileName(name) { let key; @@ -86,24 +155,6 @@ async function parseImportFile(file) { }); } -async function importTar(req) { - const parentNoteId = req.params.parentNoteId; - const file = req.file; - - const parentNote = await repository.getNote(parentNoteId); - - if (!parentNote) { - return [404, `Note ${parentNoteId} doesn't exist.`]; - } - - const files = await parseImportFile(file); - - // maps from original noteId (in tar file) to newly generated noteId - const noteIdMap = {}; - - await importNotes(files, parentNoteId, noteIdMap); -} - async function importNotes(files, parentNoteId, noteIdMap) { for (const file of files) { if (file.meta.version !== 1) { @@ -143,5 +194,5 @@ async function importNotes(files, parentNoteId, noteIdMap) { } module.exports = { - importTar + importToBranch }; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 0204f9f5e..f899622e0 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -123,7 +123,7 @@ function register(app) { apiRoute(PUT, '/api/notes/:noteId/clone-after/:afterBranchId', cloningApiRoute.cloneNoteAfter); route(GET, '/api/notes/:noteId/export/:format', [auth.checkApiAuthOrElectron], exportRoute.exportNote); - route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importTar, apiResultHandler); + route(POST, '/api/notes/:parentNoteId/import', [auth.checkApiAuthOrElectron, uploadMiddleware], importRoute.importToBranch, apiResultHandler); route(POST, '/api/notes/:parentNoteId/upload', [auth.checkApiAuthOrElectron, uploadMiddleware], filesRoute.uploadFile, apiResultHandler);