import os import shutil import hashlib import json import zipfile from pathlib import Path from datetime import datetime from typing import List, Dict, Optional from werkzeug.utils import secure_filename from config import config import humanize class ModManager: def __init__(self): self.mods_dir = config.MODS_DIR self.mods_data_file = config.SERVER_DIR / "mods_data.json" self.mods_data = self._load_data() self.allowed_extensions = {'.jar'} # Ensure mods directory exists self.mods_dir.mkdir(parents=True, exist_ok=True) def _load_data(self) -> Dict: """Load mods metadata""" if self.mods_data_file.exists(): try: with open(self.mods_data_file, 'r') as f: return json.load(f) except: pass return {'mods': {}} def _save_data(self): """Save mods metadata""" with open(self.mods_data_file, 'w') as f: json.dump(self.mods_data, f, indent=2) def _get_file_hash(self, filepath: Path) -> str: """Get MD5 hash of file""" hash_md5 = hashlib.md5() with open(filepath, 'rb') as f: for chunk in iter(lambda: f.read(4096), b''): hash_md5.update(chunk) return hash_md5.hexdigest() def get_installed_mods(self) -> List[Dict]: """Get list of installed mods""" mods = [] for file in self.mods_dir.iterdir(): if file.suffix.lower() == '.jar': mod_info = self._get_mod_info(file) mods.append(mod_info) return sorted(mods, key=lambda x: x['name'].lower()) def _get_mod_info(self, filepath: Path) -> Dict: """Extract mod information""" stat = filepath.stat() file_hash = self._get_file_hash(filepath) # Check if we have cached metadata cached = self.mods_data['mods'].get(file_hash, {}) # Try to read mod info from jar mod_name = filepath.stem mod_version = "Unknown" mod_description = "" mod_authors = [] try: with zipfile.ZipFile(filepath, 'r') as jar: # Try fabric.mod.json if 'fabric.mod.json' in jar.namelist(): with jar.open('fabric.mod.json') as f: mod_meta = json.load(f) mod_name = mod_meta.get('name', mod_name) mod_version = mod_meta.get('version', mod_version) mod_description = mod_meta.get('description', '') mod_authors = mod_meta.get('authors', []) if isinstance(mod_authors, list): mod_authors = [a if isinstance(a, str) else a.get('name', '') for a in mod_authors] except: pass # Check if mod is disabled is_disabled = filepath.name.endswith('.disabled') return { 'filename': filepath.name, 'name': mod_name, 'version': mod_version, 'description': mod_description, 'authors': mod_authors, 'size': humanize.naturalsize(stat.st_size), 'size_bytes': stat.st_size, 'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(), 'hash': file_hash, 'enabled': not is_disabled, 'notes': cached.get('notes', '') } def upload_mod(self, file) -> Dict: """Upload a new mod""" if not file or not file.filename: return {'success': False, 'message': 'No file provided'} filename = secure_filename(file.filename) if not filename.endswith('.jar'): return {'success': False, 'message': 'Only .jar files are allowed'} filepath = self.mods_dir / filename # Check if file already exists if filepath.exists(): return {'success': False, 'message': f'Mod {filename} already exists'} try: file.save(str(filepath)) mod_info = self._get_mod_info(filepath) # Store metadata self.mods_data['mods'][mod_info['hash']] = { 'uploaded': datetime.now().isoformat(), 'original_name': filename, 'notes': '' } self._save_data() return { 'success': True, 'message': f'Mod {mod_info["name"]} uploaded successfully', 'mod': mod_info } except Exception as e: return {'success': False, 'message': str(e)} def delete_mod(self, filename: str) -> Dict: """Delete a mod""" filepath = self.mods_dir / secure_filename(filename) if not filepath.exists(): return {'success': False, 'message': 'Mod not found'} try: # Get hash before deleting file_hash = self._get_file_hash(filepath) filepath.unlink() # Remove from metadata if file_hash in self.mods_data['mods']: del self.mods_data['mods'][file_hash] self._save_data() return {'success': True, 'message': f'Mod {filename} deleted'} except Exception as e: return {'success': False, 'message': str(e)} def toggle_mod(self, filename: str) -> Dict: """Enable/disable a mod""" filename = secure_filename(filename) if filename.endswith('.disabled'): # Enable disabled_path = self.mods_dir / filename enabled_path = self.mods_dir / filename[:-9] # Remove .disabled if not disabled_path.exists(): return {'success': False, 'message': 'Mod not found'} disabled_path.rename(enabled_path) return {'success': True, 'message': f'Mod enabled: {enabled_path.name}', 'enabled': True} else: # Disable enabled_path = self.mods_dir / filename disabled_path = self.mods_dir / (filename + '.disabled') if not enabled_path.exists(): return {'success': False, 'message': 'Mod not found'} enabled_path.rename(disabled_path) return {'success': True, 'message': f'Mod disabled: {filename}', 'enabled': False} def get_mod_details(self, filename: str) -> Optional[Dict]: """Get detailed mod information""" filepath = self.mods_dir / secure_filename(filename) if filepath.exists(): return self._get_mod_info(filepath) return None def update_notes(self, filename: str, notes: str) -> Dict: """Update mod notes""" filepath = self.mods_dir / secure_filename(filename) if not filepath.exists(): return {'success': False, 'message': 'Mod not found'} file_hash = self._get_file_hash(filepath) if file_hash not in self.mods_data['mods']: self.mods_data['mods'][file_hash] = {} self.mods_data['mods'][file_hash]['notes'] = notes self._save_data() return {'success': True, 'message': 'Notes updated'} def get_total_size(self) -> str: """Get total size of all mods""" total = sum(f.stat().st_size for f in self.mods_dir.iterdir() if f.is_file()) return humanize.naturalsize(total) def export_mod_list(self) -> List[Dict]: """Export mod list for backup""" mods = self.get_installed_mods() return [{ 'name': m['name'], 'version': m['version'], 'filename': m['filename'], 'hash': m['hash'] } for m in mods]