full notes file system architecture
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user