chore(nx): move all monorepo-style in subfolder for processing

This commit is contained in:
Elian Doran
2025-04-22 10:06:06 +03:00
parent 2e200eab39
commit 62dbcc0a2e
1469 changed files with 16 additions and 16 deletions

View File

@@ -1,34 +0,0 @@
# 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.18.0, but it will probably work on other versions as well.
```
npm install
```
## Running
See output of `npx tsx dump.ts --help`:
```
dump-db.ts <path_to_document> <target_directory>
dump the contents of document.db into the target directory
Positionals:
path_to_document path to the document.db
target_directory path of the directory into which the notes should be dumped
Options:
--help Show help [boolean]
--version Show version number [boolean]
--password Set password to be able to decrypt protected notes.[string]
--include-deleted If set to true, dump also deleted notes.
[boolean] [default: false]
```

View File

@@ -1,37 +0,0 @@
#!/usr/bin/env node
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import dumpService from "./inc/dump.js";
yargs(hideBin(process.argv))
.command(
"$0 <path_to_document> <target_directory>",
"dump the contents of document.db into the target directory",
(yargs) => {
return yargs
.option("path_to_document", { alias: "p", describe: "path to the document.db", type: "string", demandOption: true })
.option("target_directory", { alias: "t", describe: "path of the directory into which the notes should be dumped", type: "string", demandOption: true });
},
(argv) => {
try {
dumpService.dumpDocument(argv.path_to_document, argv.target_directory, {
includeDeleted: argv.includeDeleted,
password: argv.password
});
} catch (e) {
console.error(`Unrecoverable error:`, e);
process.exit(1);
}
}
)
.option("password", {
type: "string",
description: "Set password to be able to decrypt protected notes."
})
.option("include-deleted", {
type: "boolean",
default: false,
description: "If set to true, dump also deleted notes."
})
.parse();

View File

@@ -1,41 +0,0 @@
import crypto from "crypto";
import sql from "./sql.js";
import decryptService from "./decrypt.js";
function getDataKey(password: any) {
if (!password) {
return null;
}
try {
const passwordDerivedKey = getPasswordDerivedKey(password);
const encryptedDataKey = getOption("encryptedDataKey");
const decryptedDataKey = decryptService.decrypt(passwordDerivedKey, encryptedDataKey);
return decryptedDataKey;
} catch (e: any) {
throw new Error(`Cannot read data key, the entered password might be wrong. The underlying error: '${e.message}', stack:\n${e.stack}`);
}
}
function getPasswordDerivedKey(password: any) {
const salt = getOption("passwordDerivedKeySalt");
return getScryptHash(password, salt);
}
function getScryptHash(password: any, salt: any) {
const hashed = crypto.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
return hashed;
}
function getOption(name: string) {
return sql.getValue("SELECT value FROM options WHERE name = ?", [name]);
}
export default {
getDataKey
};

View File

@@ -1,90 +0,0 @@
import crypto from "crypto";
function decryptString(dataKey: any, cipherText: any) {
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: any, cipherText: any) {
if (cipherText === null) {
return null;
}
if (!key) {
return "[protected]";
}
try {
const cipherTextBufferWithIv = Buffer.from(cipherText.toString(), "base64");
// old encrypted data can have IV of length 13, see some details here: https://github.com/zadam/trilium/issues/3017
const ivLength = cipherTextBufferWithIv.length % 16 === 0 ? 16 : 13;
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: any) {
// recovery from https://github.com/zadam/trilium/issues/510
if (e.message?.includes("WRONG_FINAL_BLOCK_LENGTH") || e.message?.includes("wrong final block length")) {
console.log("Caught WRONG_FINAL_BLOCK_LENGTH, returning cipherText instead");
return cipherText;
} else {
throw e;
}
}
}
function pad(data: any) {
if (data.length > 16) {
data = data.slice(0, 16);
} else if (data.length < 16) {
const zeros = Array(16 - data.length).fill(0);
data = Buffer.concat([data, Buffer.from(zeros)]);
}
return Buffer.from(data);
}
function arraysIdentical(a: any, b: any) {
let i = a.length;
if (i !== b.length) return false;
while (i--) {
if (a[i] !== b[i]) return false;
}
return true;
}
function shaArray(content: any) {
// 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();
}
export default {
decrypt,
decryptString
};

View File

@@ -1,167 +0,0 @@
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
};

View File

@@ -1,36 +0,0 @@
import path from "path";
import mimeTypes from "mime-types";
function getFileName(note: any, childTargetPath: string, safeTitle: string) {
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;
}
export default {
getFileName
};

View File

@@ -1,20 +0,0 @@
import Database, { Database as DatabaseType } from "better-sqlite3";
let dbConnection: DatabaseType;
const openDatabase = (documentPath: string) => {
dbConnection = new Database(documentPath, { readonly: true });
};
const getRow = (query: string, params: string[] = []): Record<string, any> => dbConnection.prepare(query).get(params) as Record<string, any>;
const getRows = (query: string, params = []) => dbConnection.prepare(query).all(params);
const getValue = (query: string, params: string[] = []) => dbConnection.prepare(query).pluck().get(params);
const getColumn = (query: string, params: string[] = []) => dbConnection.prepare(query).pluck().all(params);
export default {
openDatabase,
getRow,
getRows,
getValue,
getColumn
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
{
"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.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/TriliumNext/Notes.git"
},
"author": "TriliumNext",
"license": "ISC",
"bugs": {
"url": "https://github.com/TriliumNext/Notes/issues"
},
"homepage": "https://github.com/TriliumNext/Notes/blob/master/dump-db/README.md",
"dependencies": {
"better-sqlite3": "^11.1.2",
"mime-types": "^3.0.0",
"sanitize-filename": "^1.6.3",
"tsx": "^4.19.3",
"yargs": "^17.3.1"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.11",
"@types/mime-types": "^2.1.4",
"@types/yargs": "^17.0.33"
}
}

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "ES6",
"strict": true
}
}