Files
mcphub/src/controllers/dxtController.ts

155 lines
4.6 KiB
TypeScript

import { Request, Response } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import AdmZip from 'adm-zip';
import { ApiResponse } from '../types/index.js';
// Configure multer for file uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = path.join(process.cwd(), 'data/uploads/dxt');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const timestamp = Date.now();
const originalName = path.parse(file.originalname).name;
cb(null, `${originalName}-${timestamp}.dxt`);
},
});
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (file.originalname.endsWith('.dxt')) {
cb(null, true);
} else {
cb(new Error('Only .dxt files are allowed'));
}
},
limits: {
fileSize: 500 * 1024 * 1024, // 500MB limit
},
});
export const uploadMiddleware = upload.single('dxtFile');
// Clean up old DXT server files when installing a new version
const cleanupOldDxtServer = (serverName: string): void => {
try {
const uploadDir = path.join(process.cwd(), 'data/uploads/dxt');
const serverPattern = `server-${serverName}`;
if (fs.existsSync(uploadDir)) {
const files = fs.readdirSync(uploadDir);
files.forEach((file) => {
if (file.startsWith(serverPattern)) {
const filePath = path.join(uploadDir, file);
if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
console.log(`Cleaned up old DXT server directory: ${filePath}`);
}
}
});
}
} catch (error) {
console.warn('Failed to cleanup old DXT server files:', error);
// Don't fail the installation if cleanup fails
}
};
export const uploadDxtFile = async (req: Request, res: Response): Promise<void> => {
try {
if (!req.file) {
res.status(400).json({
success: false,
message: 'No DXT file uploaded',
});
return;
}
const dxtFilePath = req.file.path;
const timestamp = Date.now();
const tempExtractDir = path.join(path.dirname(dxtFilePath), `temp-extracted-${timestamp}`);
try {
// Extract the DXT file (which is a ZIP archive) to a temporary directory first
const zip = new AdmZip(dxtFilePath);
zip.extractAllTo(tempExtractDir, true);
// Read and validate the manifest.json
const manifestPath = path.join(tempExtractDir, 'manifest.json');
if (!fs.existsSync(manifestPath)) {
throw new Error('manifest.json not found in DXT file');
}
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
const manifest = JSON.parse(manifestContent);
// Validate required fields in manifest
if (!manifest.dxt_version) {
throw new Error('Invalid manifest: missing dxt_version');
}
if (!manifest.name) {
throw new Error('Invalid manifest: missing name');
}
if (!manifest.version) {
throw new Error('Invalid manifest: missing version');
}
if (!manifest.server) {
throw new Error('Invalid manifest: missing server configuration');
}
// Use server name as the final extract directory for automatic version management
const finalExtractDir = path.join(path.dirname(dxtFilePath), `server-${manifest.name}`);
// Clean up any existing version of this server
cleanupOldDxtServer(manifest.name);
if (!fs.existsSync(finalExtractDir)) {
fs.mkdirSync(finalExtractDir, { recursive: true });
}
// Move the temporary directory to the final location
fs.renameSync(tempExtractDir, finalExtractDir);
console.log(`DXT server extracted to: ${finalExtractDir}`);
// Clean up the uploaded DXT file
fs.unlinkSync(dxtFilePath);
const response: ApiResponse = {
success: true,
data: {
manifest,
extractDir: finalExtractDir,
},
};
res.json(response);
} catch (extractError) {
// Clean up files on error
if (fs.existsSync(dxtFilePath)) {
fs.unlinkSync(dxtFilePath);
}
if (fs.existsSync(tempExtractDir)) {
fs.rmSync(tempExtractDir, { recursive: true, force: true });
}
throw extractError;
}
} catch (error) {
console.error('DXT upload error:', error);
let message = 'Failed to process DXT file';
if (error instanceof Error) {
message = error.message;
}
res.status(500).json({
success: false,
message,
});
}
};