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>
256 lines
6.7 KiB
JavaScript
256 lines
6.7 KiB
JavaScript
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; |