dump.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. const fs = require("fs");
  2. const sanitize = require("sanitize-filename");
  3. const sql = require('./sql.js');
  4. const decryptService = require('./decrypt.js');
  5. const dataKeyService = require('./data_key.js');
  6. const extensionService = require('./extension.js');
  7. function dumpDocument(documentPath, targetPath, options) {
  8. const stats = {
  9. succeeded: 0,
  10. failed: 0,
  11. protected: 0,
  12. deleted: 0
  13. };
  14. validatePaths(documentPath, targetPath);
  15. sql.openDatabase(documentPath);
  16. const dataKey = dataKeyService.getDataKey(options.password);
  17. const existingPaths = {};
  18. const noteIdToPath = {};
  19. dumpNote(targetPath, 'root');
  20. printDumpResults(stats, options);
  21. function dumpNote(targetPath, noteId) {
  22. console.log(`Reading note '${noteId}'`);
  23. let childTargetPath, noteRow, fileNameWithPath;
  24. try {
  25. noteRow = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
  26. if (noteRow.isDeleted) {
  27. stats.deleted++;
  28. if (!options.includeDeleted) {
  29. console.log(`Note '${noteId}' is deleted and --include-deleted option is not used, skipping.`);
  30. return;
  31. }
  32. }
  33. if (noteRow.isProtected) {
  34. stats.protected++;
  35. noteRow.title = decryptService.decryptString(dataKey, noteRow.title);
  36. }
  37. let safeTitle = sanitize(noteRow.title);
  38. if (safeTitle.length > 20) {
  39. safeTitle = safeTitle.substring(0, 20);
  40. }
  41. childTargetPath = targetPath + '/' + safeTitle;
  42. for (let i = 1; i < 100000 && childTargetPath in existingPaths; i++) {
  43. childTargetPath = targetPath + '/' + safeTitle + '_' + i;
  44. }
  45. existingPaths[childTargetPath] = true;
  46. if (noteRow.noteId in noteIdToPath) {
  47. const message = `Note '${noteId}' has been already dumped to ${noteIdToPath[noteRow.noteId]}`;
  48. console.log(message);
  49. fs.writeFileSync(childTargetPath, message);
  50. return;
  51. }
  52. let {content} = sql.getRow("SELECT content FROM blobs WHERE blobId = ?", [noteRow.blobId]);
  53. if (content !== null && noteRow.isProtected && dataKey) {
  54. content = decryptService.decrypt(dataKey, content);
  55. }
  56. if (isContentEmpty(content)) {
  57. console.log(`Note '${noteId}' is empty, skipping.`);
  58. } else {
  59. fileNameWithPath = extensionService.getFileName(noteRow, childTargetPath, safeTitle);
  60. fs.writeFileSync(fileNameWithPath, content);
  61. stats.succeeded++;
  62. console.log(`Dumped note '${noteId}' into ${fileNameWithPath} successfully.`);
  63. }
  64. noteIdToPath[noteId] = childTargetPath;
  65. }
  66. catch (e) {
  67. console.error(`DUMPERROR: Writing '${noteId}' failed with error '${e.message}':\n${e.stack}`);
  68. stats.failed++;
  69. }
  70. const childNoteIds = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ?", [noteId]);
  71. if (childNoteIds.length > 0) {
  72. if (childTargetPath === fileNameWithPath) {
  73. childTargetPath += '_dir';
  74. }
  75. try {
  76. fs.mkdirSync(childTargetPath, {recursive: true});
  77. }
  78. catch (e) {
  79. console.error(`DUMPERROR: Creating directory ${childTargetPath} failed with error '${e.message}'`);
  80. }
  81. for (const childNoteId of childNoteIds) {
  82. dumpNote(childTargetPath, childNoteId);
  83. }
  84. }
  85. }
  86. }
  87. function printDumpResults(stats, options) {
  88. console.log('\n----------------------- STATS -----------------------');
  89. console.log('Successfully dumpted notes: ', stats.succeeded.toString().padStart(5, ' '));
  90. console.log('Protected notes: ', stats.protected.toString().padStart(5, ' '), options.password ? '' : '(skipped)');
  91. console.log('Failed notes: ', stats.failed.toString().padStart(5, ' '));
  92. console.log('Deleted notes: ', stats.deleted.toString().padStart(5, ' '), options.includeDeleted ? "(dumped)" : "(at least, skipped)");
  93. console.log('-----------------------------------------------------');
  94. if (!options.password && stats.protected > 0) {
  95. console.log("\nWARNING: protected notes are present in the document but no password has been provided. Protected notes have not been dumped.");
  96. }
  97. }
  98. function isContentEmpty(content) {
  99. if (!content) {
  100. return true;
  101. }
  102. if (typeof content === "string") {
  103. return !content.trim() || content.trim() === '<p></p>';
  104. }
  105. else if (Buffer.isBuffer(content)) {
  106. return content.length === 0;
  107. }
  108. else {
  109. return false;
  110. }
  111. }
  112. function validatePaths(documentPath, targetPath) {
  113. if (!fs.existsSync(documentPath)) {
  114. console.error(`Path to document '${documentPath}' has not been found. Run with --help to see usage.`);
  115. process.exit(1);
  116. }
  117. if (!fs.existsSync(targetPath)) {
  118. const ret = fs.mkdirSync(targetPath, {recursive: true});
  119. if (!ret) {
  120. console.error(`Target path '${targetPath}' could not be created. Run with --help to see usage.`);
  121. process.exit(1);
  122. }
  123. }
  124. }
  125. module.exports = {
  126. dumpDocument
  127. };