| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  | 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 }) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |     const stats = { | 
					
						
							|  |  |  |         succeeded: 0, | 
					
						
							|  |  |  |         failed: 0, | 
					
						
							|  |  |  |         protected: 0, | 
					
						
							|  |  |  |         deleted: 0 | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     validatePaths(documentPath, targetPath); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sql.openDatabase(documentPath); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const dataKey = dataKeyService.getDataKey(options.password); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  |     const existingPaths: Record<string, any> = {}; | 
					
						
							|  |  |  |     const noteIdToPath: Record<string, any> = {}; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     dumpNote(targetPath, "root"); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     printDumpResults(stats, options); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  |     function dumpNote(targetPath: any, noteId: any) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |         console.log(`Reading note '${noteId}'`); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |         let childTargetPath, noteRow, fileNameWithPath; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         try { | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             if (noteRow.isDeleted) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |                 stats.deleted++; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 if (!options.includeDeleted) { | 
					
						
							|  |  |  |                     console.log(`Note '${noteId}' is deleted and --include-deleted option is not used, skipping.`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     return; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             if (noteRow.isProtected) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |                 stats.protected++; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |                 noteRow.title = decryptService.decryptString(dataKey, noteRow.title); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             let safeTitle = sanitize(noteRow.title); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             if (safeTitle.length > 20) { | 
					
						
							|  |  |  |                 safeTitle = safeTitle.substring(0, 20); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             childTargetPath = targetPath + "/" + safeTitle; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |             for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |                 childTargetPath = targetPath + "/" + safeTitle + "_" + i; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             existingPaths[childTargetPath] = true; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             if (noteRow.noteId in noteIdToPath) { | 
					
						
							|  |  |  |                 const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 console.log(message); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 fs.writeFileSync(childTargetPath, message); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 return; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-03-30 10:49:40 +02:00
										 |  |  |             let { content } = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [noteRow.blobId]); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |             if (content !== null && noteRow.isProtected && dataKey) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |                 content = decryptService.decrypt(dataKey, content); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (isContentEmpty(content)) { | 
					
						
							|  |  |  |                 console.log(`Note '${noteId}' is empty, skipping.`); | 
					
						
							|  |  |  |             } else { | 
					
						
							| 
									
										
										
										
											2023-06-05 09:23:42 +02:00
										 |  |  |                 fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |                 fs.writeFileSync(fileNameWithPath, content); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 stats.succeeded++; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 console.log(`Dumped note '${noteId}' into ${fileNameWithPath} successfully.`); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             noteIdToPath[noteId] = childTargetPath; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         } catch (e: any) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |             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) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |                 childTargetPath += "_dir"; | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             try { | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  |                 fs.mkdirSync(childTargetPath as string, { recursive: true }); | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |             } catch (e: any) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |                 console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             for (const childNoteId of childNoteIds) { | 
					
						
							|  |  |  |                 dumpNote(childTargetPath, childNoteId); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  | function printDumpResults(stats: any, options: any) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     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("-----------------------------------------------------"); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  | function isContentEmpty(content: any) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |     if (!content) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (typeof content === "string") { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |         return !content.trim() || content.trim() === "<p></p>"; | 
					
						
							|  |  |  |     } else if (Buffer.isBuffer(content)) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |         return content.length === 0; | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     } else { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  | function validatePaths(documentPath: string, targetPath: string) { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |     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)) { | 
					
						
							| 
									
										
										
										
											2024-03-30 10:49:40 +02:00
										 |  |  |         const ret = fs.mkdirSync(targetPath, { recursive: true }); | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if (!ret) { | 
					
						
							|  |  |  |             console.error(`Target path '${targetPath}' could not be created. Run with --help to see usage.`); | 
					
						
							|  |  |  |             process.exit(1); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-10 18:23:49 +02:00
										 |  |  | export default { | 
					
						
							| 
									
										
										
										
											2022-02-12 22:20:15 +01:00
										 |  |  |     dumpDocument | 
					
						
							|  |  |  | }; |