fix(fs_sync): cls errors in router

This commit is contained in:
Elian Doran 2025-07-26 18:40:22 +03:00
parent 3da6838395
commit 15bd5aa4e4
No known key found for this signature in database

View File

@ -1,6 +1,5 @@
"use strict"; "use strict";
import express from "express";
import becca from "../../becca/becca.js"; import becca from "../../becca/becca.js";
import BFileSystemMapping from "../../becca/entities/bfile_system_mapping.js"; import BFileSystemMapping from "../../becca/entities/bfile_system_mapping.js";
import fileSystemSyncInit from "../../services/file_system_sync_init.js"; import fileSystemSyncInit from "../../services/file_system_sync_init.js";
@ -8,8 +7,7 @@ import log from "../../services/log.js";
import ValidationError from "../../errors/validation_error.js"; import ValidationError from "../../errors/validation_error.js";
import fs from "fs-extra"; import fs from "fs-extra";
import path from "path"; import path from "path";
import { router, asyncApiRoute, apiRoute } from "../route_api.js";
const router = express.Router();
interface FileStat { interface FileStat {
isFile: boolean; isFile: boolean;
@ -19,348 +17,281 @@ interface FileStat {
} }
// Get all file system mappings // Get all file system mappings
router.get("/mappings", (req, res) => { apiRoute("get", "/mappings", () => {
try { const mappings = Object.values(becca.fileSystemMappings || {}).map(mapping => ({
const mappings = Object.values(becca.fileSystemMappings || {}).map(mapping => ({ mappingId: mapping.mappingId,
mappingId: mapping.mappingId, noteId: mapping.noteId,
noteId: mapping.noteId, filePath: mapping.filePath,
filePath: mapping.filePath, syncDirection: mapping.syncDirection,
syncDirection: mapping.syncDirection, isActive: mapping.isActive,
isActive: mapping.isActive, includeSubtree: mapping.includeSubtree,
includeSubtree: mapping.includeSubtree, preserveHierarchy: mapping.preserveHierarchy,
preserveHierarchy: mapping.preserveHierarchy, contentFormat: mapping.contentFormat,
contentFormat: mapping.contentFormat, excludePatterns: mapping.excludePatterns,
excludePatterns: mapping.excludePatterns, lastSyncTime: mapping.lastSyncTime,
lastSyncTime: mapping.lastSyncTime, syncErrors: mapping.syncErrors,
syncErrors: mapping.syncErrors, dateCreated: mapping.dateCreated,
dateCreated: mapping.dateCreated, dateModified: mapping.dateModified
dateModified: mapping.dateModified }));
}));
res.json(mappings); return mappings;
} catch (error) {
log.error(`Error getting file system mappings: ${error}`);
res.status(500).json({ error: "Failed to get file system mappings" });
}
}); });
// Get a specific file system mapping // Get a specific file system mapping
router.get("/mappings/:mappingId", (req, res) => { apiRoute("get", "/mappings/:mappingId", (req) => {
try { const { mappingId } = req.params;
const { mappingId } = req.params; const mapping = becca.fileSystemMappings[mappingId];
const mapping = becca.fileSystemMappings[mappingId];
if (!mapping) { if (!mapping) {
return res.status(404).json({ error: "Mapping not found" }); return [404, { error: "Mapping not found" }];
}
res.json({
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns,
lastSyncTime: mapping.lastSyncTime,
syncErrors: mapping.syncErrors,
dateCreated: mapping.dateCreated,
dateModified: mapping.dateModified
});
} catch (error) {
log.error(`Error getting file system mapping: ${error}`);
res.status(500).json({ error: "Failed to get file system mapping" });
} }
return {
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns,
lastSyncTime: mapping.lastSyncTime,
syncErrors: mapping.syncErrors,
dateCreated: mapping.dateCreated,
dateModified: mapping.dateModified
};
}); });
// Create a new file system mapping // Create a new file system mapping
router.post("/mappings", async (req, res) => { asyncApiRoute("post", "/mappings", async (req) => {
try { const {
const { noteId,
noteId, filePath,
filePath, syncDirection = 'bidirectional',
syncDirection = 'bidirectional', isActive = true,
isActive = true, includeSubtree = false,
includeSubtree = false, preserveHierarchy = true,
preserveHierarchy = true, contentFormat = 'auto',
contentFormat = 'auto', excludePatterns = null
excludePatterns = null } = req.body;
} = req.body;
// Validate required fields // Validate required fields
if (!noteId || !filePath) { if (!noteId || !filePath) {
throw new ValidationError("noteId and filePath are required"); throw new ValidationError("noteId and filePath are required");
} }
// Validate note exists // Validate note exists
const note = becca.notes[noteId]; const note = becca.notes[noteId];
if (!note) { if (!note) {
throw new ValidationError(`Note ${noteId} not found`); throw new ValidationError(`Note ${noteId} not found`);
} }
// Check if mapping already exists for this note // Check if mapping already exists for this note
const existingMapping = becca.getFileSystemMappingByNoteId(noteId); const existingMapping = becca.getFileSystemMappingByNoteId(noteId);
if (existingMapping) { if (existingMapping) {
throw new ValidationError(`File system mapping already exists for note ${noteId}`); throw new ValidationError(`File system mapping already exists for note ${noteId}`);
} }
// Validate file path exists // Validate file path exists
const normalizedPath = path.resolve(filePath);
if (!await fs.pathExists(normalizedPath)) {
throw new ValidationError(`File path does not exist: ${normalizedPath}`);
}
// Validate sync direction
const validDirections = ['bidirectional', 'trilium_to_disk', 'disk_to_trilium'];
if (!validDirections.includes(syncDirection)) {
throw new ValidationError(`Invalid sync direction. Must be one of: ${validDirections.join(', ')}`);
}
// Validate content format
const validFormats = ['auto', 'markdown', 'html', 'raw'];
if (!validFormats.includes(contentFormat)) {
throw new ValidationError(`Invalid content format. Must be one of: ${validFormats.join(', ')}`);
}
// Create the mapping
const mapping = new BFileSystemMapping({
noteId,
filePath: normalizedPath,
syncDirection,
isActive: isActive ? 1 : 0,
includeSubtree: includeSubtree ? 1 : 0,
preserveHierarchy: preserveHierarchy ? 1 : 0,
contentFormat,
excludePatterns: Array.isArray(excludePatterns) ? JSON.stringify(excludePatterns) : excludePatterns
}).save();
log.info(`Created file system mapping ${mapping.mappingId} for note ${noteId} -> ${normalizedPath}`);
return [201, {
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns
}];
});
// Update a file system mapping
asyncApiRoute("put", "/mappings/:mappingId", async (req) => {
const { mappingId } = req.params;
const mapping = becca.fileSystemMappings[mappingId];
if (!mapping) {
return [404, { error: "Mapping not found" }];
}
const {
filePath,
syncDirection,
isActive,
includeSubtree,
preserveHierarchy,
contentFormat,
excludePatterns
} = req.body;
// Update fields if provided
if (filePath !== undefined) {
const normalizedPath = path.resolve(filePath); const normalizedPath = path.resolve(filePath);
if (!await fs.pathExists(normalizedPath)) { if (!await fs.pathExists(normalizedPath)) {
throw new ValidationError(`File path does not exist: ${normalizedPath}`); throw new ValidationError(`File path does not exist: ${normalizedPath}`);
} }
mapping.filePath = normalizedPath;
}
// Validate sync direction if (syncDirection !== undefined) {
const validDirections = ['bidirectional', 'trilium_to_disk', 'disk_to_trilium']; const validDirections = ['bidirectional', 'trilium_to_disk', 'disk_to_trilium'];
if (!validDirections.includes(syncDirection)) { if (!validDirections.includes(syncDirection)) {
throw new ValidationError(`Invalid sync direction. Must be one of: ${validDirections.join(', ')}`); throw new ValidationError(`Invalid sync direction. Must be one of: ${validDirections.join(', ')}`);
} }
mapping.syncDirection = syncDirection;
}
// Validate content format if (isActive !== undefined) {
mapping.isActive = !!isActive;
}
if (includeSubtree !== undefined) {
mapping.includeSubtree = !!includeSubtree;
}
if (preserveHierarchy !== undefined) {
mapping.preserveHierarchy = !!preserveHierarchy;
}
if (contentFormat !== undefined) {
const validFormats = ['auto', 'markdown', 'html', 'raw']; const validFormats = ['auto', 'markdown', 'html', 'raw'];
if (!validFormats.includes(contentFormat)) { if (!validFormats.includes(contentFormat)) {
throw new ValidationError(`Invalid content format. Must be one of: ${validFormats.join(', ')}`); throw new ValidationError(`Invalid content format. Must be one of: ${validFormats.join(', ')}`);
} }
mapping.contentFormat = contentFormat;
// Create the mapping
const mapping = new BFileSystemMapping({
noteId,
filePath: normalizedPath,
syncDirection,
isActive: isActive ? 1 : 0,
includeSubtree: includeSubtree ? 1 : 0,
preserveHierarchy: preserveHierarchy ? 1 : 0,
contentFormat,
excludePatterns: Array.isArray(excludePatterns) ? JSON.stringify(excludePatterns) : excludePatterns
}).save();
log.info(`Created file system mapping ${mapping.mappingId} for note ${noteId} -> ${normalizedPath}`);
res.status(201).json({
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns
});
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else {
log.error(`Error creating file system mapping: ${error}`);
res.status(500).json({ error: "Failed to create file system mapping" });
}
} }
});
// Update a file system mapping if (excludePatterns !== undefined) {
router.put("/mappings/:mappingId", async (req, res) => { mapping.excludePatterns = Array.isArray(excludePatterns) ? excludePatterns : null;
try {
const { mappingId } = req.params;
const mapping = becca.fileSystemMappings[mappingId];
if (!mapping) {
return res.status(404).json({ error: "Mapping not found" });
}
const {
filePath,
syncDirection,
isActive,
includeSubtree,
preserveHierarchy,
contentFormat,
excludePatterns
} = req.body;
// Update fields if provided
if (filePath !== undefined) {
const normalizedPath = path.resolve(filePath);
if (!await fs.pathExists(normalizedPath)) {
throw new ValidationError(`File path does not exist: ${normalizedPath}`);
}
mapping.filePath = normalizedPath;
}
if (syncDirection !== undefined) {
const validDirections = ['bidirectional', 'trilium_to_disk', 'disk_to_trilium'];
if (!validDirections.includes(syncDirection)) {
throw new ValidationError(`Invalid sync direction. Must be one of: ${validDirections.join(', ')}`);
}
mapping.syncDirection = syncDirection;
}
if (isActive !== undefined) {
mapping.isActive = !!isActive;
}
if (includeSubtree !== undefined) {
mapping.includeSubtree = !!includeSubtree;
}
if (preserveHierarchy !== undefined) {
mapping.preserveHierarchy = !!preserveHierarchy;
}
if (contentFormat !== undefined) {
const validFormats = ['auto', 'markdown', 'html', 'raw'];
if (!validFormats.includes(contentFormat)) {
throw new ValidationError(`Invalid content format. Must be one of: ${validFormats.join(', ')}`);
}
mapping.contentFormat = contentFormat;
}
if (excludePatterns !== undefined) {
mapping.excludePatterns = Array.isArray(excludePatterns) ? excludePatterns : null;
}
mapping.save();
log.info(`Updated file system mapping ${mappingId}`);
res.json({
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns
});
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else {
log.error(`Error updating file system mapping: ${error}`);
res.status(500).json({ error: "Failed to update file system mapping" });
}
} }
mapping.save();
log.info(`Updated file system mapping ${mappingId}`);
return {
mappingId: mapping.mappingId,
noteId: mapping.noteId,
filePath: mapping.filePath,
syncDirection: mapping.syncDirection,
isActive: mapping.isActive,
includeSubtree: mapping.includeSubtree,
preserveHierarchy: mapping.preserveHierarchy,
contentFormat: mapping.contentFormat,
excludePatterns: mapping.excludePatterns
};
}); });
// Delete a file system mapping // Delete a file system mapping
router.delete("/mappings/:mappingId", (req, res) => { apiRoute("delete", "/mappings/:mappingId", (req) => {
try { const { mappingId } = req.params;
const { mappingId } = req.params; const mapping = becca.fileSystemMappings[mappingId];
const mapping = becca.fileSystemMappings[mappingId];
if (!mapping) { if (!mapping) {
return res.status(404).json({ error: "Mapping not found" }); return [404, { error: "Mapping not found" }];
}
mapping.markAsDeleted();
log.info(`Deleted file system mapping ${mappingId}`);
res.json({ success: true });
} catch (error) {
log.error(`Error deleting file system mapping: ${error}`);
res.status(500).json({ error: "Failed to delete file system mapping" });
} }
mapping.markAsDeleted();
log.info(`Deleted file system mapping ${mappingId}`);
return { success: true };
}); });
// Trigger full sync for a mapping // Trigger full sync for a mapping
router.post("/mappings/:mappingId/sync", async (req, res) => { asyncApiRoute("post", "/mappings/:mappingId/sync", async (req) => {
try { const { mappingId } = req.params;
const { mappingId } = req.params;
if (!fileSystemSyncInit.isInitialized()) { if (!fileSystemSyncInit.isInitialized()) {
return res.status(503).json({ error: "File system sync is not initialized" }); return [503, { error: "File system sync is not initialized" }];
} }
const result = await fileSystemSyncInit.fullSync(mappingId); const result = await fileSystemSyncInit.fullSync(mappingId);
if (result.success) { if (result.success) {
res.json(result); return result;
} else { } else {
res.status(400).json(result); return [400, result];
}
} catch (error) {
log.error(`Error triggering sync: ${error}`);
res.status(500).json({ error: "Failed to trigger sync" });
} }
}); });
// Get sync status for all mappings // Get sync status for all mappings
router.get("/status", (req, res) => { apiRoute("get", "/status", () => {
try { return fileSystemSyncInit.getStatus();
const status = fileSystemSyncInit.getStatus();
res.json(status);
} catch (error) {
log.error(`Error getting sync status: ${error}`);
res.status(500).json({ error: "Failed to get sync status" });
}
}); });
// Enable/disable file system sync // Enable file system sync
router.post("/enable", async (req, res) => { asyncApiRoute("post", "/enable", async () => {
try { await fileSystemSyncInit.enable();
await fileSystemSyncInit.enable(); return { success: true, message: "File system sync enabled" };
res.json({ success: true, message: "File system sync enabled" });
} catch (error) {
log.error(`Error enabling file system sync: ${error}`);
res.status(500).json({ error: "Failed to enable file system sync" });
}
}); });
router.post("/disable", async (req, res) => { // Disable file system sync
try { asyncApiRoute("post", "/disable", async () => {
await fileSystemSyncInit.disable(); await fileSystemSyncInit.disable();
res.json({ success: true, message: "File system sync disabled" }); return { success: true, message: "File system sync disabled" };
} catch (error) {
log.error(`Error disabling file system sync: ${error}`);
res.status(500).json({ error: "Failed to disable file system sync" });
}
}); });
// Validate file path // Validate file path
router.post("/validate-path", async (req, res) => { asyncApiRoute("post", "/validate-path", async (req) => {
try { const { filePath } = req.body;
const { filePath } = req.body;
if (!filePath) { if (!filePath) {
throw new ValidationError("filePath is required"); throw new ValidationError("filePath is required");
}
const normalizedPath = path.resolve(filePath);
const exists = await fs.pathExists(normalizedPath);
let stats: FileStat | null = null;
if (exists) {
const fileStats = await fs.stat(normalizedPath);
stats = {
isFile: fileStats.isFile(),
isDirectory: fileStats.isDirectory(),
size: fileStats.size,
modified: fileStats.mtime.toISOString()
};
}
res.json({
path: normalizedPath,
exists,
stats
});
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message });
} else {
log.error(`Error validating file path: ${error}`);
res.status(500).json({ error: "Failed to validate file path" });
}
} }
const normalizedPath = path.resolve(filePath);
const exists = await fs.pathExists(normalizedPath);
let stats: FileStat | null = null;
if (exists) {
const fileStats = await fs.stat(normalizedPath);
stats = {
isFile: fileStats.isFile(),
isDirectory: fileStats.isDirectory(),
size: fileStats.size,
modified: fileStats.mtime.toISOString()
};
}
return {
path: normalizedPath,
exists,
stats
};
}); });
export default router; export default router;