674 lines
20 KiB
JavaScript
674 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import express from 'express';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import fs from 'fs';
|
||
import chokidar from 'chokidar';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 4000;
|
||
|
||
const rootDir = path.resolve(__dirname, '..');
|
||
const distDir = path.join(rootDir, 'dist');
|
||
const vaultDir = path.join(rootDir, 'vault');
|
||
|
||
const vaultEventClients = new Set();
|
||
|
||
const registerVaultEventClient = (res) => {
|
||
const heartbeat = setInterval(() => {
|
||
try {
|
||
res.write(':keepalive\n\n');
|
||
} catch {
|
||
// Write failures will be handled by the close handler.
|
||
}
|
||
}, 20000);
|
||
|
||
const client = { res, heartbeat };
|
||
vaultEventClients.add(client);
|
||
return client;
|
||
};
|
||
|
||
const unregisterVaultEventClient = (client) => {
|
||
clearInterval(client.heartbeat);
|
||
vaultEventClients.delete(client);
|
||
};
|
||
|
||
const broadcastVaultEvent = (payload) => {
|
||
if (!vaultEventClients.size) {
|
||
return;
|
||
}
|
||
|
||
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
||
for (const client of [...vaultEventClients]) {
|
||
try {
|
||
client.res.write(data);
|
||
} catch (error) {
|
||
console.error('Failed to notify vault event client:', error);
|
||
unregisterVaultEventClient(client);
|
||
}
|
||
}
|
||
};
|
||
|
||
const isMarkdownFile = (entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md');
|
||
|
||
const normalizeString = (value) => {
|
||
return value
|
||
.normalize('NFKD')
|
||
.replace(/[\u0300-\u036f]/g, '')
|
||
.trim();
|
||
};
|
||
|
||
const slugifySegment = (segment) => {
|
||
const normalized = normalizeString(segment);
|
||
const slug = normalized
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9]+/g, '-')
|
||
.replace(/^-+|-+$/g, '');
|
||
return slug || normalized.toLowerCase() || segment.toLowerCase();
|
||
};
|
||
|
||
const slugifyPath = (relativePath) => {
|
||
return relativePath
|
||
.split('/')
|
||
.map((segment) => slugifySegment(segment))
|
||
.filter(Boolean)
|
||
.join('/');
|
||
};
|
||
|
||
const extractTitle = (content, fallback) => {
|
||
const headingMatch = content.match(/^\s*#\s+(.+)$/m);
|
||
if (headingMatch) {
|
||
return headingMatch[1].trim();
|
||
}
|
||
return fallback;
|
||
};
|
||
|
||
const extractTags = (content) => {
|
||
const tagRegex = /(^|\s)#([A-Za-z0-9_\/-]+)/g;
|
||
const tags = new Set();
|
||
let match;
|
||
while ((match = tagRegex.exec(content)) !== null) {
|
||
tags.add(match[2]);
|
||
}
|
||
return Array.from(tags);
|
||
};
|
||
|
||
const loadVaultNotes = (vaultPath) => {
|
||
const notes = [];
|
||
|
||
const walk = (currentDir) => {
|
||
if (!fs.existsSync(currentDir)) {
|
||
return;
|
||
}
|
||
|
||
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const entryPath = path.join(currentDir, entry.name);
|
||
|
||
if (entry.isDirectory()) {
|
||
walk(entryPath);
|
||
continue;
|
||
}
|
||
|
||
if (!isMarkdownFile(entry)) {
|
||
continue;
|
||
}
|
||
|
||
try {
|
||
const content = fs.readFileSync(entryPath, 'utf-8');
|
||
const stats = fs.statSync(entryPath);
|
||
const relativePathWithExt = path.relative(vaultPath, entryPath).replace(/\\/g, '/');
|
||
const relativePath = relativePathWithExt.replace(/\.md$/i, '');
|
||
const id = slugifyPath(relativePath);
|
||
const fileNameWithExt = entry.name;
|
||
|
||
const fallbackTitle = path.basename(relativePath);
|
||
const title = extractTitle(content, fallbackTitle);
|
||
const finalId = id || slugifySegment(fallbackTitle) || fallbackTitle;
|
||
const createdDate = stats.birthtimeMs ? new Date(stats.birthtimeMs) : new Date(stats.ctimeMs);
|
||
const updatedDate = new Date(stats.mtimeMs);
|
||
|
||
notes.push({
|
||
id: finalId,
|
||
title,
|
||
content,
|
||
tags: extractTags(content),
|
||
mtime: stats.mtimeMs,
|
||
fileName: fileNameWithExt,
|
||
filePath: relativePathWithExt,
|
||
originalPath: relativePath,
|
||
createdAt: createdDate.toISOString(),
|
||
updatedAt: updatedDate.toISOString()
|
||
});
|
||
} catch (err) {
|
||
console.error(`Failed to read note at ${entryPath}:`, err);
|
||
}
|
||
}
|
||
};
|
||
|
||
walk(vaultPath);
|
||
return notes;
|
||
};
|
||
|
||
const buildFileMetadata = (notes) =>
|
||
notes.map((note) => ({
|
||
id: note.id,
|
||
title: note.title,
|
||
path: note.filePath,
|
||
createdAt: note.createdAt,
|
||
updatedAt: note.updatedAt,
|
||
}));
|
||
|
||
const normalizeDateInput = (value) => {
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
const date = new Date(value);
|
||
return Number.isNaN(date.getTime()) ? null : date;
|
||
};
|
||
|
||
const isDateWithinRange = (target, start, end) => {
|
||
const targetTime = target.getTime();
|
||
return targetTime >= start.getTime() && targetTime <= end.getTime();
|
||
};
|
||
|
||
const vaultWatcher = chokidar.watch(vaultDir, {
|
||
persistent: true,
|
||
ignoreInitial: true,
|
||
awaitWriteFinish: {
|
||
stabilityThreshold: 250,
|
||
pollInterval: 100,
|
||
},
|
||
});
|
||
|
||
const watchedVaultEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
|
||
|
||
watchedVaultEvents.forEach((eventName) => {
|
||
vaultWatcher.on(eventName, (changedPath) => {
|
||
const relativePath = path.relative(vaultDir, changedPath).replace(/\\/g, '/');
|
||
broadcastVaultEvent({
|
||
event: eventName,
|
||
path: relativePath,
|
||
timestamp: Date.now(),
|
||
});
|
||
});
|
||
});
|
||
|
||
vaultWatcher.on('ready', () => {
|
||
broadcastVaultEvent({
|
||
event: 'ready',
|
||
timestamp: Date.now(),
|
||
});
|
||
});
|
||
|
||
vaultWatcher.on('error', (error) => {
|
||
console.error('Vault watcher error:', error);
|
||
broadcastVaultEvent({
|
||
event: 'error',
|
||
message: typeof error?.message === 'string' ? error.message : 'Unknown watcher error',
|
||
timestamp: Date.now(),
|
||
});
|
||
});
|
||
|
||
// Vérifier si le répertoire dist existe
|
||
if (!fs.existsSync(distDir)) {
|
||
console.warn(`Warning: build directory not found at ${distDir}. Did you run \`npm run build\`?`);
|
||
}
|
||
|
||
// Servir les fichiers statiques de l'application Angular
|
||
app.use(express.static(distDir));
|
||
|
||
// Exposer les fichiers de la voûte pour un accès direct si nécessaire
|
||
app.use('/vault', express.static(vaultDir));
|
||
|
||
// Résolution des attachements: recherche le fichier en remontant les dossiers depuis la note, puis dans la voûte
|
||
app.get('/api/attachments/resolve', (req, res) => {
|
||
try {
|
||
const rawName = typeof req.query.name === 'string' ? req.query.name.trim() : '';
|
||
if (!rawName) {
|
||
return res.status(400).type('text/plain').send('Missing required query parameter: name');
|
||
}
|
||
|
||
const sanitize = (value) => value.replace(/\\/g, '/').replace(/^[/]+|[/]+$/g, '');
|
||
const name = sanitize(rawName);
|
||
const noteRelPath = typeof req.query.note === 'string' ? sanitize(req.query.note) : '';
|
||
const baseRaw = typeof req.query.base === 'string' ? req.query.base : '';
|
||
const baseRel = sanitize(baseRaw);
|
||
|
||
const candidateDirs = new Set();
|
||
|
||
const addCandidate = (dir, extra) => {
|
||
const dirSegments = sanitize(dir);
|
||
const extraSegments = sanitize(extra);
|
||
if (dirSegments && extraSegments) {
|
||
candidateDirs.add(`${dirSegments}/${extraSegments}`);
|
||
} else if (dirSegments) {
|
||
candidateDirs.add(dirSegments);
|
||
} else if (extraSegments) {
|
||
candidateDirs.add(extraSegments);
|
||
} else {
|
||
candidateDirs.add('');
|
||
}
|
||
};
|
||
|
||
// Dossiers parents de la note
|
||
if (noteRelPath) {
|
||
const segments = noteRelPath.split('/');
|
||
segments.pop(); // retirer le nom de fichier
|
||
while (segments.length >= 0) {
|
||
const currentDir = segments.join('/');
|
||
addCandidate(currentDir, baseRel);
|
||
addCandidate(currentDir, '');
|
||
if (!segments.length) break;
|
||
segments.pop();
|
||
}
|
||
}
|
||
|
||
// Si base est défini, tenter aussi directement depuis la racine
|
||
if (baseRel) {
|
||
addCandidate('', baseRel);
|
||
}
|
||
|
||
// Toujours ajouter la racine seule en dernier recours
|
||
addCandidate('', '');
|
||
|
||
for (const dir of candidateDirs) {
|
||
const absoluteDir = dir ? path.join(vaultDir, dir) : vaultDir;
|
||
const candidatePath = path.join(absoluteDir, name);
|
||
try {
|
||
if (fs.existsSync(candidatePath) && fs.statSync(candidatePath).isFile()) {
|
||
return res.sendFile(candidatePath);
|
||
}
|
||
} catch {
|
||
// Ignorer et poursuivre
|
||
}
|
||
}
|
||
|
||
// Recherche exhaustive en dernier recours (coût plus élevé)
|
||
const stack = [vaultDir];
|
||
const nameLower = name.toLowerCase();
|
||
while (stack.length) {
|
||
const currentDir = stack.pop();
|
||
let entries = [];
|
||
try {
|
||
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
||
} catch {
|
||
continue;
|
||
}
|
||
|
||
for (const entry of entries) {
|
||
const fullPath = path.join(currentDir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
stack.push(fullPath);
|
||
} else if (entry.isFile() && entry.name.toLowerCase() === nameLower) {
|
||
return res.sendFile(fullPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
return res.status(404).type('text/plain').send(`‼️Attachement ${rawName} introuvable`);
|
||
} catch (error) {
|
||
console.error('Attachment resolve error:', error);
|
||
return res.status(500).type('text/plain').send('Attachment resolver internal error');
|
||
}
|
||
});
|
||
|
||
// API endpoint pour la santé
|
||
app.get('/api/health', (req, res) => {
|
||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||
});
|
||
|
||
app.get('/api/vault/events', (req, res) => {
|
||
res.set({
|
||
'Content-Type': 'text/event-stream',
|
||
'Cache-Control': 'no-cache',
|
||
Connection: 'keep-alive',
|
||
'X-Accel-Buffering': 'no',
|
||
});
|
||
|
||
res.flushHeaders?.();
|
||
res.write(
|
||
`data: ${JSON.stringify({
|
||
event: 'connected',
|
||
timestamp: Date.now(),
|
||
})}\n\n`,
|
||
);
|
||
|
||
const client = registerVaultEventClient(res);
|
||
|
||
req.on('close', () => {
|
||
unregisterVaultEventClient(client);
|
||
});
|
||
});
|
||
|
||
// API endpoint pour les données de la voûte (contenu réel)
|
||
app.get('/api/vault', (req, res) => {
|
||
try {
|
||
const notes = loadVaultNotes(vaultDir);
|
||
res.json({ notes });
|
||
} catch (error) {
|
||
console.error('Failed to load vault notes:', error);
|
||
res.status(500).json({ error: 'Unable to load vault notes.' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/files/metadata', (req, res) => {
|
||
try {
|
||
const notes = loadVaultNotes(vaultDir);
|
||
res.json(buildFileMetadata(notes));
|
||
} catch (error) {
|
||
console.error('Failed to load file metadata:', error);
|
||
res.status(500).json({ error: 'Unable to load file metadata.' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/files/by-date', (req, res) => {
|
||
const { date } = req.query;
|
||
const targetDate = normalizeDateInput(date);
|
||
|
||
if (!targetDate) {
|
||
return res.status(400).json({ error: 'Invalid or missing date query parameter.' });
|
||
}
|
||
|
||
try {
|
||
const notes = loadVaultNotes(vaultDir);
|
||
const startOfDay = new Date(targetDate);
|
||
startOfDay.setHours(0, 0, 0, 0);
|
||
const endOfDay = new Date(targetDate);
|
||
endOfDay.setHours(23, 59, 59, 999);
|
||
|
||
const filtered = notes.filter((note) => {
|
||
const createdAt = normalizeDateInput(note.createdAt);
|
||
const updatedAt = normalizeDateInput(note.updatedAt);
|
||
const matchesCreated = createdAt && isDateWithinRange(createdAt, startOfDay, endOfDay);
|
||
const matchesUpdated = updatedAt && isDateWithinRange(updatedAt, startOfDay, endOfDay);
|
||
return matchesCreated || matchesUpdated;
|
||
});
|
||
|
||
res.json(buildFileMetadata(filtered));
|
||
} catch (error) {
|
||
console.error('Failed to search files by date:', error);
|
||
res.status(500).json({ error: 'Unable to search files by date.' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/files/by-date-range', (req, res) => {
|
||
const { start, end } = req.query;
|
||
const startDate = normalizeDateInput(start);
|
||
const endDate = normalizeDateInput(end ?? start);
|
||
|
||
if (!startDate || !endDate || startDate > endDate) {
|
||
return res.status(400).json({ error: 'Invalid start or end date parameters.' });
|
||
}
|
||
|
||
const normalizedStart = new Date(startDate);
|
||
normalizedStart.setHours(0, 0, 0, 0);
|
||
const normalizedEnd = new Date(endDate);
|
||
normalizedEnd.setHours(23, 59, 59, 999);
|
||
|
||
try {
|
||
const notes = loadVaultNotes(vaultDir);
|
||
const filtered = notes.filter((note) => {
|
||
const createdAt = normalizeDateInput(note.createdAt);
|
||
const updatedAt = normalizeDateInput(note.updatedAt);
|
||
const matchesCreated = createdAt && isDateWithinRange(createdAt, normalizedStart, normalizedEnd);
|
||
const matchesUpdated = updatedAt && isDateWithinRange(updatedAt, normalizedStart, normalizedEnd);
|
||
return matchesCreated || matchesUpdated;
|
||
});
|
||
|
||
res.json(buildFileMetadata(filtered));
|
||
} catch (error) {
|
||
console.error('Failed to search files by date range:', error);
|
||
res.status(500).json({ error: 'Unable to search files by date range.' });
|
||
}
|
||
});
|
||
|
||
// Bookmarks API - reads/writes <vault>/.obsidian/bookmarks.json
|
||
app.use(express.json());
|
||
|
||
app.post('/api/logs', (req, res) => {
|
||
const { source = 'frontend', level = 'info', message = '', data = null, timestamp = Date.now() } = req.body || {};
|
||
|
||
const prefix = `[ClientLog:${source}]`;
|
||
const payload = data !== undefined ? { data } : undefined;
|
||
|
||
switch (level) {
|
||
case 'error':
|
||
console.error(prefix, message, payload ?? '', new Date(timestamp).toISOString());
|
||
break;
|
||
case 'warn':
|
||
console.warn(prefix, message, payload ?? '', new Date(timestamp).toISOString());
|
||
break;
|
||
case 'debug':
|
||
console.debug(prefix, message, payload ?? '', new Date(timestamp).toISOString());
|
||
break;
|
||
default:
|
||
console.log(prefix, message, payload ?? '', new Date(timestamp).toISOString());
|
||
break;
|
||
}
|
||
|
||
res.status(202).json({ status: 'queued' });
|
||
});
|
||
|
||
function ensureBookmarksStorage() {
|
||
const obsidianDir = path.join(vaultDir, '.obsidian');
|
||
if (!fs.existsSync(obsidianDir)) {
|
||
fs.mkdirSync(obsidianDir, { recursive: true });
|
||
}
|
||
|
||
const bookmarksPath = path.join(obsidianDir, 'bookmarks.json');
|
||
if (!fs.existsSync(bookmarksPath)) {
|
||
const emptyDoc = { items: [] };
|
||
const initialContent = JSON.stringify(emptyDoc, null, 2);
|
||
fs.writeFileSync(bookmarksPath, initialContent, 'utf-8');
|
||
}
|
||
|
||
return { obsidianDir, bookmarksPath };
|
||
}
|
||
|
||
// Ensure bookmarks storage is ready on startup
|
||
ensureBookmarksStorage();
|
||
|
||
// Graph config API - reads/writes <vault>/.obsidian/graph.json
|
||
function ensureGraphStorage() {
|
||
const obsidianDir = path.join(vaultDir, '.obsidian');
|
||
if (!fs.existsSync(obsidianDir)) {
|
||
fs.mkdirSync(obsidianDir, { recursive: true });
|
||
}
|
||
|
||
const graphPath = path.join(obsidianDir, 'graph.json');
|
||
if (!fs.existsSync(graphPath)) {
|
||
// Create default graph config matching Obsidian defaults
|
||
const defaultConfig = {
|
||
'collapse-filter': false,
|
||
search: '',
|
||
showTags: false,
|
||
showAttachments: false,
|
||
hideUnresolved: false,
|
||
showOrphans: true,
|
||
'collapse-color-groups': false,
|
||
colorGroups: [],
|
||
'collapse-display': false,
|
||
showArrow: false,
|
||
textFadeMultiplier: 0,
|
||
nodeSizeMultiplier: 1,
|
||
lineSizeMultiplier: 1,
|
||
'collapse-forces': false,
|
||
centerStrength: 0.3,
|
||
repelStrength: 17,
|
||
linkStrength: 0.5,
|
||
linkDistance: 200,
|
||
scale: 1,
|
||
close: false
|
||
};
|
||
const initialContent = JSON.stringify(defaultConfig, null, 2);
|
||
fs.writeFileSync(graphPath, initialContent, 'utf-8');
|
||
}
|
||
|
||
return { obsidianDir, graphPath };
|
||
}
|
||
|
||
// Ensure graph storage is ready on startup
|
||
ensureGraphStorage();
|
||
|
||
app.get('/api/vault/graph', (req, res) => {
|
||
try {
|
||
const { graphPath } = ensureGraphStorage();
|
||
const content = fs.readFileSync(graphPath, 'utf-8');
|
||
const config = JSON.parse(content);
|
||
const rev = calculateSimpleHash(content);
|
||
|
||
res.json({ config, rev });
|
||
} catch (error) {
|
||
console.error('Failed to load graph config:', error);
|
||
res.status(500).json({ error: 'Unable to load graph config.' });
|
||
}
|
||
});
|
||
|
||
app.put('/api/vault/graph', (req, res) => {
|
||
try {
|
||
const { graphPath } = ensureGraphStorage();
|
||
const ifMatch = req.headers['if-match'];
|
||
|
||
// Check for conflicts if If-Match header is present
|
||
if (ifMatch) {
|
||
const currentContent = fs.readFileSync(graphPath, 'utf-8');
|
||
const currentRev = calculateSimpleHash(currentContent);
|
||
|
||
if (ifMatch !== currentRev) {
|
||
return res.status(409).json({ error: 'Conflict: File modified externally' });
|
||
}
|
||
}
|
||
|
||
// Create backup before writing
|
||
const backupPath = graphPath + '.bak';
|
||
if (fs.existsSync(graphPath)) {
|
||
fs.copyFileSync(graphPath, backupPath);
|
||
}
|
||
|
||
// Atomic write: write to temp file, then rename
|
||
const tempPath = graphPath + '.tmp';
|
||
const content = JSON.stringify(req.body, null, 2);
|
||
|
||
try {
|
||
fs.writeFileSync(tempPath, content, 'utf-8');
|
||
fs.renameSync(tempPath, graphPath);
|
||
} catch (writeError) {
|
||
// If write failed, restore backup if it exists
|
||
if (fs.existsSync(backupPath)) {
|
||
fs.copyFileSync(backupPath, graphPath);
|
||
}
|
||
throw writeError;
|
||
}
|
||
|
||
const newRev = calculateSimpleHash(content);
|
||
res.json({ rev: newRev });
|
||
} catch (error) {
|
||
console.error('Failed to save graph config:', error);
|
||
res.status(500).json({ error: 'Unable to save graph config.' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/vault/bookmarks', (req, res) => {
|
||
try {
|
||
const { bookmarksPath } = ensureBookmarksStorage();
|
||
const content = fs.readFileSync(bookmarksPath, 'utf-8');
|
||
const doc = JSON.parse(content);
|
||
const rev = calculateSimpleHash(content);
|
||
|
||
res.json({ ...doc, rev });
|
||
} catch (error) {
|
||
console.error('Failed to load bookmarks:', error);
|
||
res.status(500).json({ error: 'Unable to load bookmarks.' });
|
||
}
|
||
});
|
||
|
||
app.put('/api/vault/bookmarks', (req, res) => {
|
||
try {
|
||
const { bookmarksPath } = ensureBookmarksStorage();
|
||
const ifMatch = req.headers['if-match'];
|
||
|
||
// Check for conflicts if If-Match header is present
|
||
if (ifMatch) {
|
||
const currentContent = fs.readFileSync(bookmarksPath, 'utf-8');
|
||
const currentRev = calculateSimpleHash(currentContent);
|
||
|
||
if (ifMatch !== currentRev) {
|
||
return res.status(409).json({ error: 'Conflict: File modified externally' });
|
||
}
|
||
}
|
||
|
||
// Create backup before writing
|
||
const backupPath = bookmarksPath + '.bak';
|
||
if (fs.existsSync(bookmarksPath)) {
|
||
fs.copyFileSync(bookmarksPath, backupPath);
|
||
}
|
||
|
||
// Atomic write: write to temp file, then rename
|
||
const tempPath = bookmarksPath + '.tmp';
|
||
const content = JSON.stringify(req.body, null, 2);
|
||
|
||
try {
|
||
fs.writeFileSync(tempPath, content, 'utf-8');
|
||
fs.renameSync(tempPath, bookmarksPath);
|
||
} catch (writeError) {
|
||
// If write failed, restore backup if it exists
|
||
if (fs.existsSync(backupPath)) {
|
||
fs.copyFileSync(backupPath, bookmarksPath);
|
||
}
|
||
throw writeError;
|
||
}
|
||
|
||
const newRev = calculateSimpleHash(content);
|
||
res.json({ rev: newRev });
|
||
} catch (error) {
|
||
console.error('Failed to save bookmarks:', error);
|
||
res.status(500).json({ error: 'Unable to save bookmarks.' });
|
||
}
|
||
});
|
||
|
||
// Simple hash function for rev generation
|
||
function calculateSimpleHash(content) {
|
||
let hash = 0;
|
||
for (let i = 0; i < content.length; i++) {
|
||
const char = content.charCodeAt(i);
|
||
hash = (hash << 5) - hash + char;
|
||
hash = hash & hash;
|
||
}
|
||
return Math.abs(hash).toString(36) + '-' + content.length;
|
||
}
|
||
|
||
// Servir l'index.html pour toutes les routes (SPA)
|
||
const sendIndex = (req, res) => {
|
||
const indexPath = path.join(distDir, 'index.html');
|
||
if (!fs.existsSync(indexPath)) {
|
||
return res
|
||
.status(500)
|
||
.send('Application build missing. Please run `npm run build` before starting the server.');
|
||
}
|
||
res.sendFile(indexPath);
|
||
};
|
||
|
||
app.get('/', sendIndex);
|
||
app.use((req, res) => {
|
||
if (req.path.startsWith('/api/')) {
|
||
return res.status(404).json({ error: 'Not found' });
|
||
}
|
||
return sendIndex(req, res);
|
||
});
|
||
|
||
// Créer le répertoire de la voûte s'il n'existe pas
|
||
if (!fs.existsSync(vaultDir)) {
|
||
fs.mkdirSync(vaultDir, { recursive: true });
|
||
console.log('Created vault directory:', vaultDir);
|
||
}
|
||
|
||
app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`ObsiViewer server running on http://0.0.0.0:${PORT}`);
|
||
console.log(`Vault directory: ${vaultDir}`);
|
||
});
|