feat(electron): main process, vault, search index, links, import, capture

Vault on plain .md files with FTS5 index, link rewriting, canvas files,
history/trash/backup, link previews, image fetch, tasks and unlinked
mentions, Notion/Obsidian importer, quick-capture window and shortcut.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Your NameandClaude Sonnet 5.5 committed 2026-10-15 11:57:09 +02:00
1 parent 0b5f8bfa8b
commit 313c41f867
16 files changed
+2706 -15

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
'use strict';
// Backups: the notes folder as one .zip, written without any dependency (deflate from zlib, a small zip writer).
// Version history and the trash are left out (they are large and can be rebuilt or are meant to be gone),
// and so is the search index.
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const path = require('node:path');
const zlib = require('node:zlib');
const SKIP = ['.notes/history', '.notes/trash', '.notes/index.sqlite', '.notes/index.sqlite-wal', '.notes/index.sqlite-shm'];
let table = null;
function crc32(buf) {
if (!table) {
table = new Uint32Array(256);
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c >>> 0;
}
}
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function dosTime(d) {
const year = Math.max(1980, d.getFullYear());
return {
time: (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1),
date: ((year - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate()
};
}
async function* walk(root, rel = '') {
let entries;
try { entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true }); } catch { return; }
for (const e of entries) {
const r = rel ? `${rel}/${e.name}` : e.name;
if (SKIP.includes(r)) continue;
if (e.isDirectory()) yield* walk(root, r);
else if (e.isFile()) yield r;
}
}
// Zip `root` into `out`. Returns the number of files.
async function zipFolder(root, out) {
await fsp.mkdir(path.dirname(out), { recursive: true });
const tmp = out + '.part';
const fd = await fsp.open(tmp, 'w');
const central = [];
let offset = 0, count = 0;
try {
for await (const rel of walk(root)) {
const file = path.join(root, rel);
let data, st;
try { data = await fsp.readFile(file); st = await fsp.stat(file); } catch { continue; } // changed while we ran
const packed = zlib.deflateRawSync(data);
const useDeflate = packed.length < data.length;
const body = useDeflate ? packed : data;
const name = Buffer.from(rel, 'utf8');
const { time, date } = dosTime(st.mtime);
const crc = crc32(data);
const head = Buffer.alloc(30);
head.writeUInt32LE(0x04034b50, 0);
head.writeUInt16LE(20, 4);
head.writeUInt16LE(0x0800, 6); // UTF-8 names
head.writeUInt16LE(useDeflate ? 8 : 0, 8);
head.writeUInt16LE(time, 10);
head.writeUInt16LE(date, 12);
head.writeUInt32LE(crc, 14);
head.writeUInt32LE(body.length, 18);
head.writeUInt32LE(data.length, 22);
head.writeUInt16LE(name.length, 26);
await fd.write(head);
await fd.write(name);
await fd.write(body);
central.push({ name, crc, size: data.length, packed: body.length, method: useDeflate ? 8 : 0, time, date, offset });
offset += 30 + name.length + body.length;
count++;
}
let size = 0;
for (const c of central) {
const h = Buffer.alloc(46);
h.writeUInt32LE(0x02014b50, 0);
h.writeUInt16LE(20, 4);
h.writeUInt16LE(20, 6);
h.writeUInt16LE(0x0800, 8);
h.writeUInt16LE(c.method, 10);
h.writeUInt16LE(c.time, 12);
h.writeUInt16LE(c.date, 14);
h.writeUInt32LE(c.crc, 16);
h.writeUInt32LE(c.packed, 20);
h.writeUInt32LE(c.size, 24);
h.writeUInt16LE(c.name.length, 28);
h.writeUInt32LE(c.offset, 42);
await fd.write(h);
await fd.write(c.name);
size += 46 + c.name.length;
}
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(count, 8);
end.writeUInt16LE(count, 10);
end.writeUInt32LE(size, 12);
end.writeUInt32LE(offset, 16);
await fd.write(end);
} finally {
await fd.close();
}
await fsp.rename(tmp, out);
return count;
}
const pad = (n) => String(n).padStart(2, '0');
function stamp(d = new Date()) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}${pad(d.getMinutes())}`;
}
// Back up the folder into `dir` as "<folder name> <date time>.zip", then keep only the newest `keep` of this folder's.
async function backup(root, dir, keep) {
const base = path.basename(root);
const out = path.join(dir, `${base} ${stamp()}.zip`);
const count = await zipFolder(root, out);
const mine = (await fsp.readdir(dir)).filter((f) => f.startsWith(base + ' ') && /\d{4}-\d{2}-\d{2} \d{4}\.zip$/.test(f)).sort().reverse();
for (const old of mine.slice(Math.max(1, keep))) await fsp.unlink(path.join(dir, old)).catch(() => {});
return { file: out, count };
}
const EVERY = { daily: 20 * 3600e3, weekly: 6.5 * 24 * 3600e3 };
// Whether a backup is due: every = 'off' | 'daily' | 'weekly'; last = time of the previous one (ms) or 0.
const due = (every, last, now = Date.now()) => !!EVERY[every] && now - (last || 0) >= EVERY[every];
module.exports = { zipFolder, backup, due, crc32, stamp };