diff --git a/package-lock.json b/package-lock.json index 6625326be..b5682c41c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "trilium", - "version": "0.6.2", + "version": "0.7.0-beta", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5ac43d5ae..e84a479de 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "session-file-store": "^1.1.2", "simple-node-logger": "^0.93.30", "sqlite": "^2.9.0", + "tar-stream": "^1.5.5", "unescape": "^1.0.1", "ws": "^3.3.2" }, diff --git a/src/public/javascripts/export.js b/src/public/javascripts/export.js index 6938d49ec..33b498b99 100644 --- a/src/public/javascripts/export.js +++ b/src/public/javascripts/export.js @@ -6,6 +6,26 @@ function exportSubTree(noteId) { download(url); } -function importSubTree(noteId) { +let importNoteId; -} \ No newline at end of file +function importSubTree(noteId) { + importNoteId = noteId; + + $("#import-upload").trigger('click'); +} + +$("#import-upload").change(async function() { + const formData = new FormData(); + formData.append('upload', this.files[0]); + + await $.ajax({ + url: baseApiUrl + 'import/' + importNoteId, + headers: server.getHeaders(), + data: formData, + type: 'POST', + contentType: false, // NEEDED, DON'T OMIT THIS + processData: false, // NEEDED, DON'T OMIT THIS + }); + + await noteTree.reload(); +}); \ No newline at end of file diff --git a/src/public/javascripts/init.js b/src/public/javascripts/init.js index f0e25a28b..dcef0f84d 100644 --- a/src/public/javascripts/init.js +++ b/src/public/javascripts/init.js @@ -226,10 +226,10 @@ if (isElectron()) { } function uploadAttachment() { - $("#file-upload").trigger('click'); + $("#attachment-upload").trigger('click'); } -$("#file-upload").change(async function() { +$("#attachment-upload").change(async function() { const formData = new FormData(); formData.append('upload', this.files[0]); diff --git a/src/routes/api/export.js b/src/routes/api/export.js index d719335af..344a8da76 100644 --- a/src/routes/api/export.js +++ b/src/routes/api/export.js @@ -31,6 +31,10 @@ 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]); + if (note.isProtected) { + return; + } + const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; const childFileName = directory + sanitize(note.title); diff --git a/src/routes/api/import.js b/src/routes/api/import.js index 8857018bb..6cac8e1e9 100644 --- a/src/routes/api/import.js +++ b/src/routes/api/import.js @@ -2,104 +2,128 @@ const express = require('express'); const router = express.Router(); -const fs = require('fs'); const sql = require('../../services/sql'); -const data_dir = require('../../services/data_dir'); -const utils = require('../../services/utils'); -const sync_table = require('../../services/sync_table'); const auth = require('../../services/auth'); +const notes = require('../../services/notes'); const wrap = require('express-promise-wrap').wrap; +const tar = require('tar-stream'); +const multer = require('multer')(); +const stream = require('stream'); +const path = require('path'); -router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { - const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); +function getFileName(name) { + let key; + + if (name.endsWith(".dat")) { + key = "data"; + name = name.substr(0, name.length - 4); + } + else if (name.endsWith((".meta"))) { + key = "meta"; + name = name.substr(0, name.length - 5); + } + else { + throw new Error("Unknown file type in import archive: " + name); + } + return {name, key}; +} + +async function parseImportFile(file) { + const fileMap = {}; + const files = []; + + const extract = tar.extract(); + + extract.on('entry', function(header, stream, next) { + let {name, key} = getFileName(header.name); + + let file = fileMap[name]; + + if (!file) { + file = fileMap[name] = { + children: [] + }; + + let parentFileName = path.dirname(header.name); + + if (parentFileName && parentFileName !== '.') { + fileMap[parentFileName].children.push(file); + } + else { + files.push(file); + } + } + + const chunks = []; + + stream.on("data", function (chunk) { + chunks.push(chunk); + }); + + // header is the tar header + // stream is the content body (might be an empty stream) + // call next when you are done with this entry + + stream.on('end', function() { + file[key] = Buffer.concat(chunks); + + if (key === "meta") { + file[key] = JSON.parse(file[key].toString("UTF-8")); + } + + next(); // ready for next entry + }); + + stream.resume(); // just auto drain the stream + }); + + return new Promise(resolve => { + extract.on('finish', function() { + resolve(files); + }); + + const bufferStream = new stream.PassThrough(); + bufferStream.end(file.buffer); + + bufferStream.pipe(extract); + }); +} + +router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => { + const sourceId = req.headers.source_id; const parentNoteId = req.params.parentNoteId; + const file = req.file; - const dir = data_dir.EXPORT_DIR + '/' + directory; + const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); - await sql.doInTransaction(async () => await importNotes(dir, parentNoteId)); + if (!note) { + return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); + } + + const files = await parseImportFile(file); + + await sql.doInTransaction(async () => { + await importNotes(files, parentNoteId, sourceId); + }); res.send({}); })); -async function importNotes(dir, parentNoteId) { - const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); - - if (!parent) { - return; - } - - const fileList = fs.readdirSync(dir); - - for (const file of fileList) { - const path = dir + '/' + file; - - if (fs.lstatSync(path).isDirectory()) { - continue; +async function importNotes(files, parentNoteId, sourceId) { + for (const file of files) { + if (file.meta.type !== 'file') { + file.data = file.data.toString("UTF-8"); } - if (!file.endsWith('.html')) { - continue; - } - - const fileNameWithoutExt = file.substr(0, file.length - 5); - - let noteTitle; - let notePos; - - const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/); - if (match) { - notePos = parseInt(match[1]); - noteTitle = match[2]; - } - else { - let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); - if (maxPos) { - notePos = maxPos + 1; - } - else { - notePos = 0; - } - - noteTitle = fileNameWithoutExt; - } - - const noteText = fs.readFileSync(path, "utf8"); - - const noteId = utils.newNoteId(); - const noteTreeId = utils.newNoteRevisionId(); - - const now = utils.nowDate(); - - await sql.insert('note_tree', { - noteTreeId: noteTreeId, - noteId: noteId, - parentNoteId: parentNoteId, - notePosition: notePos, - isExpanded: 0, - isDeleted: 0, - dateModified: now + const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { + type: file.meta.type, + mime: file.meta.mime, + attributes: file.meta.attributes, + sourceId: sourceId }); - await sync_table.addNoteTreeSync(noteTreeId); - - await sql.insert('notes', { - noteId: noteId, - title: noteTitle, - content: noteText, - isDeleted: 0, - isProtected: 0, - type: 'text', - mime: 'text/html', - dateCreated: now, - dateModified: now - }); - - await sync_table.addNoteSync(noteId); - - const noteDir = dir + '/' + fileNameWithoutExt; - - if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) { - await importNotes(noteDir, noteId); + if (file.children.length > 0) { + await importNotes(file.children, noteId, sourceId); } } } diff --git a/src/services/notes.js b/src/services/notes.js index 45cf16538..38ecf9b37 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -83,6 +83,37 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) { }; } +async function createNote(parentNoteId, title, content = "", extraOptions = {}) { + const note = { + title: title, + content: extraOptions.json ? JSON.stringify(content, null, '\t') : content, + target: 'into', + isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, + type: extraOptions.type, + mime: extraOptions.mime + }; + + if (extraOptions.json) { + note.type = "code"; + note.mime = "application/json"; + } + + if (!note.type) { + note.type = "text"; + note.mime = "text/html"; + } + + const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId); + + if (extraOptions.attributes) { + for (const attrName in extraOptions.attributes) { + await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); + } + } + + return noteId; +} + async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); @@ -307,6 +338,7 @@ async function deleteNote(noteTreeId, sourceId) { module.exports = { createNewNote, + createNote, updateNote, deleteNote, protectNoteRecursively diff --git a/src/services/script_context.js b/src/services/script_context.js index 3026a986b..921504ba7 100644 --- a/src/services/script_context.js +++ b/src/services/script_context.js @@ -27,35 +27,10 @@ function ScriptContext(dataKey) { return notes.length > 0 ? notes[0] : null; }; - this.createNote = async function (parentNoteId, title, content = "", extraOptions = {}) { - const note = { - title: title, - content: extraOptions.json ? JSON.stringify(content, null, '\t') : content, - target: 'into', - isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, - type: extraOptions.type, - mime: extraOptions.mime - }; + this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) { + extraOptions.dataKey = dataKey; - if (extraOptions.json) { - note.type = "code"; - note.mime = "application/json"; - } - - if (!note.type) { - note.type = "text"; - note.mime = "text/html"; - } - - const noteId = (await notes.createNewNote(parentNoteId, note, dataKey)).noteId; - - if (extraOptions.attributes) { - for (const attrName in extraOptions.attributes) { - await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); - } - } - - return noteId; + notes.createNote(parentNoteId, title, content, extraOptions); }; this.createAttribute = attributes.createAttribute; diff --git a/src/views/index.ejs b/src/views/index.ejs index b575eb7b3..7f390e66c 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -56,6 +56,8 @@ + +
- +