Files
LuHost/routes/mods.js
Nathan Schneider 3aed09b60f Initial commit: LuHost - Luanti Server Management Web Interface
A modern web interface for Luanti (Minetest) server management with ContentDB integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 17:32:37 -06:00

318 lines
9.4 KiB
JavaScript

const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const paths = require('../utils/paths');
const ConfigParser = require('../utils/config-parser');
const router = express.Router();
// Mods listing page
router.get('/', async (req, res) => {
try {
paths.ensureDirectories();
let globalMods = [];
let worlds = [];
// Get global mods
try {
const modDirs = await fs.readdir(paths.modsDir);
for (const modDir of modDirs) {
try {
const modPath = paths.getModPath(modDir);
const configPath = paths.getModConfigPath(modDir);
const stats = await fs.stat(modPath);
if (!stats.isDirectory()) continue;
const config = await ConfigParser.parseModConfig(configPath);
globalMods.push({
name: modDir,
title: config.title || modDir,
description: config.description || '',
author: config.author || '',
depends: config.depends || [],
optional_depends: config.optional_depends || [],
min_minetest_version: config.min_minetest_version || '',
max_minetest_version: config.max_minetest_version || '',
location: 'global',
path: modPath,
lastModified: stats.mtime
});
} catch (modError) {
console.error(`Error reading mod ${modDir}:`, modError);
}
}
} catch (dirError) {}
// Get worlds for dropdown
try {
const worldDirs = await fs.readdir(paths.worldsDir);
for (const worldDir of worldDirs) {
try {
const worldPath = paths.getWorldPath(worldDir);
const configPath = paths.getWorldConfigPath(worldDir);
const stats = await fs.stat(worldPath);
if (stats.isDirectory()) {
const config = await ConfigParser.parseWorldConfig(configPath);
worlds.push({
name: worldDir,
displayName: config.server_name || worldDir
});
}
} catch {}
}
} catch {}
const selectedWorld = req.query.world;
let worldMods = [];
if (selectedWorld && paths.isValidWorldName(selectedWorld)) {
try {
const worldModsPath = paths.getWorldModsPath(selectedWorld);
const modDirs = await fs.readdir(worldModsPath);
for (const modDir of modDirs) {
try {
const modPath = path.join(worldModsPath, modDir);
const configPath = path.join(modPath, 'mod.conf');
const stats = await fs.stat(modPath);
if (!stats.isDirectory()) continue;
const config = await ConfigParser.parseModConfig(configPath);
worldMods.push({
name: modDir,
title: config.title || modDir,
description: config.description || '',
author: config.author || '',
depends: config.depends || [],
optional_depends: config.optional_depends || [],
location: 'world',
enabled: true,
path: modPath,
lastModified: stats.mtime
});
} catch (modError) {
console.error(`Error reading world mod ${modDir}:`, modError);
}
}
} catch (dirError) {}
}
res.render('mods/index', {
title: 'Mod Management',
globalMods: globalMods,
worldMods: worldMods,
worlds: worlds,
selectedWorld: selectedWorld,
currentPage: 'mods'
});
} catch (error) {
console.error('Error getting mods:', error);
res.status(500).render('error', {
error: 'Failed to load mods',
message: error.message
});
}
});
// Install mod to world
router.post('/install/:worldName/:modName', async (req, res) => {
try {
const { worldName, modName } = req.params;
if (!paths.isValidWorldName(worldName) || !paths.isValidModName(modName)) {
return res.status(400).json({ error: 'Invalid world or mod name' });
}
const worldPath = paths.getWorldPath(worldName);
const globalModPath = paths.getModPath(modName);
const worldModsPath = paths.getWorldModsPath(worldName);
const targetModPath = path.join(worldModsPath, modName);
try {
await fs.access(worldPath);
} catch {
return res.status(404).json({ error: 'World not found' });
}
try {
await fs.access(globalModPath);
} catch {
return res.status(404).json({ error: 'Mod not found' });
}
try {
await fs.access(targetModPath);
return res.status(409).json({ error: 'Mod already installed in world' });
} catch {}
await fs.mkdir(worldModsPath, { recursive: true });
await fs.cp(globalModPath, targetModPath, { recursive: true });
res.redirect(`/mods?world=${worldName}&installed=${modName}`);
} catch (error) {
console.error('Error installing mod to world:', error);
res.status(500).json({ error: 'Failed to install mod to world' });
}
});
// Remove mod from world
router.post('/remove/:worldName/:modName', async (req, res) => {
try {
const { worldName, modName } = req.params;
if (!paths.isValidWorldName(worldName) || !paths.isValidModName(modName)) {
return res.status(400).json({ error: 'Invalid world or mod name' });
}
const worldModsPath = paths.getWorldModsPath(worldName);
const modPath = path.join(worldModsPath, modName);
try {
await fs.access(modPath);
} catch {
return res.status(404).json({ error: 'Mod not found in world' });
}
await fs.rm(modPath, { recursive: true, force: true });
res.redirect(`/mods?world=${worldName}&removed=${modName}`);
} catch (error) {
console.error('Error removing mod from world:', error);
res.status(500).json({ error: 'Failed to remove mod from world' });
}
});
// Delete global mod
router.post('/delete/:modName', async (req, res) => {
try {
const { modName } = req.params;
if (!paths.isValidModName(modName)) {
return res.status(400).json({ error: 'Invalid mod name' });
}
const modPath = paths.getModPath(modName);
try {
await fs.access(modPath);
} catch {
return res.status(404).json({ error: 'Mod not found' });
}
await fs.rm(modPath, { recursive: true, force: true });
res.redirect(`/mods?deleted=${modName}`);
} catch (error) {
console.error('Error deleting mod:', error);
res.status(500).json({ error: 'Failed to delete mod' });
}
});
// Mod details page
router.get('/:modName', async (req, res) => {
try {
const { modName } = req.params;
if (!paths.isValidModName(modName)) {
return res.status(400).render('error', {
error: 'Invalid mod name'
});
}
const modPath = paths.getModPath(modName);
const configPath = paths.getModConfigPath(modName);
try {
await fs.access(modPath);
} catch {
return res.status(404).render('error', {
error: 'Mod not found'
});
}
const config = await ConfigParser.parseModConfig(configPath);
const stats = await fs.stat(modPath);
// Get mod files info
let fileCount = 0;
let totalSize = 0;
async function countFiles(dirPath) {
try {
const items = await fs.readdir(dirPath);
for (const item of items) {
const itemPath = path.join(dirPath, item);
const itemStats = await fs.stat(itemPath);
if (itemStats.isDirectory()) {
await countFiles(itemPath);
} else {
fileCount++;
totalSize += itemStats.size;
}
}
} catch {}
}
await countFiles(modPath);
// Get worlds where this mod is installed
const installedWorlds = [];
try {
const worldDirs = await fs.readdir(paths.worldsDir);
for (const worldDir of worldDirs) {
try {
const worldModPath = path.join(paths.getWorldModsPath(worldDir), modName);
await fs.access(worldModPath);
const worldConfigPath = paths.getWorldConfigPath(worldDir);
const worldConfig = await ConfigParser.parseWorldConfig(worldConfigPath);
installedWorlds.push({
name: worldDir,
displayName: worldConfig.server_name || worldDir
});
} catch {}
}
} catch {}
const modDetails = {
name: modName,
title: config.title || modName,
description: config.description || '',
author: config.author || '',
depends: config.depends || [],
optional_depends: config.optional_depends || [],
min_minetest_version: config.min_minetest_version || '',
max_minetest_version: config.max_minetest_version || '',
location: 'global',
path: modPath,
fileCount,
totalSize,
created: stats.birthtime,
lastModified: stats.mtime,
installedWorlds: installedWorlds,
config: config
};
res.render('mods/details', {
title: `Mod: ${modDetails.title}`,
mod: modDetails,
currentPage: 'mods'
});
} catch (error) {
console.error('Error getting mod details:', error);
res.status(500).render('error', {
error: 'Failed to load mod details',
message: error.message
});
}
});
module.exports = router;