full notes file system architecture

This commit is contained in:
nicwands
2026-02-23 16:55:38 -05:00
parent 9ac9d73b0a
commit 0ab0620da8
12 changed files with 666 additions and 293 deletions

View File

@@ -1,73 +1,125 @@
"use strict";
const utils = require("@electron-toolkit/utils");
const electron = require("electron");
const fs = require("fs");
const fs = require("fs/promises");
const path = require("path");
const BASE_DIR = path.join(electron.app.getPath("userData"), "notes-storage");
const ensureBaseDir = () => {
if (!fs.existsSync(BASE_DIR)) {
fs.mkdirSync(BASE_DIR, { recursive: true });
const matter = require("gray-matter");
const flexsearch = require("flexsearch");
const crypto = require("crypto");
class NotesAPI {
constructor() {
this.notesDir = path.join(electron.app.getPath("userData"), "notes");
this.notesCache = /* @__PURE__ */ new Map();
this.index = new flexsearch.Index({
tokenize: "tolerant",
resolution: 9
});
}
};
const sanitizeRelativePath = (relativePath) => {
const resolved = path.join(BASE_DIR, relativePath);
if (!resolved.startsWith(BASE_DIR)) {
throw new Error("Invalid path");
async init() {
await fs.mkdir(this.notesDir, { recursive: true });
await this._loadAllNotes();
}
return resolved;
};
const readAllNotesRecursive = (dir = BASE_DIR, base = BASE_DIR) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(readAllNotesRecursive(fullPath, base));
}
if (entry.isFile() && entry.name.endsWith(".md")) {
const content = fs.readFileSync(fullPath, "utf-8");
results.push({
name: entry.name,
path: path.relative(base, fullPath),
content
});
async _loadAllNotes() {
const files = await fs.readdir(this.notesDir);
for (const file of files) {
if (!file.endsWith(".md")) continue;
const fullPath = path.join(this.notesDir, file);
const raw = await fs.readFile(fullPath, "utf8");
const parsed = matter(raw);
const note = {
...parsed.data,
content: parsed.content
};
this.notesCache.set(note.id, note);
this.index.add(note.id, note.title + note.content);
}
}
return results;
};
const createNote = (relativePath, content = "") => {
const fullPath = sanitizeRelativePath(relativePath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content, "utf-8");
return true;
};
const createDirectory = (relativePath) => {
const fullPath = sanitizeRelativePath(relativePath);
fs.mkdirSync(fullPath, { recursive: true });
return true;
};
const readNote = (relativePath) => {
const fullPath = sanitizeRelativePath(relativePath);
if (!fs.existsSync(fullPath)) {
createNote(relativePath);
async _writeNoteFile(note) {
const filePath = path.join(this.notesDir, `${note.id}.md`);
const fileContent = matter.stringify(note.content, {
id: note.id,
title: note.title,
category: note.category ?? null,
createdAt: note.createdAt,
updatedAt: note.updatedAt
});
await fs.writeFile(filePath, fileContent, "utf8");
}
return fs.readFileSync(fullPath, "utf-8");
};
const updateNote = (relativePath, content) => {
const fullPath = sanitizeRelativePath(relativePath);
if (!fs.existsSync(fullPath)) {
throw new Error("Note does not exist");
/* -----------------------
Public API
------------------------*/
getCategories() {
const categories = /* @__PURE__ */ new Set();
for (const note of this.notesCache.values()) {
if (note.category) {
categories.add(note.category);
}
}
return Array.from(categories).sort();
}
fs.writeFileSync(fullPath, content, "utf-8");
return true;
};
const notesAPI = {
readAllNotesRecursive,
createNote,
createDirectory,
readNote,
updateNote
};
getCategoryNotes(categoryName) {
return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
}
getNote(id) {
return this.notesCache.get(id) ?? null;
}
async createNote(metadata = {}, content = "") {
const id = crypto.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString();
const note = {
id,
title: metadata.title || "Untitled",
category: metadata.category || null,
createdAt: now,
updatedAt: now,
content
};
console.log(note);
this.notesCache.set(id, note);
this.index.add(id, note.title + "\n" + content);
await this._writeNoteFile(note);
return note;
}
async deleteNote(id) {
const filePath = path.join(this.notesDir, `${id}.md`);
await fs.unlink(filePath);
this.notesCache.delete(id);
this.index.remove(id);
}
async updateNote(id, content) {
const note = this.notesCache.get(id);
if (!note) throw new Error("Note not found");
note.content = content;
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
this.index.update(id, note.title + "\n" + content);
await this._writeNoteFile(note);
return note;
}
async updateNoteMetadata(id, updates = {}) {
const note = this.notesCache.get(id);
if (!note) throw new Error("Note not found");
const allowedFields = ["title", "category"];
for (const key of Object.keys(updates)) {
if (!allowedFields.includes(key)) {
throw new Error(`Invalid metadata field: ${key}`);
}
}
if (updates.title !== void 0) {
note.title = updates.title;
}
if (updates.category !== void 0) {
note.category = updates.category;
}
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
this.index.update(id, note.title + "\n" + note.content);
await this._writeNoteFile(note);
return note;
}
search(query) {
const ids = this.index.search(query);
return ids.map((id) => this.notesCache.get(id));
}
}
const preloadPath = path.join(__dirname, "../preload/index.js");
const rendererPath = path.join(__dirname, "../renderer/index.html");
function createWindow() {
@@ -121,14 +173,15 @@ electron.app.whenReady().then(() => {
utils.optimizer.watchWindowShortcuts(window);
});
createWindow();
ensureBaseDir();
electron.app.on("activate", function() {
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
});
electron.ipcMain.on("open-note-window", (_, noteId) => {
createNoteWindow(noteId);
});
electron.ipcMain.handle("notesAPI:call", async (_, method, args) => {
const notesAPI = new NotesAPI();
notesAPI.init();
electron.ipcMain.handle("notesAPI:call", (_, method, args) => {
if (!notesAPI[method]) {
throw new Error("Invalid method");
}