265 lines
7.8 KiB
JavaScript
265 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import express from 'express';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import fs from 'fs';
|
|
|
|
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 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();
|
|
};
|
|
|
|
// 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));
|
|
|
|
// API endpoint pour la santé
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
// 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.' });
|
|
}
|
|
});
|
|
|
|
// 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}`);
|
|
});
|