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:
256
utils/package-registry.js
Normal file
256
utils/package-registry.js
Normal file
@@ -0,0 +1,256 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
class PackageRegistry {
|
||||
constructor(dbPath = 'data/packages.db') {
|
||||
this.dbPath = dbPath;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Ensure data directory exists
|
||||
const dir = path.dirname(this.dbPath);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db = new sqlite3.Database(this.dbPath, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
this.createTables().then(resolve).catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async createTables() {
|
||||
const sql = `
|
||||
CREATE TABLE IF NOT EXISTS installed_packages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
author TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
version TEXT,
|
||||
release_id TEXT,
|
||||
install_location TEXT NOT NULL, -- 'global' or 'world:worldname'
|
||||
install_path TEXT NOT NULL,
|
||||
installed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
contentdb_url TEXT,
|
||||
package_type TEXT, -- 'mod', 'game', 'txp'
|
||||
title TEXT,
|
||||
short_description TEXT,
|
||||
dependencies TEXT, -- JSON string of dependencies
|
||||
UNIQUE(author, name, install_location)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_package_location ON installed_packages(install_location);
|
||||
CREATE INDEX IF NOT EXISTS idx_package_name ON installed_packages(author, name);
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.exec(sql, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async recordInstallation(packageInfo) {
|
||||
const {
|
||||
author,
|
||||
name,
|
||||
version,
|
||||
releaseId,
|
||||
installLocation,
|
||||
installPath,
|
||||
contentdbUrl,
|
||||
packageType,
|
||||
title,
|
||||
shortDescription,
|
||||
dependencies = []
|
||||
} = packageInfo;
|
||||
|
||||
const sql = `
|
||||
INSERT OR REPLACE INTO installed_packages
|
||||
(author, name, version, release_id, install_location, install_path,
|
||||
contentdb_url, package_type, title, short_description, dependencies, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`;
|
||||
|
||||
const dependenciesJson = JSON.stringify(dependencies);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, [
|
||||
author, name, version, releaseId, installLocation, installPath,
|
||||
contentdbUrl, packageType, title, shortDescription, dependenciesJson
|
||||
], function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ id: this.lastID, changes: this.changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledPackages(location = null) {
|
||||
let sql = 'SELECT * FROM installed_packages';
|
||||
const params = [];
|
||||
|
||||
if (location) {
|
||||
if (location === 'global') {
|
||||
sql += ' WHERE install_location = ?';
|
||||
params.push('global');
|
||||
} else if (location.startsWith('world:')) {
|
||||
sql += ' WHERE install_location = ?';
|
||||
params.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
sql += ' ORDER BY installed_at DESC';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.all(sql, params, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// Parse dependencies JSON for each row
|
||||
const packages = rows.map(row => ({
|
||||
...row,
|
||||
dependencies: row.dependencies ? JSON.parse(row.dependencies) : []
|
||||
}));
|
||||
resolve(packages);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getInstalledPackage(author, name, location) {
|
||||
const sql = `
|
||||
SELECT * FROM installed_packages
|
||||
WHERE author = ? AND name = ? AND install_location = ?
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(sql, [author, name, location], (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (row) {
|
||||
row.dependencies = row.dependencies ? JSON.parse(row.dependencies) : [];
|
||||
resolve(row);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async isPackageInstalled(author, name, location) {
|
||||
const packageInfo = await this.getInstalledPackage(author, name, location);
|
||||
return packageInfo !== null;
|
||||
}
|
||||
|
||||
async removePackage(author, name, location) {
|
||||
const sql = `
|
||||
DELETE FROM installed_packages
|
||||
WHERE author = ? AND name = ? AND install_location = ?
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, [author, name, location], function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ changes: this.changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getPackagesByWorld(worldName) {
|
||||
return this.getInstalledPackages(`world:${worldName}`);
|
||||
}
|
||||
|
||||
async getGlobalPackages() {
|
||||
return this.getInstalledPackages('global');
|
||||
}
|
||||
|
||||
async getAllInstallations() {
|
||||
return this.getInstalledPackages();
|
||||
}
|
||||
|
||||
async updatePackageInfo(author, name, location, updates) {
|
||||
const setClause = [];
|
||||
const params = [];
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (key === 'dependencies' && Array.isArray(value)) {
|
||||
setClause.push(`${key} = ?`);
|
||||
params.push(JSON.stringify(value));
|
||||
} else {
|
||||
setClause.push(`${key} = ?`);
|
||||
params.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
setClause.push('updated_at = CURRENT_TIMESTAMP');
|
||||
|
||||
const sql = `
|
||||
UPDATE installed_packages
|
||||
SET ${setClause.join(', ')}
|
||||
WHERE author = ? AND name = ? AND install_location = ?
|
||||
`;
|
||||
|
||||
params.push(author, name, location);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.run(sql, params, function(err) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve({ changes: this.changes });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async getStatistics() {
|
||||
const sql = `
|
||||
SELECT
|
||||
COUNT(*) as total_packages,
|
||||
COUNT(CASE WHEN install_location = 'global' THEN 1 END) as global_packages,
|
||||
COUNT(CASE WHEN install_location LIKE 'world:%' THEN 1 END) as world_packages,
|
||||
COUNT(DISTINCT CASE WHEN install_location LIKE 'world:%' THEN install_location END) as worlds_with_packages
|
||||
FROM installed_packages
|
||||
`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(sql, [], (err, row) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.db) {
|
||||
return new Promise((resolve) => {
|
||||
this.db.close((err) => {
|
||||
if (err) {
|
||||
console.error('Error closing database:', err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PackageRegistry;
|
Reference in New Issue
Block a user