Files
LuHost/utils/paths.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

194 lines
6.3 KiB
JavaScript

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();