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 => { 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, }); } };