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>
This commit is contained in:
194
utils/paths.js
Normal file
194
utils/paths.js
Normal file
@@ -0,0 +1,194 @@
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const appConfig = require('./app-config');
|
||||
|
||||
class LuantiPaths {
|
||||
constructor() {
|
||||
// Initialize with default, will be updated when app config loads
|
||||
this.setDataDirectory(this.getDefaultDataDirectory());
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Load app config and update data directory
|
||||
await appConfig.load();
|
||||
const configuredDataDir = appConfig.getDataDirectory();
|
||||
this.setDataDirectory(configuredDataDir);
|
||||
}
|
||||
|
||||
getDefaultDataDirectory() {
|
||||
// Check for common Luanti data directories
|
||||
const homeDir = os.homedir();
|
||||
const possibleDirs = [
|
||||
path.join(homeDir, '.luanti'),
|
||||
path.join(homeDir, '.minetest')
|
||||
];
|
||||
|
||||
// Use the first one that exists, or default to .minetest
|
||||
for (const dir of possibleDirs) {
|
||||
if (fs.existsSync(dir)) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
return path.join(homeDir, '.minetest');
|
||||
}
|
||||
|
||||
setDataDirectory(dataDir) {
|
||||
this.minetestDir = path.resolve(dataDir);
|
||||
this.worldsDir = path.join(this.minetestDir, 'worlds');
|
||||
this.modsDir = path.join(this.minetestDir, 'mods');
|
||||
this.gamesDir = path.join(this.minetestDir, 'games');
|
||||
this.texturesDir = path.join(this.minetestDir, 'textures');
|
||||
this.configFile = path.join(this.minetestDir, 'minetest.conf');
|
||||
this.debugFile = path.join(this.minetestDir, 'debug.txt');
|
||||
}
|
||||
|
||||
getDataDirectory() {
|
||||
return this.minetestDir;
|
||||
}
|
||||
|
||||
getWorldPath(worldName) {
|
||||
return path.join(this.worldsDir, worldName);
|
||||
}
|
||||
|
||||
getWorldConfigPath(worldName) {
|
||||
return path.join(this.getWorldPath(worldName), 'world.mt');
|
||||
}
|
||||
|
||||
getWorldModsPath(worldName) {
|
||||
return path.join(this.getWorldPath(worldName), 'worldmods');
|
||||
}
|
||||
|
||||
getModPath(modName) {
|
||||
return path.join(this.modsDir, modName);
|
||||
}
|
||||
|
||||
getModConfigPath(modName) {
|
||||
return path.join(this.getModPath(modName), 'mod.conf');
|
||||
}
|
||||
|
||||
getGamePath(gameName) {
|
||||
return path.join(this.gamesDir, gameName);
|
||||
}
|
||||
|
||||
getGameConfigPath(gameName) {
|
||||
return path.join(this.getGamePath(gameName), 'game.conf');
|
||||
}
|
||||
|
||||
ensureDirectories() {
|
||||
const dirs = [this.minetestDir, this.worldsDir, this.modsDir, this.gamesDir, this.texturesDir];
|
||||
dirs.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isValidWorldName(name) {
|
||||
if (!name || typeof name !== 'string') return false;
|
||||
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length >= 3 && name.length <= 50;
|
||||
}
|
||||
|
||||
isValidModName(name) {
|
||||
if (!name || typeof name !== 'string') return false;
|
||||
return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 50;
|
||||
}
|
||||
|
||||
isPathSafe(targetPath) {
|
||||
const resolvedPath = path.resolve(targetPath);
|
||||
return resolvedPath.startsWith(path.resolve(this.minetestDir));
|
||||
}
|
||||
|
||||
mapToActualGameId(directoryName) {
|
||||
// Map directory names to the actual game IDs that Luanti recognizes
|
||||
// For most cases, the directory name IS the game ID
|
||||
const gameIdMap = {
|
||||
// Only add mappings here if you're certain they're needed
|
||||
// 'minetest_game': 'minetest', // This mapping was incorrect
|
||||
};
|
||||
|
||||
return gameIdMap[directoryName] || directoryName;
|
||||
}
|
||||
|
||||
async getInstalledGames() {
|
||||
const games = [];
|
||||
const possibleGameDirs = [
|
||||
this.gamesDir, // User games directory
|
||||
'/usr/share/luanti/games', // System games directory
|
||||
'/usr/share/minetest/games', // Legacy system games directory
|
||||
path.join(process.env.HOME || '/root', '.minetest/games'), // Explicit user path
|
||||
path.join(process.env.HOME || '/root', '.luanti/games') // New user path
|
||||
];
|
||||
|
||||
for (const gameDir of possibleGameDirs) {
|
||||
try {
|
||||
const exists = fs.existsSync(gameDir);
|
||||
if (!exists) continue;
|
||||
|
||||
const gameDirs = fs.readdirSync(gameDir);
|
||||
for (const gameName of gameDirs) {
|
||||
const possibleConfigPaths = [
|
||||
path.join(gameDir, gameName, 'game.conf'),
|
||||
path.join(gameDir, gameName, gameName, 'game.conf') // Handle nested structure
|
||||
];
|
||||
|
||||
for (const gameConfigPath of possibleConfigPaths) {
|
||||
try {
|
||||
if (fs.existsSync(gameConfigPath)) {
|
||||
const ConfigParser = require('./config-parser');
|
||||
const gameConfig = await ConfigParser.parseGameConfig(gameConfigPath);
|
||||
|
||||
// Map directory names to actual game IDs that Luanti recognizes
|
||||
const actualGameId = this.mapToActualGameId(gameName);
|
||||
|
||||
// Check if we already have this game (avoid duplicates by game ID, title, and resolved path)
|
||||
const resolvedPath = fs.realpathSync(path.dirname(gameConfigPath));
|
||||
const existingGame = games.find(g =>
|
||||
g.name === actualGameId ||
|
||||
(g.title === (gameConfig.title || gameConfig.name || gameName) && g.resolvedPath === resolvedPath)
|
||||
);
|
||||
if (!existingGame) {
|
||||
games.push({
|
||||
name: actualGameId, // Use the ID that Luanti recognizes
|
||||
directoryName: gameName, // Keep original for path resolution
|
||||
title: gameConfig.title || gameConfig.name || gameName,
|
||||
description: gameConfig.description || '',
|
||||
author: gameConfig.author || '',
|
||||
path: path.dirname(gameConfigPath),
|
||||
resolvedPath: resolvedPath,
|
||||
isSystemGame: !gameDir.includes(this.minetestDir)
|
||||
});
|
||||
}
|
||||
break; // Found valid config, stop checking other paths
|
||||
}
|
||||
} catch (gameError) {
|
||||
// Skip invalid games
|
||||
console.warn(`Invalid game at ${gameConfigPath}:`, gameError.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (dirError) {
|
||||
// Skip directories that can't be read
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort games: system games first, then minetest_game first, then alphabetically
|
||||
games.sort((a, b) => {
|
||||
if (a.isSystemGame !== b.isSystemGame) {
|
||||
return a.isSystemGame ? -1 : 1;
|
||||
}
|
||||
|
||||
// Put minetest_game first as it's the default
|
||||
if (a.name === 'minetest_game') return -1;
|
||||
if (b.name === 'minetest_game') return 1;
|
||||
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
|
||||
return games;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new LuantiPaths();
|
Reference in New Issue
Block a user