mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-27 01:28:59 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			168 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			168 lines
		
	
	
		
			5.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import fs from "fs";
 | |
| import sanitize from "sanitize-filename";
 | |
| import sql from "./sql.js";
 | |
| import decryptService from "./decrypt.js";
 | |
| import dataKeyService from "./data_key.js";
 | |
| import extensionService from "./extension.js";
 | |
| 
 | |
| function dumpDocument(documentPath: string, targetPath: string, options: { password: any; includeDeleted: any }) {
 | |
|     const stats = {
 | |
|         succeeded: 0,
 | |
|         failed: 0,
 | |
|         protected: 0,
 | |
|         deleted: 0
 | |
|     };
 | |
| 
 | |
|     validatePaths(documentPath, targetPath);
 | |
| 
 | |
|     sql.openDatabase(documentPath);
 | |
| 
 | |
|     const dataKey = dataKeyService.getDataKey(options.password);
 | |
| 
 | |
|     const existingPaths: Record<string, any> = {};
 | |
|     const noteIdToPath: Record<string, any> = {};
 | |
| 
 | |
|     dumpNote(targetPath, "root");
 | |
| 
 | |
|     printDumpResults(stats, options);
 | |
| 
 | |
|     function dumpNote(targetPath: any, noteId: any) {
 | |
|         console.log(`Reading note '${noteId}'`);
 | |
| 
 | |
|         let childTargetPath, noteRow, fileNameWithPath;
 | |
| 
 | |
|         try {
 | |
|             noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
 | |
| 
 | |
|             if (noteRow.isDeleted) {
 | |
|                 stats.deleted++;
 | |
| 
 | |
|                 if (!options.includeDeleted) {
 | |
|                     console.log(`Note '${noteId}' is deleted and --include-deleted option is not used, skipping.`);
 | |
| 
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (noteRow.isProtected) {
 | |
|                 stats.protected++;
 | |
| 
 | |
|                 noteRow.title = decryptService.decryptString(dataKey, noteRow.title);
 | |
|             }
 | |
| 
 | |
|             let safeTitle = sanitize(noteRow.title);
 | |
| 
 | |
|             if (safeTitle.length > 20) {
 | |
|                 safeTitle = safeTitle.substring(0, 20);
 | |
|             }
 | |
| 
 | |
|             childTargetPath = targetPath + "/" + safeTitle;
 | |
| 
 | |
|             for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) {
 | |
|                 childTargetPath = targetPath + "/" + safeTitle + "_" + i;
 | |
|             }
 | |
| 
 | |
|             existingPaths[childTargetPath] = true;
 | |
| 
 | |
|             if (noteRow.noteId in noteIdToPath) {
 | |
|                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`;
 | |
| 
 | |
|                 console.log(message);
 | |
| 
 | |
|                 fs.writeFileSync(childTargetPath, message);
 | |
| 
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             let { content } = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [noteRow.blobId]);
 | |
| 
 | |
|             if (content !== null && noteRow.isProtected && dataKey) {
 | |
|                 content = decryptService.decrypt(dataKey, content);
 | |
|             }
 | |
| 
 | |
|             if (isContentEmpty(content)) {
 | |
|                 console.log(`Note '${noteId}' is empty, skipping.`);
 | |
|             } else {
 | |
|                 fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle);
 | |
| 
 | |
|                 fs.writeFileSync(fileNameWithPath, content);
 | |
| 
 | |
|                 stats.succeeded++;
 | |
| 
 | |
|                 console.log(`Dumped note '${noteId}' into ${fileNameWithPath} successfully.`);
 | |
|             }
 | |
| 
 | |
|             noteIdToPath[noteId] = childTargetPath;
 | |
|         } catch (e: any) {
 | |
|             console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`);
 | |
| 
 | |
|             stats.failed++;
 | |
|         }
 | |
| 
 | |
|         const childNoteIds = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ?", [noteId]);
 | |
| 
 | |
|         if (childNoteIds.length > 0) {
 | |
|             if (childTargetPath === fileNameWithPath) {
 | |
|                 childTargetPath += "_dir";
 | |
|             }
 | |
| 
 | |
|             try {
 | |
|                 fs.mkdirSync(childTargetPath as string, { recursive: true });
 | |
|             } catch (e: any) {
 | |
|                 console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`);
 | |
|             }
 | |
| 
 | |
|             for (const childNoteId of childNoteIds) {
 | |
|                 dumpNote(childTargetPath, childNoteId);
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| function printDumpResults(stats: any, options: any) {
 | |
|     console.log("\n----------------------- STATS -----------------------");
 | |
|     console.log("Successfully dumpted notes:   ", stats.succeeded.toString().padStart(5, " "));
 | |
|     console.log("Protected notes:              ", stats.protected.toString().padStart(5, " "), options.password ? "" : "(skipped)");
 | |
|     console.log("Failed notes:                 ", stats.failed.toString().padStart(5, " "));
 | |
|     console.log("Deleted notes:                ", stats.deleted.toString().padStart(5, " "), options.includeDeleted ? "(dumped)" : "(at least, skipped)");
 | |
|     console.log("-----------------------------------------------------");
 | |
| 
 | |
|     if (!options.password && stats.protected > 0) {
 | |
|         console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped.");
 | |
|     }
 | |
| }
 | |
| 
 | |
| function isContentEmpty(content: any) {
 | |
|     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 validatePaths(documentPath: string, targetPath: string) {
 | |
|     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);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| export default {
 | |
|     dumpDocument
 | |
| };
 | 
