const express = require('express'); const fs = require('fs').promises; const path = require('path'); const { plugins, plugin_name_to_folder_name, plugin_data_path, } = require('../../../public/script.js'); const extensionName = "Amily-Databin"; const extensionVersion = "0.0.1-a"; const storageBasePath = path.join(plugin_data_path, 'cc.amily49.amily-databin'); let sourceTypes = {}; // Will be loaded from sourcetype.json /** * Builds a safe file path and prevents path traversal attacks. */ function getSafeFilePath(source, objName, file) { if (typeof source !== 'string' && typeof source !== 'number' || typeof objName !== 'string' || typeof file !== 'string') { return null; } const sanitize = (part) => String(part).replace(/\.\./g, '').replace(/[/\]/g, ''); const safeSource = sanitize(source); const safeObjName = sanitize(objName); const safeFile = sanitize(file); if (!safeSource || !safeObjName || !safeFile) return null; return path.join(storageBasePath, safeSource, safeObjName, safeFile); } // #region Server-side Business Logic per SourceType // These functions implement the "strategy" for each source type. /** Generic save handler */ async function defaultSaveStrategy(filePath, data) { await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8'); } // #endregion (async () => { try { // Load source types from the file on startup. const sourceTypesRaw = await fs.readFile(path.join(__dirname, 'sourcetype.json'), 'utf8'); sourceTypes = JSON.parse(sourceTypesRaw); // Ensure the base storage directory exists. await fs.mkdir(storageBasePath, { recursive: true }); } catch (error) { console.error(`[${extensionName}] Critical startup error:`, error); return; // Stop loading the plugin } const router = express.Router(); // Endpoint to serve the source type map to the client router.get('/sourcetype', (req, res) => { res.json(sourceTypes); }); router.get('/data', async (req, res) => { const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file); if (!filePath) return res.status(400).send("Invalid parameters."); try { const fileContent = await fs.readFile(filePath, 'utf8'); res.json(JSON.parse(fileContent)); } catch (error) { if (error.code === 'ENOENT') return res.status(404).send('Not found.'); console.error(`[${extensionName}] Error reading file ${filePath}:`, error); res.status(500).send('Internal Server Error'); } }); router.post('/data', async (req, res) => { const { source, objName, file, data } = req.body; const filePath = getSafeFilePath(source, objName, file); if (!filePath) return res.status(400).send("Invalid parameters."); if (data === undefined) return res.status(400).send("'data' is required."); const sourceType = parseInt(source, 10); try { // Strategy pattern based on source type switch (sourceType) { case sourceTypes.CHAT_LOG: case sourceTypes.CHARACTER_CARD: case sourceTypes.WORLD_BOOK: // For now, all implemented types use the default save strategy. await defaultSaveStrategy(filePath, data); res.status(200).send('Data saved successfully.'); break; default: // If the source type is known but not implemented, or unknown. res.status(501).send(`Save handler for source type '${sourceType}' is not implemented.`); break; } } catch (error) { console.error(`[${extensionName}] Error in POST /data for source ${sourceType}:`, error); res.status(500).send('Internal Server Error'); } }); router.delete('/data', async (req, res) => { const filePath = getSafeFilePath(req.query.source, req.query.objName, req.query.file); if (!filePath) return res.status(400).send("Invalid parameters."); try { await fs.unlink(filePath); res.status(200).send('Data deleted successfully.'); } catch (error) { if (error.code === 'ENOENT') return res.status(200).send('Not found.'); console.error(`[${extensionName}] Error deleting file ${filePath}:`, error); res.status(500).send('Internal Server Error'); } }); plugins[extensionName] = { name: extensionName, version: extensionVersion, is_enabled: true, folder_name: plugin_name_to_folder_name[extensionName], router: router, assets: ['client.js'], }; console.log(`[${extensionName}] version ${extensionVersion} loaded. Storage path: ${storageBasePath}`); })();