mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	added "DB dump" tool, WIP
This commit is contained in:
		
							
								
								
									
										22
									
								
								dump-db/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								dump-db/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # Trilium Notes DB dump tool | ||||
|  | ||||
| This is a simple tool to dump the content of Trilium's document.db onto filesystem. | ||||
|  | ||||
| It is meant as a last resort solution when the standard mean to access your data (through main Trilium application) fail. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| This tool requires node.js, testing has been done on 16.14.0, but it will probably work on other versions as well. | ||||
|  | ||||
| ``` | ||||
| npm install | ||||
| ``` | ||||
|  | ||||
| ## Running | ||||
|  | ||||
| See output of `node dump-db.js --help`: | ||||
|  | ||||
| ``` | ||||
| Trilium Notes DB dump tool. Usage: | ||||
| node dump-db.js PATH_TO_DOCUMENT_DB TARGET_PATH | ||||
| ``` | ||||
							
								
								
									
										132
									
								
								dump-db/dump-db.js
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										132
									
								
								dump-db/dump-db.js
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| #!/usr/bin/env node | ||||
|  | ||||
| const fs = require('fs'); | ||||
| const sql = require("./inc/sql"); | ||||
|  | ||||
| const args = process.argv.slice(2); | ||||
| const sanitize = require('sanitize-filename'); | ||||
| const path = require("path"); | ||||
| const mimeTypes = require("mime-types"); | ||||
|  | ||||
| if (args[0] === '-h' || args[0] === '--help') { | ||||
|     printHelp(); | ||||
|     process.exit(0); | ||||
| } | ||||
|  | ||||
| if (args.length !== 2) { | ||||
|     console.error(`Exactly 2 arguments are expected. Run with --help to see usage.`); | ||||
|     process.exit(1); | ||||
| } | ||||
|  | ||||
| const [documentPath, targetPath] = args; | ||||
|  | ||||
| if (!fs.existsSync(documentPath)) { | ||||
|     console.error(`Path to document '${documentPath}' has not been found. Run with --help to see usage.`); | ||||
|     process.exit(1); | ||||
| } | ||||
|  | ||||
| if (!fs.existsSync(targetPath)) { | ||||
|     const ret = fs.mkdirSync(targetPath, { recursive: true }); | ||||
|  | ||||
|     if (!ret) { | ||||
|         console.error(`Target path '${targetPath}' could not be created. Run with --help to see usage.`); | ||||
|         process.exit(1); | ||||
|     } | ||||
| } | ||||
|  | ||||
| sql.openDatabase(documentPath); | ||||
|  | ||||
| const existingPaths = {}; | ||||
|  | ||||
| dumpNote(targetPath, 'root'); | ||||
|  | ||||
| function getFileName(note, childTargetPath, safeTitle) { | ||||
|     let existingExtension = path.extname(safeTitle).toLowerCase(); | ||||
|     let newExtension; | ||||
|  | ||||
|     if (note.type === 'text') { | ||||
|         newExtension = 'html'; | ||||
|     } else if (note.mime === 'application/x-javascript' || note.mime === 'text/javascript') { | ||||
|         newExtension = 'js'; | ||||
|     } else if (existingExtension.length > 0) { // if the page already has an extension, then we'll just keep it | ||||
|         newExtension = null; | ||||
|     } else { | ||||
|         if (note.mime?.toLowerCase()?.trim() === "image/jpg") { // image/jpg is invalid but pretty common | ||||
|             newExtension = 'jpg'; | ||||
|         } else { | ||||
|             newExtension = mimeTypes.extension(note.mime) || "dat"; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let fileNameWithPath = childTargetPath; | ||||
|  | ||||
|     // if the note is already named with extension (e.g. "jquery"), then it's silly to append exact same extension again | ||||
|     if (newExtension && existingExtension !== "." + newExtension.toLowerCase()) { | ||||
|         fileNameWithPath += "." + newExtension; | ||||
|     } | ||||
|     return fileNameWithPath; | ||||
| } | ||||
|  | ||||
| function dumpNote(targetPath, noteId) { | ||||
|     console.log(`Dumping note ${noteId}`); | ||||
|  | ||||
|     const note = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||
|  | ||||
|     let safeTitle = sanitize(note.title); | ||||
|  | ||||
|     if (safeTitle.length > 20) { | ||||
|         safeTitle = safeTitle.substring(0, 20); | ||||
|     } | ||||
|  | ||||
|     let childTargetPath = targetPath + '/' + safeTitle; | ||||
|  | ||||
|     for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) { | ||||
|         childTargetPath = targetPath + '/' + safeTitle + '_' + i; | ||||
|     } | ||||
|  | ||||
|     existingPaths[childTargetPath] = true; | ||||
|  | ||||
|     try { | ||||
|         const {content} = sql.getRow("SELECT content FROM note_contents WHERE noteId = ?", [noteId]); | ||||
|  | ||||
|         if (!isContentEmpty(content)) { | ||||
|             const fileNameWithPath = getFileName(note, childTargetPath, safeTitle); | ||||
|  | ||||
|             fs.writeFileSync(fileNameWithPath, content); | ||||
|         } | ||||
|     } | ||||
|     catch (e) { | ||||
|         console.log(`Writing ${note.noteId} failed with error ${e.message}`); | ||||
|     } | ||||
|  | ||||
|     const childNoteIds = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ?", [noteId]); | ||||
|  | ||||
|     if (childNoteIds.length > 0) { | ||||
|         fs.mkdirSync(childTargetPath, { recursive: true }); | ||||
|  | ||||
|         for (const childNoteId of childNoteIds) { | ||||
|             dumpNote(childTargetPath, childNoteId); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function isContentEmpty(content) { | ||||
|     if (!content) { | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     if (typeof content === "string") { | ||||
|         return !content.trim() || content.trim() === '<p></p>'; | ||||
|     } | ||||
|     else if (Buffer.isBuffer(content)) { | ||||
|         return content.length === 0; | ||||
|     } | ||||
|     else { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function printHelp() { | ||||
|     console.log(`Trilium Notes DB dump tool. Usage: | ||||
| node dump-db.js PATH_TO_DOCUMENT_DB TARGET_PATH`); | ||||
| } | ||||
							
								
								
									
										33
									
								
								dump-db/inc/data_key.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								dump-db/inc/data_key.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import crypto from "crypto"; | ||||
| import sql from "./sql.js"; | ||||
|  | ||||
| function getDataKey(password) { | ||||
|     const passwordDerivedKey = getPasswordDerivedKey(password); | ||||
|  | ||||
|     const encryptedDataKey = getOption('encryptedDataKey'); | ||||
|  | ||||
|     const decryptedDataKey = decrypt(passwordDerivedKey, encryptedDataKey, 16); | ||||
|  | ||||
|     return decryptedDataKey; | ||||
| } | ||||
|  | ||||
| function getPasswordDerivedKey(password) { | ||||
|     const salt = getOption('passwordDerivedKeySalt'); | ||||
|  | ||||
|     return getScryptHash(password, salt); | ||||
| } | ||||
|  | ||||
| function getScryptHash(password, salt) { | ||||
|     const hashed = crypto.scryptSync(password, salt, 32, | ||||
|         {N: 16384, r:8, p:1}); | ||||
|  | ||||
|     return hashed; | ||||
| } | ||||
|  | ||||
| function getOption(name) { | ||||
|     return sql.getValue("SELECT value FROM options WHERE name = ?", name); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getDataKey | ||||
| }; | ||||
							
								
								
									
										79
									
								
								dump-db/inc/decrypt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								dump-db/inc/decrypt.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| import crypto from "crypto"; | ||||
|  | ||||
| function decryptString(dataKey, cipherText) { | ||||
|     const buffer = decrypt(dataKey, cipherText); | ||||
|  | ||||
|     if (buffer === null) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     const str = buffer.toString('utf-8'); | ||||
|  | ||||
|     if (str === 'false') { | ||||
|         throw new Error("Could not decrypt string."); | ||||
|     } | ||||
|  | ||||
|     return str; | ||||
| } | ||||
|  | ||||
| function decrypt(key, cipherText, ivLength = 13) { | ||||
|     if (cipherText === null) { | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     if (!key) { | ||||
|         return "[protected]"; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), 'base64'); | ||||
|         const iv = cipherTextBufferWithIv.slice(0, ivLength); | ||||
|  | ||||
|         const cipherTextBuffer = cipherTextBufferWithIv.slice(ivLength); | ||||
|  | ||||
|         const decipher = crypto.createDecipheriv('aes-128-cbc', pad(key), pad(iv)); | ||||
|  | ||||
|         const decryptedBytes = Buffer.concat([decipher.update(cipherTextBuffer), decipher.final()]); | ||||
|  | ||||
|         const digest = decryptedBytes.slice(0, 4); | ||||
|         const payload = decryptedBytes.slice(4); | ||||
|  | ||||
|         const computedDigest = shaArray(payload).slice(0, 4); | ||||
|  | ||||
|         if (!arraysIdentical(digest, computedDigest)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return payload; | ||||
|     } | ||||
|     catch (e) { | ||||
|         // recovery from https://github.com/zadam/trilium/issues/510 | ||||
|         if (e.message && e.message.includes("WRONG_FINAL_BLOCK_LENGTH")) { | ||||
|             log.info("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead"); | ||||
|  | ||||
|             return cipherText; | ||||
|         } | ||||
|         else { | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function arraysIdentical(a, b) { | ||||
|     let i = a.length; | ||||
|     if (i !== b.length) return false; | ||||
|     while (i--) { | ||||
|         if (a[i] !== b[i]) return false; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| function shaArray(content) { | ||||
|     // we use this as simple checksum and don't rely on its security so SHA-1 is good enough | ||||
|     return crypto.createHash('sha1').update(content).digest(); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     decrypt, | ||||
|     decryptString | ||||
| }; | ||||
							
								
								
									
										17
									
								
								dump-db/inc/sql.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								dump-db/inc/sql.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| const Database = require("better-sqlite3"); | ||||
| let dbConnection; | ||||
|  | ||||
| const openDatabase = (documentPath) => { dbConnection = new Database(documentPath, { readonly: true }) }; | ||||
|  | ||||
| const getRow = (query, params = []) => dbConnection.prepare(query).get(params); | ||||
| const getRows = (query, params = []) => dbConnection.prepare(query).all(params); | ||||
| const getValue = (query, params = []) => dbConnection.prepare(query).pluck().get(params); | ||||
| const getColumn = (query, params = []) => dbConnection.prepare(query).pluck().all(params); | ||||
|  | ||||
| module.exports = { | ||||
|     openDatabase, | ||||
|     getRow, | ||||
|     getRows, | ||||
|     getValue, | ||||
|     getColumn | ||||
| }; | ||||
							
								
								
									
										1139
									
								
								dump-db/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1139
									
								
								dump-db/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										24
									
								
								dump-db/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								dump-db/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| { | ||||
|   "name": "dump-db", | ||||
|   "version": "1.0.0", | ||||
|   "description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes", | ||||
|   "main": "dump-db.js", | ||||
|   "scripts": { | ||||
|     "test": "echo \"Error: no test specified\" && exit 1" | ||||
|   }, | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/zadam/trilium.git" | ||||
|   }, | ||||
|   "author": "zadam", | ||||
|   "license": "ISC", | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/zadam/trilium/issues" | ||||
|   }, | ||||
|   "homepage": "https://github.com/zadam/trilium/dump-db#readme", | ||||
|   "dependencies": { | ||||
|     "better-sqlite3": "7.5.0", | ||||
|     "mime-types": "2.1.34", | ||||
|     "sanitize-filename": "1.6.3" | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user