import { FileUtils } from '../utils/FileUtils'; import { Logger } from '../utils/Logger'; import * as path from 'path'; import axios from 'axios'; import * as fs from 'fs-extra'; import * as stream from 'stream'; import * as util from 'util'; const pipeline = util.promisify(stream.pipeline); const finished = util.promisify(stream.finished); export class PluginManager { private logger = Logger.getInstance(); public async installPlugin(name: string, url: string, pluginsDir: string): Promise { const pluginJar = path.join(pluginsDir, `${name}.jar`); if (await FileUtils.fileExists(pluginJar)) { this.logger.info(`${name} already exists, skipping download`); return; } this.logger.info(`Downloading ${name}...`); try { const response = await axios({ method: 'GET', url: url, responseType: 'stream', timeout: 60000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'application/octet-stream' }, maxRedirects: 5 }); if (response.status !== 200) { throw new Error(`HTTP ${response.status}`); } const writer = fs.createWriteStream(pluginJar); await pipeline(response.data, writer); await finished(writer); const stats = await fs.stat(pluginJar); if (stats.size < 100000) { await fs.remove(pluginJar); throw new Error(`File too small: ${stats.size} bytes`); } this.logger.success(`${name} downloaded (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to download ${name}: ${errorMessage}`); } } public async installMultiple(plugins: Array<{name: string, url: string}>, pluginsDir: string): Promise { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; await this.installPlugin(plugin.name, plugin.url, pluginsDir); if (i < plugins.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } } public async installFromGithub(repo: string, assetName: string, pluginsDir: string): Promise { const apiUrl = `https://api.github.com/repos/${repo}/releases/latest`; try { const response = await axios.get(apiUrl, { headers: { 'User-Agent': 'Mozilla/5.0' } }); const asset = response.data.assets.find((a: any) => a.name.includes(assetName)); if (!asset) { throw new Error(`Asset ${assetName} not found in latest release`); } await this.installPlugin( asset.name.replace('.jar', ''), asset.browser_download_url, pluginsDir ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to install from GitHub ${repo}: ${errorMessage}`); } } public async updatePlugin(name: string, url: string, pluginsDir: string): Promise { const pluginJar = path.join(pluginsDir, `${name}.jar`); if (await FileUtils.fileExists(pluginJar)) { await fs.remove(pluginJar); } await this.installPlugin(name, url, pluginsDir); } public async removePlugin(name: string, pluginsDir: string): Promise { const pluginJar = path.join(pluginsDir, `${name}.jar`); if (await FileUtils.fileExists(pluginJar)) { await fs.remove(pluginJar); this.logger.info(`${name} removed`); } } public async listPlugins(pluginsDir: string): Promise { if (!await FileUtils.fileExists(pluginsDir)) { return []; } const files = await fs.readdir(pluginsDir); return files.filter(f => f.endsWith('.jar')).map(f => f.replace('.jar', '')); } public async getPluginInfo(pluginName: string, pluginsDir: string): Promise { const pluginJar = path.join(pluginsDir, `${pluginName}.jar`); if (!await FileUtils.fileExists(pluginJar)) { return null; } const stats = await fs.stat(pluginJar); return { name: pluginName, size: stats.size, modified: stats.mtime, path: pluginJar }; } public async verifyPlugin(pluginName: string, pluginsDir: string): Promise { const pluginJar = path.join(pluginsDir, `${pluginName}.jar`); if (!await FileUtils.fileExists(pluginJar)) { return false; } const stats = await fs.stat(pluginJar); if (stats.size < 100000) { return false; } try { const buffer = await fs.readFile(pluginJar); if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) { return false; } } catch { return false; } return true; } public async cleanupCorruptPlugins(pluginsDir: string): Promise { if (!await FileUtils.fileExists(pluginsDir)) { return 0; } let cleaned = 0; const files = await fs.readdir(pluginsDir); for (const file of files) { if (!file.endsWith('.jar')) continue; const filePath = path.join(pluginsDir, file); try { const stats = await fs.stat(filePath); if (stats.size < 100000) { await fs.remove(filePath); this.logger.warning(`Removed corrupt plugin: ${file}`); cleaned++; continue; } const buffer = await fs.readFile(filePath); if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) { await fs.remove(filePath); this.logger.warning(`Removed invalid ZIP: ${file}`); cleaned++; } } catch { await fs.remove(filePath); this.logger.warning(`Removed inaccessible plugin: ${file}`); cleaned++; } } if (cleaned > 0) { this.logger.info(`Cleaned up ${cleaned} corrupt plugins`); } return cleaned; } }