mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	import notes from tar archive, closes #63
This commit is contained in:
		
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.6.2", |   "version": "0.7.0-beta", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|   | |||||||
| @@ -57,6 +57,7 @@ | |||||||
|     "session-file-store": "^1.1.2", |     "session-file-store": "^1.1.2", | ||||||
|     "simple-node-logger": "^0.93.30", |     "simple-node-logger": "^0.93.30", | ||||||
|     "sqlite": "^2.9.0", |     "sqlite": "^2.9.0", | ||||||
|  |     "tar-stream": "^1.5.5", | ||||||
|     "unescape": "^1.0.1", |     "unescape": "^1.0.1", | ||||||
|     "ws": "^3.3.2" |     "ws": "^3.3.2" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -6,6 +6,26 @@ function exportSubTree(noteId) { | |||||||
|     download(url); |     download(url); | ||||||
| } | } | ||||||
|  |  | ||||||
| function importSubTree(noteId) { | let importNoteId; | ||||||
|  |  | ||||||
| } | 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(); | ||||||
|  | }); | ||||||
| @@ -226,10 +226,10 @@ if (isElectron()) { | |||||||
| } | } | ||||||
|  |  | ||||||
| function uploadAttachment() { | function uploadAttachment() { | ||||||
|     $("#file-upload").trigger('click'); |     $("#attachment-upload").trigger('click'); | ||||||
| } | } | ||||||
|  |  | ||||||
| $("#file-upload").change(async function() { | $("#attachment-upload").change(async function() { | ||||||
|     const formData = new FormData(); |     const formData = new FormData(); | ||||||
|     formData.append('upload', this.files[0]); |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,6 +31,10 @@ async function exportNote(noteTreeId, directory, pack) { | |||||||
|     const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); |     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 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 content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||||
|  |  | ||||||
|     const childFileName = directory + sanitize(note.title); |     const childFileName = directory + sanitize(note.title); | ||||||
|   | |||||||
| @@ -2,104 +2,128 @@ | |||||||
|  |  | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const fs = require('fs'); |  | ||||||
| const sql = require('../../services/sql'); | 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 auth = require('../../services/auth'); | ||||||
|  | const notes = require('../../services/notes'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | 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) => { | function getFileName(name) { | ||||||
|     const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); |     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 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({}); |     res.send({}); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function importNotes(dir, parentNoteId) { | async function importNotes(files, parentNoteId, sourceId) { | ||||||
|     const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); |     for (const file of files) { | ||||||
|  |         if (file.meta.type !== 'file') { | ||||||
|     if (!parent) { |             file.data = file.data.toString("UTF-8"); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const fileList = fs.readdirSync(dir); |  | ||||||
|  |  | ||||||
|     for (const file of fileList) { |  | ||||||
|         const path = dir + '/' + file; |  | ||||||
|  |  | ||||||
|         if (fs.lstatSync(path).isDirectory()) { |  | ||||||
|             continue; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!file.endsWith('.html')) { |         const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { | ||||||
|             continue; |             type: file.meta.type, | ||||||
|         } |             mime: file.meta.mime, | ||||||
|  |             attributes: file.meta.attributes, | ||||||
|         const fileNameWithoutExt = file.substr(0, file.length - 5); |             sourceId: sourceId | ||||||
|  |  | ||||||
|         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 |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         await sync_table.addNoteTreeSync(noteTreeId); |         if (file.children.length > 0) { | ||||||
|  |             await importNotes(file.children, noteId, sourceId); | ||||||
|         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); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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) { | async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { | ||||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
| @@ -307,6 +338,7 @@ async function deleteNote(noteTreeId, sourceId) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     createNewNote, |     createNewNote, | ||||||
|  |     createNote, | ||||||
|     updateNote, |     updateNote, | ||||||
|     deleteNote, |     deleteNote, | ||||||
|     protectNoteRecursively |     protectNoteRecursively | ||||||
|   | |||||||
| @@ -27,35 +27,10 @@ function ScriptContext(dataKey) { | |||||||
|         return notes.length > 0 ? notes[0] : null; |         return notes.length > 0 ? notes[0] : null; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.createNote = async function (parentNoteId, title, content = "", extraOptions = {}) { |     this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) { | ||||||
|         const note = { |         extraOptions.dataKey = dataKey; | ||||||
|             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) { |         notes.createNote(parentNoteId, title, content, extraOptions); | ||||||
|             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; |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.createAttribute = attributes.createAttribute; |     this.createAttribute = attributes.createAttribute; | ||||||
|   | |||||||
| @@ -56,6 +56,8 @@ | |||||||
|             <img src="images/icons/search.png" alt="Search in notes"/> |             <img src="images/icons/search.png" alt="Search in notes"/> | ||||||
|           </a> |           </a> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|  |         <input type="file" id="import-upload" style="display: none" /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> |       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> | ||||||
| @@ -167,7 +169,7 @@ | |||||||
|           </table> |           </table> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <input type="file" id="file-upload" style="display: none" /> |         <input type="file" id="attachment-upload" style="display: none" /> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="attribute-list"> |       <div id="attribute-list"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user