import json import os import shutil import subprocess import docker import requests from flask import Blueprint, jsonify, request, send_from_directory, abort, Response from .auth import oidc from .docker_utils import start_server, stop_server, restart_server, get_logs, delete_server, save_server_info, \ DEFAULT_CONFIG api = Blueprint('api', __name__) client = docker.from_env() # Server Deployment @api.route('/setup', methods=['POST']) @oidc.require_login def setup_server(): data = request.get_json() username = oidc.user_getfield('preferred_username') type_ = data['type'] version = data['version'] path = f"./servers/mc-{username}" os.makedirs(path, exist_ok=True) save_server_info(username, type_, version) return jsonify({'success': True}) @api.route('/delete', methods=['POST']) @oidc.require_login def delete(): username = oidc.user_getfield('preferred_username') if not username: return jsonify({"error": "Brak nazwy użytkownika"}), 400 result = delete_server(username) return jsonify({"message": result}) # Server Controls @api.route('/start', methods=['POST']) @oidc.require_login def start(): username = oidc.user_getfield('preferred_username') setup_file_path = f"./servers/mc-{username}/server_info.json" if not os.path.exists(setup_file_path): return jsonify({"error": "Server setup file not found."}), 400 with open(setup_file_path, 'r') as file: server_info = json.load(file) server_type = server_info.get('type') server_version = server_info.get('version') if not server_type or not server_version: return jsonify({"error": "Invalid server info."}), 400 start_server(username) return jsonify({"status": "started"}) @api.route('/stop', methods=['POST']) @oidc.require_login def stop(): username = oidc.user_getfield('preferred_username') stop_server(username) return jsonify({"status": "stopped"}) @api.route('/restart', methods=['POST']) @oidc.require_login def restart(): username = oidc.user_getfield('preferred_username') restart_server(username) return jsonify({"status": "restarted"}) @api.route('/logs', methods=['GET']) @oidc.require_login def logs(): username = oidc.user_getfield('preferred_username') return jsonify({"logs": get_logs(username)}) @api.route('/command', methods=['POST']) @oidc.require_login def send_command(): data = request.get_json() username = data['username'] command = data['command'] if username != oidc.user_getfield('preferred_username'): return jsonify({"error": "Unauthorized request."}), 403 container_name = f"mc-{username}" try: subprocess.run( ["docker", "exec", container_name, "rcon-cli", command], check=True, capture_output=True ) return jsonify(success=True) except subprocess.CalledProcessError as e: return jsonify(success=False, error=str(e)), 500 # Files APIs (Upload, download, delete) @api.route('/files', methods=['GET']) @oidc.require_login def list_files(): username = oidc.user_getfield('preferred_username') path = request.args.get('path', '') base_path = os.path.abspath(f'./servers/mc-{username}') requested_path = os.path.abspath(os.path.join(base_path, path)) if not requested_path.startswith(base_path): return abort(403) # Prevent directory traversal if not os.path.exists(requested_path): return abort(404) entries = [] for item in os.listdir(requested_path): if item == "server_info.json": # Hiding panel-specific files continue full_path = os.path.join(requested_path, item) entries.append({ 'name': item, 'is_dir': os.path.isdir(full_path) }) return jsonify(entries) @api.route('/files/download', methods=['GET']) @oidc.require_login def download_file(): username = oidc.user_getfield('preferred_username') path = request.args.get('path') base_path = os.path.abspath(f'./servers/mc-{username}') file_path = os.path.abspath(os.path.join(base_path, path)) if not file_path.startswith(base_path) or not os.path.isfile(file_path): return abort(403) directory = os.path.dirname(file_path) filename = os.path.basename(file_path) return send_from_directory(directory, filename, as_attachment=True) @api.route('/files/upload', methods=['POST']) @oidc.require_login def upload_file(): username = oidc.user_getfield('preferred_username') path = request.form.get('path', '') file = request.files['files'] base_path = os.path.abspath(f'./servers/mc-{username}') upload_path = os.path.abspath(os.path.join(base_path, path)) if not upload_path.startswith(base_path): return abort(403) os.makedirs(upload_path, exist_ok=True) file.save(os.path.join(upload_path, file.filename)) return jsonify({'success': True}) @api.route('/files/delete', methods=['POST']) @oidc.require_login def delete_file_or_folder(): data = request.get_json() username = data.get('username') if username != oidc.user_getfield('preferred_username'): return jsonify({"error": "Unauthorized request."}), 403 path = data.get('path') base_path = os.path.abspath(f'./servers/mc-{username}') target_path = os.path.abspath(os.path.join(base_path, path)) if not target_path.startswith(base_path): return abort(403) if not os.path.exists(target_path): return jsonify({"error": "Nie znaleziono pliku lub folderu"}), 404 try: if os.path.isdir(target_path): shutil.rmtree(target_path) else: os.remove(target_path) return jsonify({"success": True}) except Exception as e: return jsonify({"error": str(e)}), 500 # Server config @api.route('/config') @oidc.require_login def get_config(): username = oidc.user_getfield('preferred_username') server_info_path = f'./servers/mc-{username}/server_info.json' if not os.path.exists(server_info_path): return jsonify({"success": False, "message": "Server config not found"}) with open(server_info_path, 'r') as f: server_info = json.load(f) return jsonify({"success": True, "config": server_info["config"], "version": server_info["version"], "type": server_info["type"]}) @api.route('/config', methods=['POST']) @oidc.require_login def update_config(): data = request.json username = data.get('username') if username != oidc.user_getfield('preferred_username'): return jsonify({"error": "Unauthorized request."}), 403 incoming_config = data.get('config', {}) server_info_path = f'./servers/mc-{username}/server_info.json' if os.path.exists(server_info_path): with open(server_info_path, 'r') as f: server_info = json.load(f) else: server_info = DEFAULT_CONFIG.copy() for key in ["type", "version"]: if key in incoming_config: server_info[key] = incoming_config.pop(key) formatted_config = {key.replace('_', '-'): value for key, value in incoming_config.items()} server_info["config"] = formatted_config with open(server_info_path, 'w') as f: json.dump(server_info, f, indent=4) return jsonify({"success": True}) # Server stats @api.route("/status") def status(): username = request.args.get("username") container_name = f"mc-{username}" try: container = client.containers.get(container_name) return jsonify(running=container.status == "running") except docker.errors.NotFound: return jsonify(running=False) @api.route('/stats', methods=['GET']) @oidc.require_login def stats(): username = oidc.user_getfield("preferred_username") container_name = f"mc-{username}" try: container = client.containers.get(container_name) stats = container.stats(stream=False) # RAM (MB) memory_usage = stats['memory_stats']['usage'] / (1024 * 1024) # CPU % cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage'] system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage'] percpu = stats['cpu_stats']['cpu_usage'].get('percpu_usage') cpu_count = len(percpu) if percpu else 1 cpu_usage = (cpu_delta / system_delta) * cpu_count * 100 if system_delta > 0 else 0 # Disk Usage (GB) server_path = f"./servers/{container_name}" total_size = sum( os.path.getsize(os.path.join(dp, f)) for dp, dn, filenames in os.walk(server_path) for f in filenames ) / (1024 * 1024 * 1024) disk_usage = min(total_size, 15) return jsonify({ "cpu": round(cpu_usage, 2), "ram": round(memory_usage, 2), "disk": round(disk_usage, 2), "disk_max": 15 }) except docker.errors.NotFound: return jsonify({"error": "Container not found"}), 404 # Modrinth @api.route('/modrinth/search') def modrinth_search(): query = request.args.get("query") server_type = request.args.get("type") # 'fabric', 'paper', etc. version = request.args.get("version") categories = { 'fabric': 'mod', 'paper': 'plugin' } category = categories.get(server_type, 'mod') # fallback response = requests.get( f"https://api.modrinth.com/v2/search", params={ "query": query, "facets": f'[["project_type:{category}"],["categories:{server_type}"],["versions:{version}"]]', "limit": 10 } ) return jsonify(response.json()) @api.route('/modrinth/download', methods=['POST']) @oidc.require_login def download_mod(): data = request.json project_id = data['project_id'] username = data['username'] server_type = data['type'] server_version = data['version'] base_path = f"./servers/mc-{username}/{'plugins' if server_type == 'paper' else 'mods'}" downloaded = [] def download_project(pid, version_id=None): if version_id: version_data = requests.get(f"https://api.modrinth.com/v2/version/{version_id}").json() else: all_versions = requests.get(f"https://api.modrinth.com/v2/project/{pid}/version").json() version_data = next((v for v in all_versions if server_version in v['game_versions'] and server_type in v['loaders']), None) if not version_data: return file = version_data['files'][0] file_path = f"{base_path}/{file['filename']}" if file['filename'] not in downloaded: with requests.get(file['url'], stream=True) as r: with open(file_path, 'wb') as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) downloaded.append(file['filename']) for dep in version_data.get('dependencies', []): if dep.get('version_id'): download_project(dep['project_id'], dep['version_id']) download_project(project_id) return jsonify({"success": True, "downloaded": downloaded})