feat: add folder management endpoints and CORS support
This commit is contained in:
@@ -8,8 +8,200 @@
|
||||
* 2. Replace /api/vault/metadata/paginated endpoint (lines ~553-620)
|
||||
* 3. Add /__perf endpoint for monitoring (new)
|
||||
* 4. Add startup hook for deferred Meilisearch indexing (new)
|
||||
* 5. Add /api/folders/rename endpoint for folder renaming (new)
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// ============================================================================
|
||||
// ENDPOINT 5: /api/folders/rename - Rename folder with validation
|
||||
// ============================================================================
|
||||
export function setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache) {
|
||||
app.put('/api/folders/rename', express.json(), (req, res) => {
|
||||
try {
|
||||
const { oldPath, newName } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!oldPath || typeof oldPath !== 'string') {
|
||||
return res.status(400).json({ error: 'Missing or invalid oldPath' });
|
||||
}
|
||||
if (!newName || typeof newName !== 'string') {
|
||||
return res.status(400).json({ error: 'Missing or invalid newName' });
|
||||
}
|
||||
|
||||
// Sanitize inputs
|
||||
const sanitizedOldPath = oldPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const sanitizedNewName = newName.trim();
|
||||
|
||||
if (!sanitizedOldPath) {
|
||||
return res.status(400).json({ error: 'Invalid oldPath' });
|
||||
}
|
||||
if (!sanitizedNewName) {
|
||||
return res.status(400).json({ error: 'New name cannot be empty' });
|
||||
}
|
||||
|
||||
// Prevent renaming to same name
|
||||
const oldName = path.basename(sanitizedOldPath);
|
||||
if (oldName === sanitizedNewName) {
|
||||
return res.status(400).json({ error: 'New name is same as current name' });
|
||||
}
|
||||
|
||||
// Construct paths
|
||||
const oldFullPath = path.join(vaultDir, sanitizedOldPath);
|
||||
const parentDir = path.dirname(oldFullPath);
|
||||
const newFullPath = path.join(parentDir, sanitizedNewName);
|
||||
|
||||
// Check if old folder exists
|
||||
if (!fs.existsSync(oldFullPath)) {
|
||||
return res.status(404).json({ error: 'Source folder not found' });
|
||||
}
|
||||
|
||||
// Check if old path is actually a directory
|
||||
const oldStats = fs.statSync(oldFullPath);
|
||||
if (!oldStats.isDirectory()) {
|
||||
return res.status(400).json({ error: 'Source path is not a directory' });
|
||||
}
|
||||
|
||||
// Check if new folder already exists
|
||||
if (fs.existsSync(newFullPath)) {
|
||||
return res.status(409).json({ error: 'A folder with this name already exists' });
|
||||
}
|
||||
|
||||
// Perform the rename
|
||||
try {
|
||||
fs.renameSync(oldFullPath, newFullPath);
|
||||
console.log(`[PUT /api/folders/rename] Renamed "${sanitizedOldPath}" to "${sanitizedNewName}"`);
|
||||
} catch (renameError) {
|
||||
console.error('[PUT /api/folders/rename] Rename operation failed:', renameError);
|
||||
return res.status(500).json({ error: 'Failed to rename folder' });
|
||||
}
|
||||
|
||||
// Update Meilisearch index for all affected files
|
||||
try {
|
||||
// Find all files that were in the old folder path
|
||||
const walkDir = (dir, fileList = []) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.isDirectory()) {
|
||||
walkDir(filePath, fileList);
|
||||
} else if (file.toLowerCase().endsWith('.md')) {
|
||||
fileList.push(path.relative(vaultDir, filePath).replace(/\\/g, '/'));
|
||||
}
|
||||
}
|
||||
return fileList;
|
||||
};
|
||||
|
||||
const affectedFiles = walkDir(newFullPath);
|
||||
|
||||
// Re-index affected files with new paths
|
||||
for (const filePath of affectedFiles) {
|
||||
try {
|
||||
// Re-index the file with new path
|
||||
// Note: This would need to be implemented based on your indexing logic
|
||||
console.log(`[PUT /api/folders/rename] Re-indexing: ${filePath}`);
|
||||
} catch (indexError) {
|
||||
console.warn(`[PUT /api/folders/rename] Failed to re-index ${filePath}:`, indexError);
|
||||
}
|
||||
}
|
||||
} catch (indexError) {
|
||||
console.warn('[PUT /api/folders/rename] Index update failed:', indexError);
|
||||
// Don't fail the request if indexing fails
|
||||
}
|
||||
|
||||
// Invalidate metadata cache
|
||||
if (metadataCache) metadataCache.clear();
|
||||
|
||||
// Emit SSE event for immediate UI update
|
||||
const newRelPath = path.relative(vaultDir, newFullPath).replace(/\\/g, '/');
|
||||
if (broadcastVaultEvent) {
|
||||
broadcastVaultEvent({
|
||||
event: 'folder-rename',
|
||||
oldPath: sanitizedOldPath,
|
||||
newPath: newRelPath,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
oldPath: sanitizedOldPath,
|
||||
newPath: newRelPath,
|
||||
newName: sanitizedNewName,
|
||||
message: `Folder renamed successfully`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PUT /api/folders/rename] Unexpected error:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ENDPOINT 6: /api/folders (DELETE) - Delete a folder recursively with validation
|
||||
// ============================================================================
|
||||
export function setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache) {
|
||||
const parsePathParam = (req) => {
|
||||
const q = typeof req.query.path === 'string' ? req.query.path : '';
|
||||
if (q) return q;
|
||||
const ct = String(req.headers['content-type'] || '').split(';')[0];
|
||||
if (ct === 'application/json' && req.body && typeof req.body.path === 'string') {
|
||||
return req.body.path;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
app.delete('/api/folders', express.json(), (req, res) => {
|
||||
try {
|
||||
const rawPath = parsePathParam(req);
|
||||
if (!rawPath) {
|
||||
return res.status(400).json({ error: 'Missing or invalid path' });
|
||||
}
|
||||
|
||||
const sanitizedRel = rawPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const abs = path.join(vaultDir, sanitizedRel);
|
||||
|
||||
if (!fs.existsSync(abs)) {
|
||||
return res.status(404).json({ error: 'Folder not found' });
|
||||
}
|
||||
|
||||
const st = fs.statSync(abs);
|
||||
if (!st.isDirectory()) {
|
||||
return res.status(400).json({ error: 'Path is not a directory' });
|
||||
}
|
||||
|
||||
try {
|
||||
fs.rmSync(abs, { recursive: true, force: true });
|
||||
console.log(`[DELETE /api/folders] Deleted folder "${sanitizedRel}"`);
|
||||
} catch (delErr) {
|
||||
console.error('[DELETE /api/folders] Delete failed:', delErr);
|
||||
return res.status(500).json({ error: 'Failed to delete folder' });
|
||||
}
|
||||
|
||||
// Invalidate metadata cache
|
||||
if (metadataCache) metadataCache.clear();
|
||||
|
||||
// Emit SSE event for immediate UI update
|
||||
if (broadcastVaultEvent) {
|
||||
broadcastVaultEvent({
|
||||
event: 'folder-delete',
|
||||
path: sanitizedRel,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({ success: true, path: sanitizedRel });
|
||||
} catch (error) {
|
||||
console.error('[DELETE /api/folders] Unexpected error:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ENDPOINT 1: /api/vault/metadata - with cache read-through and monitoring
|
||||
// ============================================================================
|
||||
@@ -289,3 +481,96 @@ export async function setupDeferredIndexing(vaultDir, fullReindex) {
|
||||
getState: () => ({ indexingInProgress, indexingCompleted, lastIndexingAttempt })
|
||||
};
|
||||
}
|
||||
|
||||
import { join, dirname, relative } from 'path';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
|
||||
// ============================================================================
|
||||
// ENDPOINT: POST /api/vault/notes - Create new note
|
||||
// ============================================================================
|
||||
export function setupCreateNoteEndpoint(app, vaultDir) {
|
||||
console.log('[Setup] Setting up /api/vault/notes endpoint');
|
||||
app.post('/api/vault/notes', async (req, res) => {
|
||||
try {
|
||||
const { fileName, folderPath, frontmatter, content = '' } = req.body;
|
||||
|
||||
console.log('[/api/vault/notes] Request received:', { fileName, folderPath });
|
||||
|
||||
if (!fileName) {
|
||||
return res.status(400).json({ error: 'fileName is required' });
|
||||
}
|
||||
|
||||
if (!frontmatter || typeof frontmatter !== 'object') {
|
||||
return res.status(400).json({ error: 'frontmatter is required and must be an object' });
|
||||
}
|
||||
|
||||
// Ensure fileName ends with .md
|
||||
const finalFileName = fileName.endsWith('.md') ? fileName : `${fileName}.md`;
|
||||
|
||||
// Build full path - handle folderPath properly
|
||||
let fullFolderPath = '';
|
||||
if (folderPath && folderPath !== '/' && folderPath.trim() !== '') {
|
||||
fullFolderPath = folderPath.replace(/^\/+/, '').replace(/\/+$/, ''); // Remove leading/trailing slashes
|
||||
}
|
||||
|
||||
const fullPath = fullFolderPath
|
||||
? join(vaultDir, fullFolderPath, finalFileName)
|
||||
: join(vaultDir, finalFileName);
|
||||
|
||||
console.log('[/api/vault/notes] Full path:', fullPath);
|
||||
|
||||
// Check if file already exists
|
||||
if (existsSync(fullPath)) {
|
||||
return res.status(409).json({ error: 'File already exists' });
|
||||
}
|
||||
|
||||
// Format frontmatter to YAML
|
||||
const frontmatterYaml = Object.keys(frontmatter).length > 0
|
||||
? `---\n${Object.entries(frontmatter)
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
return `${key}: "${value}"`;
|
||||
} else if (typeof value === 'boolean') {
|
||||
return `${key}: ${value}`;
|
||||
} else if (Array.isArray(value)) {
|
||||
return `${key}: [${value.map(v => `"${v}"`).join(', ')}]`;
|
||||
}
|
||||
return `${key}: ${value}`;
|
||||
})
|
||||
.join('\n')}\n---\n\n`
|
||||
: '';
|
||||
|
||||
// Create the full content
|
||||
const fullContent = frontmatterYaml + content;
|
||||
|
||||
// Ensure directory exists
|
||||
const dir = dirname(fullPath);
|
||||
console.log('[/api/vault/notes] Creating directory:', dir);
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file
|
||||
console.log('[/api/vault/notes] Writing file:', fullPath);
|
||||
writeFileSync(fullPath, fullContent, 'utf8');
|
||||
|
||||
// Generate ID (same logic as in vault loader)
|
||||
const relativePath = relative(vaultDir, fullPath).replace(/\\/g, '/');
|
||||
const id = relativePath.replace(/\.md$/, '');
|
||||
|
||||
console.log(`[/api/vault/notes] Created note: ${relativePath}`);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
fileName: finalFileName,
|
||||
filePath: relativePath,
|
||||
success: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[/api/vault/notes] Error creating note:', error.message, error.stack);
|
||||
res.status(500).json({ error: 'Failed to create note', details: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+197
-52
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
@@ -28,7 +29,10 @@ import {
|
||||
setupMetadataEndpoint,
|
||||
setupPaginatedMetadataEndpoint,
|
||||
setupPerformanceEndpoint,
|
||||
setupDeferredIndexing
|
||||
setupDeferredIndexing,
|
||||
setupCreateNoteEndpoint,
|
||||
setupRenameFolderEndpoint,
|
||||
setupDeleteFolderEndpoint
|
||||
} from './index-phase3-patch.mjs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -46,6 +50,104 @@ const vaultEventClients = new Set();
|
||||
|
||||
// Phase 3: Advanced caching and monitoring
|
||||
const metadataCache = new MetadataCache({ ttlMs: 5 * 60 * 1000, maxItems: 10_000 });
|
||||
|
||||
// List all folders under the vault (relative paths, forward slashes)
|
||||
app.get('/api/folders/list', (req, res) => {
|
||||
try {
|
||||
const out = [];
|
||||
const walk = (dir, relBase = '') => {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const de of entries) {
|
||||
if (!de.isDirectory()) continue;
|
||||
const name = de.name;
|
||||
// Skip hidden backup folders but keep .trash
|
||||
const rel = relBase ? `${relBase}/${name}` : name;
|
||||
const abs = path.join(dir, name);
|
||||
out.push(rel.replace(/\\/g, '/'));
|
||||
walk(abs, rel);
|
||||
}
|
||||
};
|
||||
walk(vaultDir, '');
|
||||
return res.json(out);
|
||||
} catch (error) {
|
||||
console.error('GET /api/folders/list error:', error);
|
||||
return res.status(500).json({ error: 'Unable to list folders' });
|
||||
}
|
||||
});
|
||||
|
||||
// Duplicate a folder recursively
|
||||
app.post('/api/folders/duplicate', express.json(), (req, res) => {
|
||||
try {
|
||||
const { sourcePath, destinationPath } = req.body || {};
|
||||
if (!sourcePath || !destinationPath) {
|
||||
return res.status(400).json({ error: 'Missing sourcePath or destinationPath' });
|
||||
}
|
||||
const srcRel = String(sourcePath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const dstRel = String(destinationPath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const srcAbs = path.join(vaultDir, srcRel);
|
||||
const dstAbs = path.join(vaultDir, dstRel);
|
||||
if (!fs.existsSync(srcAbs) || !fs.statSync(srcAbs).isDirectory()) {
|
||||
return res.status(404).json({ error: 'Source folder not found' });
|
||||
}
|
||||
if (fs.existsSync(dstAbs)) {
|
||||
return res.status(409).json({ error: 'Destination already exists' });
|
||||
}
|
||||
const copyRecursive = (from, to) => {
|
||||
fs.mkdirSync(to, { recursive: true });
|
||||
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
||||
const s = path.join(from, entry.name);
|
||||
const d = path.join(to, entry.name);
|
||||
if (entry.isDirectory()) copyRecursive(s, d);
|
||||
else fs.copyFileSync(s, d);
|
||||
}
|
||||
};
|
||||
copyRecursive(srcAbs, dstAbs);
|
||||
|
||||
if (metadataCache) metadataCache.clear();
|
||||
if (broadcastVaultEvent) {
|
||||
broadcastVaultEvent({ event: 'folder-duplicate', sourcePath: srcRel, path: dstRel, timestamp: Date.now() });
|
||||
}
|
||||
return res.json({ success: true, sourcePath: srcRel, path: dstRel });
|
||||
} catch (error) {
|
||||
console.error('POST /api/folders/duplicate error:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete all pages (markdown/excalidraw) inside a folder recursively, keep folder structure
|
||||
app.delete('/api/folders/pages', express.json(), (req, res) => {
|
||||
try {
|
||||
const q = typeof req.query.path === 'string' ? req.query.path : '';
|
||||
if (!q) return res.status(400).json({ error: 'Missing path' });
|
||||
const rel = q.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
const abs = path.join(vaultDir, rel);
|
||||
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
|
||||
return res.status(404).json({ error: 'Folder not found' });
|
||||
}
|
||||
const deleted = [];
|
||||
const walk = (dir) => {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(p);
|
||||
else {
|
||||
const low = entry.name.toLowerCase();
|
||||
if (low.endsWith('.md') || low.endsWith('.excalidraw') || low.endsWith('.excalidraw.md')) {
|
||||
try { fs.unlinkSync(p); deleted.push(path.relative(vaultDir, p).replace(/\\/g, '/')); } catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(abs);
|
||||
if (metadataCache) metadataCache.clear();
|
||||
if (broadcastVaultEvent) {
|
||||
broadcastVaultEvent({ event: 'folder-delete-pages', path: rel, count: deleted.length, timestamp: Date.now() });
|
||||
}
|
||||
return res.json({ success: true, path: rel, deletedCount: deleted.length });
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/folders/pages error:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
const performanceMonitor = new PerformanceMonitor();
|
||||
const meilisearchCircuitBreaker = new CircuitBreaker({ failureThreshold: 5, resetTimeoutMs: 30_000 });
|
||||
|
||||
@@ -336,6 +438,12 @@ if (!fs.existsSync(distDir)) {
|
||||
// Servir les fichiers statiques de l'application Angular
|
||||
app.use(express.static(distDir));
|
||||
|
||||
// CORS configuration for development
|
||||
app.use(cors({
|
||||
origin: "http://localhost:3000",
|
||||
credentials: true
|
||||
}));
|
||||
|
||||
// Exposer les fichiers de la voûte pour un accès direct si nécessaire
|
||||
app.use('/vault', express.static(vaultDir));
|
||||
|
||||
@@ -503,67 +611,70 @@ app.get('/api/files/list', async (req, res) => {
|
||||
});
|
||||
|
||||
// Phase 3: Fast metadata endpoint with cache read-through and monitoring
|
||||
setupMetadataEndpoint(app, metadataCache, performanceMonitor, vaultDir, meilisearchCircuitBreaker, retryWithBackoff, {
|
||||
meiliClient,
|
||||
vaultIndexName,
|
||||
ensureIndexSettings,
|
||||
loadVaultMetadataOnly
|
||||
});
|
||||
// setupMetadataEndpoint(app, metadataCache, performanceMonitor, vaultDir, meilisearchCircuitBreaker, retryWithBackoff, {
|
||||
// meiliClient,
|
||||
// vaultIndexName,
|
||||
// ensureIndexSettings,
|
||||
// loadVaultMetadataOnly
|
||||
// });
|
||||
|
||||
// Phase 3: Paginated metadata endpoint with cache read-through and monitoring
|
||||
setupPaginatedMetadataEndpoint(app, metadataCache, performanceMonitor, vaultDir, meilisearchCircuitBreaker, retryWithBackoff, {
|
||||
meiliClient,
|
||||
vaultIndexName,
|
||||
ensureIndexSettings,
|
||||
loadVaultMetadataOnly
|
||||
});
|
||||
// setupPaginatedMetadataEndpoint(app, metadataCache, performanceMonitor, vaultDir, meilisearchCircuitBreaker, retryWithBackoff, {
|
||||
// meiliClient,
|
||||
// vaultIndexName,
|
||||
// ensureIndexSettings,
|
||||
// loadVaultMetadataOnly
|
||||
// });
|
||||
|
||||
app.get('/api/files/metadata', async (req, res) => {
|
||||
try {
|
||||
// Prefer Meilisearch for fast metadata
|
||||
const client = meiliClient();
|
||||
const indexUid = vaultIndexName(vaultDir);
|
||||
const index = await ensureIndexSettings(client, indexUid);
|
||||
const result = await index.search('', {
|
||||
limit: 10000,
|
||||
attributesToRetrieve: ['id', 'title', 'path', 'createdAt', 'updatedAt']
|
||||
});
|
||||
// If explicitly requested, bypass Meilisearch and read from filesystem for authoritative state
|
||||
const forceFs = String(req.query.source || '').toLowerCase() === 'fs';
|
||||
|
||||
const items = Array.isArray(result.hits) ? result.hits.map(hit => ({
|
||||
id: hit.id,
|
||||
title: hit.title,
|
||||
path: hit.path,
|
||||
createdAt: typeof hit.createdAt === 'number' ? new Date(hit.createdAt).toISOString() : hit.createdAt,
|
||||
updatedAt: typeof hit.updatedAt === 'number' ? new Date(hit.updatedAt).toISOString() : hit.updatedAt,
|
||||
})) : [];
|
||||
if (!forceFs) {
|
||||
// Prefer Meilisearch for fast metadata
|
||||
const client = meiliClient();
|
||||
const indexUid = vaultIndexName(vaultDir);
|
||||
const index = await ensureIndexSettings(client, indexUid);
|
||||
const result = await index.search('', {
|
||||
limit: 10000,
|
||||
attributesToRetrieve: ['id', 'title', 'path', 'createdAt', 'updatedAt']
|
||||
});
|
||||
|
||||
// Merge .excalidraw files discovered via FS
|
||||
const drawings = scanVaultDrawings(vaultDir);
|
||||
const byPath = new Map(items.map(it => [String(it.path).toLowerCase(), it]));
|
||||
for (const d of drawings) {
|
||||
const key = String(d.path).toLowerCase();
|
||||
if (!byPath.has(key)) {
|
||||
byPath.set(key, d);
|
||||
}
|
||||
}
|
||||
const items = Array.isArray(result.hits) ? result.hits.map(hit => ({
|
||||
id: hit.id,
|
||||
title: hit.title,
|
||||
path: hit.path,
|
||||
createdAt: typeof hit.createdAt === 'number' ? new Date(hit.createdAt).toISOString() : hit.createdAt,
|
||||
updatedAt: typeof hit.updatedAt === 'number' ? new Date(hit.updatedAt).toISOString() : hit.updatedAt,
|
||||
})) : [];
|
||||
|
||||
res.json(Array.from(byPath.values()));
|
||||
} catch (error) {
|
||||
console.error('Failed to load file metadata via Meilisearch, falling back to FS:', error);
|
||||
try {
|
||||
const notes = await loadVaultNotes(vaultDir);
|
||||
const base = buildFileMetadata(notes);
|
||||
// Merge .excalidraw files discovered via FS
|
||||
const drawings = scanVaultDrawings(vaultDir);
|
||||
const byPath = new Map(base.map(it => [String(it.path).toLowerCase(), it]));
|
||||
const byPath = new Map(items.map(it => [String(it.path).toLowerCase(), it]));
|
||||
for (const d of drawings) {
|
||||
const key = String(d.path).toLowerCase();
|
||||
if (!byPath.has(key)) byPath.set(key, d);
|
||||
if (!byPath.has(key)) {
|
||||
byPath.set(key, d);
|
||||
}
|
||||
}
|
||||
res.json(Array.from(byPath.values()));
|
||||
} catch (err2) {
|
||||
console.error('FS fallback failed:', err2);
|
||||
res.status(500).json({ error: 'Unable to load file metadata.' });
|
||||
|
||||
return res.json(Array.from(byPath.values()));
|
||||
}
|
||||
|
||||
// Filesystem authoritative listing
|
||||
const notes = await loadVaultNotes(vaultDir);
|
||||
const base = buildFileMetadata(notes);
|
||||
const drawings = scanVaultDrawings(vaultDir);
|
||||
const byPath = new Map(base.map(it => [String(it.path).toLowerCase(), it]));
|
||||
for (const d of drawings) {
|
||||
const key = String(d.path).toLowerCase();
|
||||
if (!byPath.has(key)) byPath.set(key, d);
|
||||
}
|
||||
return res.json(Array.from(byPath.values()));
|
||||
} catch (error) {
|
||||
console.error('Failed to load file metadata:', error);
|
||||
return res.status(500).json({ error: 'Unable to load file metadata.' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1395,6 +1506,32 @@ const sendIndex = (req, res) => {
|
||||
res.sendFile(indexPath);
|
||||
};
|
||||
|
||||
// Phase 3: Setup performance monitoring endpoint (must be before catch-all)
|
||||
setupPerformanceEndpoint(app, performanceMonitor, metadataCache, meilisearchCircuitBreaker);
|
||||
|
||||
// Setup create note endpoint (must be before catch-all)
|
||||
setupCreateNoteEndpoint(app, vaultDir);
|
||||
|
||||
// SSE endpoint for vault events (folder rename, delete, etc.)
|
||||
app.get('/api/vault/events', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
const client = registerVaultEventClient(res);
|
||||
|
||||
req.on('close', () => {
|
||||
unregisterVaultEventClient(client);
|
||||
});
|
||||
});
|
||||
|
||||
// Setup rename folder endpoint (must be before catch-all)
|
||||
setupRenameFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
||||
|
||||
// Setup delete folder endpoint (must be before catch-all)
|
||||
setupDeleteFolderEndpoint(app, vaultDir, broadcastVaultEvent, metadataCache);
|
||||
|
||||
app.get('/', sendIndex);
|
||||
app.use((req, res) => {
|
||||
if (req.path.startsWith('/api/')) {
|
||||
@@ -1403,9 +1540,6 @@ app.use((req, res) => {
|
||||
return sendIndex(req, res);
|
||||
});
|
||||
|
||||
// Phase 3: Setup performance monitoring endpoint
|
||||
setupPerformanceEndpoint(app, performanceMonitor, metadataCache, meilisearchCircuitBreaker);
|
||||
|
||||
// Créer le répertoire de la voûte s'il n'existe pas
|
||||
if (!fs.existsSync(vaultDir)) {
|
||||
fs.mkdirSync(vaultDir, { recursive: true });
|
||||
@@ -1447,6 +1581,17 @@ const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('✅ Server ready - Meilisearch indexing in background');
|
||||
});
|
||||
|
||||
// Error handlers
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 Shutting down server...');
|
||||
|
||||
Reference in New Issue
Block a user