import gradio as gr
import os
import subprocess
import threading
import shutil
import spaces
import asyncio
import json
import time
import re
import base64
try:
import psutil
except ImportError:
subprocess.run(["pip", "install", "psutil"], capture_output=True)
import psutil
try:
import huggingface_hub
except ImportError:
subprocess.run(["pip", "install", "huggingface_hub>=0.20.0"], capture_output=True)
import huggingface_hub
WORKDIR = "/home/user/app/server"
os.makedirs(WORKDIR, exist_ok=True)
log_output = []
mc_process = None
# ─── نظام الصلاحيات والمستخدمين ───
USERS_FILE = os.path.join(WORKDIR, "users.json")
PERMISSIONS = {
"admin": ["console", "start", "stop", "restart", "files_read", "files_write", "files_delete", "users_manage", "cloud_manage"],
"editor": ["console", "files_read", "files_write", "cloud_manage"],
"viewer": ["console", "files_read"],
}
def load_users():
try:
if os.path.exists(USERS_FILE):
with open(USERS_FILE) as f:
return json.load(f)
except: pass
return {}
def save_users(users):
try:
with open(USERS_FILE, "w") as f:
json.dump(users, f, indent=2)
except: pass
def get_user_role(username):
if not username:
return None
users = load_users()
return users.get(username, None)
LOCAL_DEV_MODE = True
def has_perm(username, perm):
if username is None:
return LOCAL_DEV_MODE
role = get_user_role(username)
if role is None:
return False
return perm in PERMISSIONS.get(role, [])
@spaces.GPU
def activate_gpu():
return "[SYSTEM] GPU Activated."
def log(message):
log_output.append(message)
if len(log_output) > 400:
log_output.pop(0)
start_time = time.time()
_last_auto_sync_time = time.time()
# ─── محرك التزامن المباشر مع Hugging Face Dataset (Server/) ───
HF_CONFIG_FILE = os.path.join(WORKDIR, "hf_sync.json")
def load_hf_config():
try:
if os.path.exists(HF_CONFIG_FILE):
with open(HF_CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except: pass
return {
"hf_token": os.environ.get("HF_TOKEN", "").strip(),
"hf_dataset_repo": os.environ.get("HF_DATASET_REPO", "").strip(),
"auto_sync": True,
"sync_interval": 10
}
def save_hf_config(config_dict):
try:
with open(HF_CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config_dict, f, indent=2, ensure_ascii=False)
return True, "✅ تم حفظ إعدادات التزامن المباشر مع الـ Dataset بنجاح!"
except Exception as e:
return False, f"❌ فشل حفظ الإعدادات: {e}"
def get_full_repo_id(repo_id, token):
"""
دالة لتصحيح اسم المستودع تلقائياً إذا تم إدخاله بدون اسم المستخدم (مثل: SAW).
"""
if not repo_id:
return None
if "/" not in repo_id and token:
try:
from huggingface_hub import HfApi
api = HfApi(token=token)
user_info = api.whoami()
username = user_info.get("name")
if username:
resolved = f"{username}/{repo_id}"
log(f"[DATASET SYNC] 🔄 تم تصحيح اسم المستودع تلقائياً إلى: {resolved}")
return resolved
except Exception as e:
log(f"[DATASET SYNC WARNING] لم نتمكن من التعرف على اسم المستخدم لتصحيح المستودع تلقائياً: {e}")
return repo_id
def init_dataset_and_pull():
"""
1. التحقق من وجود الـ Dataset على Hugging Face أو إنشاؤه فوراً باسم المستخدم الصحيح.
2. سحب وتنزيل محتويات مجلد Server/ إلى WORKDIR.
"""
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
msg = "⚠️ التزامن المباشر: لم يتم ضبط HF_TOKEN أو HF_DATASET_REPO. سيتم العمل على المجلد المحلي فقط."
log(f"[DATASET SYNC] {msg}")
return False, msg
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "❌ اسم مستودع الـ Dataset غير صالح."
try:
from huggingface_hub import HfApi, snapshot_download
api = HfApi(token=token)
# 1. إنشاء الـ Dataset أو التحقق منه
try:
log(f"[DATASET SYNC] 🛡️ التحقق من وجود الـ Dataset [{resolved_repo_id}] أو إنشاؤه الآن...")
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
log(f"[DATASET SYNC] ✅ الـ Dataset [{resolved_repo_id}] جاهز ومربوط.")
except Exception as create_err:
err_str = str(create_err)
if "403" in err_str or "401" in err_str:
msg = f"❌ خطأ صلاحيات (Forbidden/Unauthorized) عند التحقق من الـ Dataset [{resolved_repo_id}]: تأكد من صلاحية Write للتوكن الخاص بك."
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# في حال وجود خطأ آخر حرج نوقف العملية ولا نحاول التحميل أو الرفع
msg = f"❌ فشل تهيئة مستودع الـ Dataset [{resolved_repo_id}]: {create_err}"
log(f"[DATASET SYNC ERROR] {msg}")
return False, msg
# 2. تنزيل وسحب الملفات
local_tmp = os.path.join(WORKDIR, "../_cloud_restore_tmp")
if os.path.exists(local_tmp): shutil.rmtree(local_tmp, ignore_errors=True)
os.makedirs(local_tmp, exist_ok=True)
try:
log(f"[DATASET SYNC] 📥 جاري تنزيل ملفات السيرفر من المجلد Server/ في الـ Dataset [{resolved_repo_id}] ...")
snapshot_download(
repo_id=resolved_repo_id,
repo_type="dataset",
allow_patterns=["Server/*"],
local_dir=local_tmp,
token=token
)
server_cloud_dir = os.path.join(local_tmp, "Server")
if os.path.exists(server_cloud_dir) and os.listdir(server_cloud_dir):
file_count = 0
for root, dirs, files in os.walk(server_cloud_dir):
rel_path = os.path.relpath(root, server_cloud_dir)
dest_dir = os.path.join(WORKDIR, rel_path) if rel_path != "." else WORKDIR
os.makedirs(dest_dir, exist_ok=True)
for f in files:
shutil.copy2(os.path.join(root, f), os.path.join(dest_dir, f))
file_count += 1
shutil.rmtree(local_tmp, ignore_errors=True)
msg = f"✅ تم تنزيل ({file_count}) ملف من المجلد Server/ داخل الـ Dataset [{resolved_repo_id}] بنجاح!"
log(f"[DATASET SYNC] {msg}")
return True, msg
else:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] ℹ️ المجلد Server/ غير موجود بعد في الـ Dataset [{resolved_repo_id}]. سيتم إنشاؤه ورفع الملفات الآن...")
push_to_dataset()
return True, f"✅ تم إنشاء المجلد Server/ داخل الـ Dataset [{resolved_repo_id}] ومزامنة الملفات."
except Exception as inner_e:
shutil.rmtree(local_tmp, ignore_errors=True)
log(f"[DATASET SYNC] المجلد Server غير متوفر بعد، جاري التأسيس والرفع الأولي: {inner_e}")
push_to_dataset()
return True, f"✅ تم تهيئة المجلد Server/ في الـ Dataset [{resolved_repo_id}]."
except Exception as e:
err_msg = f"❌ خطأ في الاتصال بالـ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def push_to_dataset():
"""
مزامنة ورفع كل الملفات مباشرة في المجلد Server/ داخل الـ Dataset.
"""
config = load_hf_config()
token = config.get("hf_token") or os.environ.get("HF_TOKEN", "").strip()
repo_id = config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO", "").strip()
if not token or not repo_id:
return False, "⚠️ أدخل HF Token واسم الـ Dataset أولاً في تبويب '☁️ التزامن المباشر'."
resolved_repo_id = get_full_repo_id(repo_id, token)
if not resolved_repo_id:
return False, "❌ اسم مستودع الـ Dataset غير صالح."
try:
log(f"[DATASET SYNC] 📤 جاري مزامنة ورفع ملفات السيرفر وإعدادات الـ IP إلى [{resolved_repo_id}]/Server ...")
from huggingface_hub import HfApi
api = HfApi(token=token)
# التحقق من وجود المستودع قبل محاولة الرفع لتلافي أخطاء الـ 404
try:
api.create_repo(repo_id=resolved_repo_id, repo_type="dataset", exist_ok=True, private=True, token=token)
except Exception as ce:
err_str = str(ce)
if "403" in err_str or "401" in err_str or "404" in err_str:
err_msg = f"❌ خطأ أذونات أو عدم وجود المستودع ({ce}) عند التحقق من الـ Dataset [{resolved_repo_id}]."
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
# في حال وجود مشكلة تأسيسية أخرى نلغي الرفع لمنع تكرار الأخطاء السابقة
err_msg = f"❌ فشل تأسيس المستودع [{resolved_repo_id}]: {ce}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
ignore_list = [
"jdk/*", "jdk.tar.gz", "logs/*", "*.lock", "*.tmp", ".cache/*", "_cloud_restore_tmp/*"
]
api.upload_folder(
folder_path=WORKDIR,
path_in_repo="Server",
repo_id=resolved_repo_id,
repo_type="dataset",
token=token,
ignore_patterns=ignore_list,
commit_message=f"Live sync server files & Playit IP -> Server/ ({time.strftime('%Y-%m-%d %H:%M:%S')})"
)
msg = f"✅ تم مزامنة ورفع جميع ملفات السيرفر مباشرة إلى المجلد Server/ في الـ Dataset [{resolved_repo_id}]!"
log(f"[DATASET SYNC] {msg}")
return True, msg
except Exception as e:
err_msg = f"❌ خطأ أثناء الرفع إلى المجلد Server داخل الـ Dataset [{resolved_repo_id}]: {e}"
log(f"[DATASET SYNC ERROR] {err_msg}")
return False, err_msg
def dataset_sync_worker():
global _last_auto_sync_time
while True:
time.sleep(30)
try:
config = load_hf_config()
if config.get("auto_sync", True) and (config.get("hf_token") or os.environ.get("HF_TOKEN")) and (config.get("hf_dataset_repo") or os.environ.get("HF_DATASET_REPO")):
interval_mins = int(config.get("sync_interval", 10))
if interval_mins < 1: interval_mins = 10
if time.time() - _last_auto_sync_time >= interval_mins * 60:
_last_auto_sync_time = time.time()
if mc_process and mc_process.poll() is None:
log(f"[DATASET SYNC] 🕒 التزامن الدوري التلقائي مع المجلد Server في الـ Dataset (كل {interval_mins} دقيقة)...")
push_to_dataset()
except Exception as e:
log(f"[DATASET SYNC] Worker error: {e}")
threading.Thread(target=dataset_sync_worker, daemon=True).start()
def get_system_resources():
try:
cpu = psutil.cpu_percent(interval=0.1)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
uptime_sec = int(time.time() - start_time)
days = uptime_sec // 86400
hours = (uptime_sec % 86400) // 3600
mins = (uptime_sec % 3600) // 60
uptime_str = f"{days}d {hours}h {mins}m" if days else f"{hours}h {mins}m"
return {
"cpu": round(cpu, 1),
"ram_used": round(mem.used / (1024**3), 1),
"ram_total": round(mem.total / (1024**3), 1),
"ram_pct": round(mem.percent, 1),
"disk_used": round(disk.used / (1024**3), 1),
"disk_total": round(disk.total / (1024**3), 1),
"disk_pct": round(disk.percent, 1),
"uptime": uptime_str,
}
except:
return {"cpu": 0, "ram_used": 0, "ram_total": 4, "ram_pct": 0, "disk_used": 0, "disk_total": 20, "disk_pct": 0, "uptime": "0m"}
def get_online_players():
try:
joined = set()
left = set()
lines = log_output[-300:]
for line in lines:
m = re.search(r"(\S+)\s+joined the game", line)
if m: joined.add(m.group(1))
m = re.search(r"(\S+)\s+left the game", line)
if m: left.add(m.group(1))
return list(joined - left)
except: return []
def send_console_command(cmd, user=None):
if not has_perm(user, "console"):
return "⛔ You don't have `console` permission"
if not cmd or not cmd.strip():
return "⚠️ Enter a command"
if not (mc_process and mc_process.poll() is None):
return "⚠️ Server is not running"
try:
mc_process.stdin.write(cmd.strip() + "\n")
mc_process.stdin.flush()
log(f"[CONSOLE] > {cmd.strip()}")
return f"✅ Command sent: `{cmd.strip()}`"
except Exception as e:
return f"❌ Failed to send command: {e}"
def safe_download(url, dest_path, label, timeout=180):
try:
result = subprocess.run(["wget", "-q", "-O", dest_path, url], capture_output=True, text=True, timeout=timeout)
if result.returncode != 0 or not os.path.exists(dest_path) or os.path.getsize(dest_path) == 0:
log(f"[ERROR] فشل تحميل {label}: {result.stderr.strip()[:200] if result.stderr else 'رابط غير صالح'}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
log(f"[SYSTEM] تم تحميل {label} بنجاح.")
return True
except subprocess.TimeoutExpired:
log(f"[ERROR] انتهت مهلة تحميل {label} (timeout).")
if os.path.exists(dest_path): os.remove(dest_path)
return False
except Exception as e:
log(f"[ERROR] خطأ أثناء تحميل {label}: {e}")
if os.path.exists(dest_path): os.remove(dest_path)
return False
def download_dependencies():
if not os.path.exists(os.path.join(WORKDIR, "jdk")):
jdk_tar = os.path.join(WORKDIR, "jdk.tar.gz")
jdk_url = "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.4%2B7/OpenJDK21U-jre_x64_linux_hotspot_21.0.4_7.tar.gz"
if safe_download(jdk_url, jdk_tar, "Java 21 (JDK)"):
try:
subprocess.run(["tar", "-xzf", jdk_tar, "-C", WORKDIR], check=True, capture_output=True, text=True)
for extracted in os.listdir(WORKDIR):
if extracted.startswith("jdk-21"):
os.rename(os.path.join(WORKDIR, extracted), os.path.join(WORKDIR, "jdk"))
os.remove(jdk_tar)
except Exception as e:
log(f"[ERROR] فشل استخراج JDK: {e}")
if not os.path.exists(os.path.join(WORKDIR, "paper.jar")):
safe_download("https://api.purpurmc.org/v2/purpur/1.21.1/latest/download", os.path.join(WORKDIR, "paper.jar"), "Purpur Server (1.21.1)")
plugins_dir = os.path.join(WORKDIR, "plugins")
os.makedirs(plugins_dir, exist_ok=True)
if not os.path.exists(os.path.join(plugins_dir, "playit.jar")):
safe_download("https://github.com/playit-cloud/playit-minecraft-plugin/releases/latest/download/playit-minecraft-plugin.jar", os.path.join(plugins_dir, "playit.jar"), "Playit Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaVersion.jar")):
safe_download("https://cdn.modrinth.com/data/P1OZGk5p/versions/ruzmiBqe/ViaVersion-5.10.0.jar", os.path.join(plugins_dir, "ViaVersion.jar"), "ViaVersion Plugin")
if not os.path.exists(os.path.join(plugins_dir, "ViaBackwards.jar")):
safe_download("https://cdn.modrinth.com/data/NpvuJQoq/versions/YjpKsm6j/ViaBackwards-5.10.0.jar", os.path.join(plugins_dir, "ViaBackwards.jar"), "ViaBackwards Plugin")
if not os.path.exists(os.path.join(plugins_dir, "Geyser-Spigot.jar")):
safe_download("https://cdn.modrinth.com/data/wKkoqHrH/versions/RHInMdHJ/Geyser-Spigot.jar", os.path.join(plugins_dir, "Geyser-Spigot.jar"), "Geyser-Spigot Plugin")
if not os.path.exists(os.path.join(plugins_dir, "floodgate.jar")):
safe_download("https://download.geysermc.org/v2/projects/floodgate/versions/latest/builds/latest/downloads/spigot", os.path.join(plugins_dir, "floodgate.jar"), "Floodgate Plugin")
eula_p = os.path.join(WORKDIR, "eula.txt")
if not os.path.exists(eula_p):
with open(eula_p, "w") as f: f.write("eula=true\n")
props_p = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_p):
log("[SYSTEM] Configuring server.properties for cracked mode...")
with open(props_p, "w") as f: f.write("online-mode=false\nspawn-protection=0\n")
_playit_detected = False
def detect_playit_ip():
for line in log_output[-500:]:
m = re.search(r'(?:found minecraft java tunnel|playit\.gg[^\w]*)\s*:\s*(\S+)', line, re.I)
if m:
return m.group(1)
return None
def configure_geyser(playit_addr):
geyser_config = os.path.join(WORKDIR, "plugins", "Geyser-Spigot", "config.yml")
os.makedirs(os.path.dirname(geyser_config), exist_ok=True)
hostname = playit_addr.split(":")[0] if ":" in playit_addr else playit_addr
port = int(playit_addr.split(":")[1]) if ":" in playit_addr else 19132
try:
import yaml
if os.path.exists(geyser_config):
with open(geyser_config) as f:
config = yaml.safe_load(f) or {}
else:
config = {}
if "bedrock" not in config:
config["bedrock"] = {}
config["bedrock"]["address"] = "0.0.0.0"
config["bedrock"]["port"] = 19132
config["bedrock"]["public-address"] = hostname
config["bedrock"]["public-port"] = port
config["bedrock"]["clone-remote-port"] = False
if "remote" not in config:
config["remote"] = {}
config["remote"]["address"] = "127.0.0.1"
config["remote"]["port"] = 25565
config["remote"]["auth-type"] = "offline"
with open(geyser_config, "w") as f:
yaml.dump(config, f, default_flow_style=False)
log(f"[GEYSER] ✅ تم ضبط Geyser تلقائياً للـ Bedrock على {hostname}:{port}")
except Exception as e:
log(f"[GEYSER ERROR] فشل ضبط Geyser: {e}")
floodgate_config = os.path.join(WORKDIR, "plugins", "floodgate", "config.yml")
os.makedirs(os.path.dirname(floodgate_config), exist_ok=True)
if not os.path.exists(floodgate_config):
try:
default_floodgate = {
"floodgate": {
"proxy-connection": "true",
"use-proxy-commands": "true",
"send-floodgate-data": "true"
}
}
with open(floodgate_config, "w") as f:
yaml.dump(default_floodgate, f, default_flow_style=False)
log("[FLOODGATE] ✅ تم إنشاء Config تلقائياً")
except Exception as e:
log(f"[FLOODGATE ERROR] {e}")
def monitor_playit_and_configure():
global _playit_detected
time.sleep(15)
while True:
time.sleep(5)
if _playit_detected:
break
if mc_process and mc_process.poll() is None:
addr = detect_playit_ip()
if addr:
configure_geyser(addr)
_playit_detected = True
log(f"[SYSTEM] ✅ تم اكتشاف Playit IP: {addr} وتحديث Geyser تلقائياً للـ Bedrock")
push_to_dataset()
break
def start_server_backend():
global mc_process, _playit_detected
_playit_detected = False
if mc_process and mc_process.poll() is None:
log("[SYSTEM] Server is already running!")
return
# 1. تنزيل وسحب محتويات المجلد Server/ من الـ Dataset
init_dataset_and_pull()
# 2. تنزيل التبعيات المطلوبة في حال عدم وجودها
download_dependencies()
# 3. مزامنة فورية لتأكيد وجود المجلد Server/ والملفات داخل الـ Dataset قبل بدء ماين كرافت
push_to_dataset()
log("[SYSTEM] Starting Minecraft Server (Purpur 1.21.1)...")
# 4. بدء مراقب Playit لاكتشاف IP السيرفر تلقائياً
threading.Thread(target=monitor_playit_and_configure, daemon=True).start()
java_path = os.path.join(WORKDIR, "jdk/bin/java")
mc_process = subprocess.Popen(
[java_path, "-Xms2G", "-Xmx4G", "-jar", "paper.jar", "nogui"],
cwd=WORKDIR, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)
for line in iter(mc_process.stdout.readline, ''):
if line: log(f"[MINECRAFT] {line.strip()}")
mc_process.stdout.close()
def control_server(action, user):
perm = action.lower()
if not has_perm(user, perm):
return f"⛔ You don't have `{perm}` permission"
global mc_process
if action == "START":
if mc_process and mc_process.poll() is None:
return "❌ السيرفر يعمل بالفعل الآن!"
threading.Thread(target=start_server_backend, daemon=True).start()
return "⚡ جاري تشغيل خادم ماين كرافت الآن (مع تنزيل ورفع الملفات مباشرة من الـ Dataset)..."
elif action == "STOP":
if mc_process and mc_process.poll() is None:
try:
mc_process.stdin.write("/stop\n")
mc_process.stdin.flush()
log("[SYSTEM] Sent /stop command — waiting for graceful shutdown...")
except: pass
deadline = time.time() + 8
while time.time() < deadline:
if mc_process.poll() is not None: break
time.sleep(0.5)
if mc_process and mc_process.poll() is None:
mc_process.terminate()
time.sleep(1)
mc_process = None
log("[SYSTEM] Server stopped by user.")
# مزامنة محتويات السيرفر فوراً إلى مجلد Server داخل الـ Dataset عند الإيقاف
log("[SYSTEM] Syncing directly to Server/ inside Dataset upon stop...")
threading.Thread(target=push_to_dataset, daemon=True).start()
return "🛑 Server stopped gracefully — Synced directly to Dataset/Server."
return "⚠️ Server is already stopped."
elif action == "RESTART":
if mc_process and mc_process.poll() is None:
mc_process.terminate()
mc_process = None
log("[SYSTEM] Restarting server...")
threading.Thread(target=start_server_backend, daemon=True).start()
return "🔄 جاري إعادة تشغيل السيرفر..."
async def get_logs():
return "\n".join(log_output)
async def get_server_status():
status = "🟢 **Running**" if (mc_process and mc_process.poll() is None) else "🔴 **Stopped**"
return f"## {status}\n\n**Purpur 1.21.1** — Java 21 — 2-4GB RAM"
BINARY_EXTENSIONS = {
".jar", ".zip", ".gz", ".tar", ".rar", ".7z", ".class",
".dat", ".mca", ".mcr", ".png", ".jpg", ".jpeg", ".gif",
".ico", ".exe", ".bin", ".db", ".so", ".dll"
}
MAX_EDITABLE_SIZE = 200 * 1024
PAGE_SIZE = 200
def get_safe_path(rel_path):
if rel_path is None: rel_path = ""
abs_path = os.path.abspath(os.path.join(WORKDIR, rel_path))
if abs_path.startswith(os.path.abspath(WORKDIR)): return abs_path
return WORKDIR
async def scan_directory(current_rel):
target_dir = get_safe_path(current_rel)
folders = []
text_files = []
binary_count = 0
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target_dir))), timeout=5.0)
for item in entries:
full_path = os.path.join(target_dir, item)
try:
if await asyncio.to_thread(os.path.isdir, full_path): folders.append(item)
else:
ext = os.path.splitext(item)[1].lower()
if ext in BINARY_EXTENSIONS: binary_count += 1
else: text_files.append(item)
except: text_files.append(item)
except Exception as e:
log(f"[FILE MGR ERROR] {e}")
folders.sort()
text_files.sort()
folder_choices = [".. (المجلد الأعلى)"] + folders if current_rel else folders
file_choices = text_files if text_files else ["لا توجد ملفات نصية في هذا المسار"]
path_display = f"📁 المسار الحالي المفتوح: /root/{current_rel}" if current_rel else "📁 المسار الحالي المفتوح: /root (المجلد الرئيسي)"
if binary_count > 0: path_display += f" \n📎 {binary_count} ملف ثنائي مخفي (jar/zip/png...)"
return path_display, gr.Dropdown(choices=folder_choices, value=None), gr.Dropdown(choices=file_choices, value=None)
async def enter_folder(selected_folder, current_rel):
if not selected_folder: return current_rel, *await scan_directory(current_rel)
if selected_folder == ".. (المجلد الأعلى)":
if not current_rel: return "", *await scan_directory("")
parent_rel = "/".join(current_rel.strip("/").split("/")[:-1])
return parent_rel, *await scan_directory(parent_rel)
else:
new_rel = os.path.join(current_rel, selected_folder).strip("/")
return new_rel, *await scan_directory(new_rel)
def is_probably_binary(file_path, chunk_size=1024):
try:
with open(file_path, "rb") as f: return b"\x00" in f.read(chunk_size)
except: return True
async def load_file_content(selected_file, current_rel):
empty, title_reset = gr.update(value=""), gr.update(value="### 📝 تعديل الملف: —")
if not selected_file or "لا توجد ملفات" in selected_file:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value="👈 اختر ملفاً نصياً من القائمة ليظهر صندوق التعديل هنا."), "", 0, gr.update(visible=False)
file_path = os.path.join(get_safe_path(current_rel), selected_file)
if not os.path.isfile(file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"⚠️ [{selected_file}] مجلد وليس ملفاً."), "", 0, gr.update(visible=False)
ext = os.path.splitext(selected_file)[1].lower()
if ext in BINARY_EXTENSIONS:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] ثنائي (Binary) ولا يمكن تحريره نصياً."), "", 0, gr.update(visible=False)
try: size = await asyncio.to_thread(os.path.getsize, file_path)
except Exception as e: return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"خطأ أثناء قراءة الحجم: {e}"), "", 0, gr.update(visible=False)
if size > MAX_EDITABLE_SIZE:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] حجمه كبير جداً ({size/1024/1024:.2f} MB)."), "", 0, gr.update(visible=False)
if await asyncio.to_thread(is_probably_binary, file_path):
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"🚫 الملف [{selected_file}] يحتوي على بيانات ثنائية."), "", 0, gr.update(visible=False)
try:
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total_lines, display_lines = len(lines), lines[:PAGE_SIZE]
has_more = total_lines > PAGE_SIZE
info = f"### 📝 تعديل الملف: `{selected_file}`" + (f" — (أول {PAGE_SIZE} من {total_lines} سطر)" if has_more else "")
return gr.update(value="".join(display_lines)), gr.update(value=info), gr.update(visible=True), gr.update(visible=False), file_path, PAGE_SIZE, gr.update(visible=has_more)
except Exception as e:
return empty, title_reset, gr.update(visible=False), gr.update(visible=True, value=f"خطأ أثناء القراءة: {e}"), "", 0, gr.update(visible=False)
async def load_more_content(file_path, load_offset):
if not file_path or not os.path.isfile(file_path): return gr.update(), 0, gr.update(visible=False)
content = await asyncio.to_thread(lambda: open(file_path, "r", encoding="utf-8", errors="ignore").read())
lines = content.splitlines(keepends=True)
total, new_offset = len(lines), min(load_offset + PAGE_SIZE, len(lines))
return gr.update(value="".join(lines[:new_offset])), new_offset, gr.update(visible=new_offset < total)
async def save_file_content(selected_file, content, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission"
if not selected_file or "لا توجد ملفات" in selected_file: return "❌ لم يتم تحديد ملف صالح لحفظه."
file_path = os.path.join(get_safe_path(current_rel), selected_file)
try:
await asyncio.to_thread(lambda: open(file_path, "w", encoding="utf-8").write(content))
threading.Thread(target=push_to_dataset, daemon=True).start()
return f"✅ تم حفظ الملف [{selected_file}] ومزامنتُه مع الـ Dataset!"
except Exception as e: return f"❌ فشل الحفظ: {e}"
async def create_item(name, item_type, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not name: return "⚠️ الرجاء إدخال اسم المجلد أو الملف أولاً.", *await scan_directory(current_rel)
target_path = os.path.join(get_safe_path(current_rel), name)
try:
if "Folder" in item_type or "مجلد" in item_type:
await asyncio.to_thread(os.makedirs, target_path, exist_ok=True)
msg = f"📁 Created folder [{name}]"
else:
await asyncio.to_thread(lambda: open(target_path, "w", encoding="utf-8").write(""))
msg = f"📄 Created file [{name}]"
threading.Thread(target=push_to_dataset, daemon=True).start()
return msg, *await scan_directory(current_rel)
except Exception as e: return f"❌ فشل الإنشاء: {e}", *await scan_directory(current_rel)
async def rename_item(old_name, new_name, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not old_name or not new_name: return "⚠️ حدد العنصر واكتب الاسم الجديد.", *await scan_directory(current_rel)
try:
await asyncio.to_thread(os.rename, os.path.join(get_safe_path(current_rel), old_name), os.path.join(get_safe_path(current_rel), new_name))
threading.Thread(target=push_to_dataset, daemon=True).start()
return f"✏️ تم إعادة التسمية بنجاح إلى [{new_name}].", *await scan_directory(current_rel)
except Exception as e: return f"❌ خطأ أثناء التسمية: {e}", *await scan_directory(current_rel)
async def delete_item(item_name, is_folder, current_rel, user):
if not has_perm(user, "files_delete"): return "⛔ You don't have `files_delete` permission", *await scan_directory(current_rel)
if not item_name or "لا توجد ملفات" in item_name: return "⚠️ لم يتم تحديد عنصر لحذفه.", *await scan_directory(current_rel)
target_p = os.path.join(get_safe_path(current_rel), item_name)
try:
if is_folder:
await asyncio.to_thread(shutil.rmtree, target_p)
msg = f"🗑️ تم حذف المجلد ومحتوياته [{item_name}] بالكامل."
else:
await asyncio.to_thread(os.remove, target_p)
msg = f"🗑️ تم حذف الملف [{item_name}]."
threading.Thread(target=push_to_dataset, daemon=True).start()
return msg, *await scan_directory(current_rel)
except Exception as e: return f"❌ فشل الحذف: {e}", *await scan_directory(current_rel)
async def upload_to_current_dir(file_objs, current_rel, user):
if not has_perm(user, "files_write"): return "⛔ You don't have `files_write` permission", *await scan_directory(current_rel)
if not file_objs: return "❌ لم يتم اختيار ملفات للرفع.", *await scan_directory(current_rel)
target_dir, uploaded_list = get_safe_path(current_rel), []
if not isinstance(file_objs, list): file_objs = [file_objs]
for f_obj in file_objs:
orig_name = getattr(f_obj, 'orig_name', os.path.basename(f_obj.name))
await asyncio.to_thread(shutil.copy, f_obj.name, os.path.join(target_dir, orig_name))
uploaded_list.append(orig_name)
threading.Thread(target=push_to_dataset, daemon=True).start()
return f"✅ تم رفع {len(uploaded_list)} ملفات ومزامنتُها بنجاح!", *await scan_directory(current_rel)
# السيرفر سيعمل فقط عند ضغطك يدوياً على زر "▶ Start Server" بعد تجهيز الـ Dataset
def on_space_boot():
log("[SYSTEM] Space booted. Initializing Hugging Face Dataset connection (`Server/` directory)...")
init_dataset_and_pull()
log("[SYSTEM] Ready! Click '▶ Start Server' from the Console tab to boot Minecraft manually.")
threading.Thread(target=on_space_boot, daemon=True).start()
# ─── واجهة PTERODACTYL UI DASHBOARD ───
DASHBOARD_HTML = r"""
⚡ PTERODACTYL MC
Purpur 1.21.1
Server Stopped
Management
Console & Overview
Real-time resource utilization and terminal logs
Memory Usage🧠
0.0 / 4.0 GB
0% allocated
CPU Utilization⚡
0%
Purpur Main Thread
Online Players🎮
0
No players connected
System Uptime⏱️
0m
Continuous execution
~/server/logs/latest.log● LIVE STREAM
[SYSTEM] Panel initialized. Ready to launch Minecraft Server...
File Explorer
📁 /root
Directory Contents
Loading files...
Select a file to edit
➕ Create Item
🗑️ Delete / Rename
☁️ Dataset Live Sync (Server/ Folder)
يتم إنشاء الـ Dataset تلقائياً عند أول تشغيل، وتنزيل ورفع كل محتويات ومجلدات السيرفر مباشرة داخل المجلد Server/
☁️ Hugging Face Dataset Connection
HF Token (Write access)
Dataset Repository ID
Auto Direct Sync (Server/)
Sync Interval (Mins)
🔄 المزامنة الفورية مع الـ Dataset
⚡ التزامن المباشر والإنشاء التلقائي:
1. عند أول تشغيل، يتحقق النظام من وجود الـ Dataset على حسابك، وإذا لم يكن موجوداً يقوم بإنشائه تلقائياً كأرشيف مغلق (Private).
2. يتم تنزيل كل الملفات الموجودة في [repo]/Server وتشغيل الخادم عليها مباشرة.
3. أي ملف أو مجلد جديد يُنشئه السيرفر يتم دفعه ومزامنتُه فوراً داخل Server/ ليكون المستودع بمثابة وحدة تخزين سحابية مستمرة للسيرفر.
Server Configuration
Modify core server.properties parameters directly
🎮 Gameplay Mechanics
Difficulty
Default Gamemode
Max Players
View Distance
🔒 Network & Security
Online Mode (Cracked)
Player vs Player (PvP)
Spawn Monsters
Enable Whitelist
Access Control (RBAC)
Manage panel collaborators and granular permissions
➕ Assign Collaborator
📋 Authorized Users List
No users configured.
"""
DASHBOARD_JS = r"""
function showToast(msg, isErr=false) {
var box = document.getElementById('toast-box');
if(!box) return;
var t = document.createElement('div');
t.className = 'toast' + (isErr ? ' err' : '');
t.innerHTML = (isErr ? '❌ ' : '✅ ') + msg;
box.appendChild(t);
setTimeout(function(){ t.remove() }, 3500);
}
function switchTab(name, btn) {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
if(btn) btn.classList.add('active');
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
var pane = document.getElementById('tab-' + name);
if(pane) pane.classList.add('active');
if(name === 'files') bridgeClick('gb-refresh-dir');
if(name === 'cloud') bridgeClick('gb-load-cloud-config');
if(name === 'settings') bridgeClick('gb-load-settings');
if(name === 'users') bridgeClick('gb-refresh-users');
}
function bridgeClick(id) {
var el = document.getElementById(id);
if(el){
var btn = el.querySelector('button') || el;
btn.click();
}
}
function setGradioInput(id, val) {
var root = document.getElementById(id);
if(!root) return;
var inp = root.querySelector('textarea, input');
if(!inp) return;
var proto = inp instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(inp, val);
inp.dispatchEvent(new Event('input', { bubbles: true }));
}
function getGradioText(id) {
var root = document.getElementById(id);
if(!root) return '';
var ta = root.querySelector('textarea, input');
return ta ? ta.value : (root.innerText || '');
}
var _selectedFile = '';
var _selectedFolder = '';
function selectFolderAction(fName) {
_selectedFolder = fName;
setGradioInput('gv-folder', fName);
bridgeClick('gb-enter-folder');
}
function selectFileAction(fName) {
_selectedFile = fName;
document.getElementById('editor-filename').textContent = 'Editing: ' + fName;
var ren = document.getElementById('rename-new-inp');
if(ren) ren.value = fName;
setGradioInput('gv-file', fName);
bridgeClick('gb-select-file');
}
function saveCurrentFile() {
if(!_selectedFile) return showToast('No file selected!', true);
var val = document.getElementById('editor-area').value;
setGradioInput('gv-content', val);
bridgeClick('gb-save-file');
}
function createItemAction() {
var name = document.getElementById('create-name-inp').value;
var type = document.getElementById('create-type-sel').value;
if(!name) return showToast('Enter name first!', true);
setGradioInput('gv-name', name);
setGradioInput('gv-type', type);
bridgeClick('gb-create');
}
function renameItemAction() {
var newN = document.getElementById('rename-new-inp').value;
if(!_selectedFile || !newN) return showToast('Select a file and enter new name!', true);
setGradioInput('gv-old', _selectedFile);
setGradioInput('gv-name', newN);
bridgeClick('gb-rename');
}
function deleteItemAction() {
if(!_selectedFile) return showToast('Select a file to delete!', true);
setGradioInput('gv-old', _selectedFile);
bridgeClick('gb-delete');
_selectedFile = '';
}
function triggerUpload(inp) {
if(!inp.files.length) return;
var dt = new DataTransfer();
for(var i=0; i= 0;
var pill = document.getElementById('nav-status');
if(pill) {
pill.className = 'status-pill ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-dot').className = 'status-dot ' + (isRun ? 'running' : 'stopped');
document.getElementById('nav-status-text').textContent = isRun ? 'Server Online' : 'Server Stopped';
}
var con = getGradioText('g-console');
if(con && con !== _lastConsole) {
_lastConsole = con;
var term = document.getElementById('term-logs');
if(term) { term.textContent = con; term.scrollTop = term.scrollHeight; }
}
var resTxt = getGradioText('g-resources');
if(resTxt){
try {
var r = JSON.parse(resTxt);
document.getElementById('stat-ram').textContent = r.ram_used + ' / ' + r.ram_total + ' GB';
document.getElementById('stat-ram-pct').textContent = r.ram_pct + '%';
document.getElementById('bar-ram').style.width = r.ram_pct + '%';
document.getElementById('stat-cpu').textContent = r.cpu + '%';
document.getElementById('bar-cpu').style.width = Math.min(r.cpu, 100) + '%';
document.getElementById('stat-uptime').textContent = r.uptime;
} catch(e){}
}
var pl = getGradioText('g-players');
if(pl && pl !== 'No players online') {
var arr = pl.split(',').filter(Boolean);
document.getElementById('stat-players').textContent = arr.length;
document.getElementById('stat-player-names').textContent = pl;
} else {
document.getElementById('stat-players').textContent = '0';
document.getElementById('stat-player-names').textContent = 'No players connected';
}
var op = getGradioText('g-op');
if(op && op !== _lastOp) {
_lastOp = op;
showToast(op, op.indexOf('❌') >= 0 || op.indexOf('⛔') >= 0);
}
var fld = getGradioText('g-folders');
var fil = getGradioText('g-files');
if(fld || fil) {
try {
var fldArr = fld ? JSON.parse(fld) : [];
var filArr = fil ? JSON.parse(fil) : [];
var html = '';
fldArr.forEach(function(f){ html += '
📁 '+f+'
'; });
filArr.forEach(function(f){ html += '
📄 '+f+'
'; });
var box = document.getElementById('file-list-box');
if(box) box.innerHTML = html || '
Empty directory
';
} catch(e){}
}
var edContent = getGradioText('g-editor');
if(edContent && document.activeElement !== document.getElementById('editor-area')) {
document.getElementById('editor-area').value = edContent;
}
var pDisplay = getGradioText('g-path');
if(pDisplay) document.getElementById('path-display').textContent = pDisplay;
var usrList = getGradioText('g-users');
if(usrList) document.getElementById('users-list-area').innerHTML = usrList.replace(/\n/g, ' ');
var cCfg = getGradioText('g-cloud-config');
if(cCfg && document.activeElement !== document.getElementById('cloud-token') && document.activeElement !== document.getElementById('cloud-repo')) {
try {
var cfg = JSON.parse(cCfg);
if(cfg.hf_token) document.getElementById('cloud-token').value = cfg.hf_token;
if(cfg.hf_dataset_repo) document.getElementById('cloud-repo').value = cfg.hf_dataset_repo;
document.getElementById('cloud-auto-sync').checked = cfg.auto_sync !== false;
if(cfg.sync_interval) document.getElementById('cloud-interval').value = cfg.sync_interval;
} catch(e){}
}
bridgeClick('gb-refresh-con');
if(_pollCount % 2 === 0) bridgeClick('gb-periodic-status');
if(_pollCount % 3 === 0) bridgeClick('gb-refresh-res');
if(_pollCount % 4 === 0) bridgeClick('gb-refresh-players');
}
setTimeout(function(){
setInterval(pollSystem, 1500);
}, 500);
"""
with gr.Blocks(title="Pterodactyl MC Panel", css="#bridge-wrap{display:none!important}", head="") as demo:
demo.load(fn=activate_gpu, inputs=None, outputs=gr.Textbox(visible=False))
current_path_state, file_path_state, load_offset_state, current_user = gr.State(""), gr.State(""), gr.State(0), gr.State(None)
gr.HTML(value=DASHBOARD_HTML)
with gr.Column(elem_id="bridge-wrap"):
_console = gr.Textbox(elem_id="g-console")
_status = gr.Textbox(elem_id="g-status")
_path = gr.Textbox(elem_id="g-path")
_folders = gr.Textbox(elem_id="g-folders")
_files = gr.Textbox(elem_id="g-files")
_op = gr.Textbox(elem_id="g-op")
_editor = gr.Textbox(elem_id="g-editor")
_editor_title = gr.Textbox(elem_id="g-editor-title")
_editor_fpath = gr.Textbox(elem_id="g-editor-fpath")
_editor_offset = gr.Textbox(elem_id="g-editor-offset")
_editor_has_more = gr.Textbox(elem_id="g-editor-has-more")
_users = gr.Textbox(elem_id="g-users")
_resources = gr.Textbox(elem_id="g-resources")
_players = gr.Textbox(elem_id="g-players")
_cmd_result = gr.Textbox(elem_id="g-cmd-result")
_download_data = gr.Textbox(elem_id="g-download")
_settings = gr.Textbox(elem_id="g-settings")
_cloud_config_json = gr.Textbox(elem_id="g-cloud-config")
_b_start = gr.Button(elem_id="gb-start")
_b_stop = gr.Button(elem_id="gb-stop")
_b_restart = gr.Button(elem_id="gb-restart")
_b_refresh_con = gr.Button(elem_id="gb-refresh-con")
_b_refresh_dir = gr.Button(elem_id="gb-refresh-dir")
_b_enter_folder = gr.Button(elem_id="gb-enter-folder")
_b_select_file = gr.Button(elem_id="gb-select-file")
_b_save_file = gr.Button(elem_id="gb-save-file")
_b_create = gr.Button(elem_id="gb-create")
_b_rename = gr.Button(elem_id="gb-rename")
_b_delete = gr.Button(elem_id="gb-delete")
_b_upload = gr.Button(elem_id="gb-upload")
_b_add_user = gr.Button(elem_id="gb-add-user")
_b_refresh_users = gr.Button(elem_id="gb-refresh-users")
_b_periodic_status = gr.Button(elem_id="gb-periodic-status")
_b_send_cmd = gr.Button(elem_id="gb-send-cmd")
_b_refresh_res = gr.Button(elem_id="gb-refresh-res")
_b_refresh_players = gr.Button(elem_id="gb-refresh-players")
_b_download_file = gr.Button(elem_id="gb-download-file")
_b_load_settings = gr.Button(elem_id="gb-load-settings")
_b_save_settings = gr.Button(elem_id="gb-save-settings")
_b_cloud_restore = gr.Button(elem_id="gb-cloud-restore")
_b_cloud_backup = gr.Button(elem_id="gb-cloud-backup")
_b_load_cloud_config = gr.Button(elem_id="gb-load-cloud-config")
_b_save_cloud_config = gr.Button(elem_id="gb-save-cloud-config")
_v_folder = gr.Textbox(elem_id="gv-folder")
_v_file = gr.Textbox(elem_id="gv-file")
_v_content = gr.Textbox(elem_id="gv-content")
_v_name = gr.Textbox(elem_id="gv-name")
_v_type = gr.Textbox(elem_id="gv-type", value="File")
_v_old = gr.Textbox(elem_id="gv-old")
_v_del_folder = gr.Checkbox(elem_id="gv-del-folder", value=False)
_v_uname = gr.Textbox(elem_id="gv-uname")
_v_role = gr.Textbox(elem_id="gv-role", value="editor")
_v_upload = gr.File(elem_id="gv-upload", file_count="multiple")
_v_cmd = gr.Textbox(elem_id="gv-cmd")
_v_download_path = gr.Textbox(elem_id="gv-download-path")
_v_settings_data = gr.Textbox(elem_id="gv-settings-data")
_v_cloud_config_data = gr.Textbox(elem_id="gv-cloud-config-data")
async def w_scan_directory(rel):
target = get_safe_path(rel)
folders, files = [], []
try:
entries = await asyncio.wait_for(asyncio.to_thread(lambda: list(os.listdir(target))), timeout=5.0)
for item in entries:
fp = os.path.join(target, item)
try:
if await asyncio.to_thread(os.path.isdir, fp): folders.append(item)
elif os.path.splitext(item)[1].lower() not in BINARY_EXTENSIONS: files.append(item)
except: files.append(item)
except: pass
folders.sort(); files.sort()
return f"📁 /root/{rel}" if rel else "📁 /root", json.dumps(folders), json.dumps(files)
async def w_enter_folder(folder, rel):
if rel is None: rel = ""
if not folder: return rel, *await w_scan_directory(rel)
if folder == ".. (المجلد الأعلى)":
parent = "/".join(rel.strip("/").split("/")[:-1]) if rel else ""
return parent, *await w_scan_directory(parent)
new_rel = os.path.join(rel, folder).strip("/")
return new_rel, *await w_scan_directory(new_rel)
async def w_load_file(file, rel):
r = await load_file_content(file, rel)
content = r[0].get("value", "") if isinstance(r[0], dict) else r[0]
title = r[1].get("value", "") if isinstance(r[1], dict) else r[1]
has_more = r[6].get("visible", False) if isinstance(r[6], dict) else False
return content, title, str(r[4]), str(r[5]), str(has_more)
async def w_create(name, typ, rel, user):
r = await create_item(name, typ, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_rename(old, new, rel, user):
r = await rename_item(old, new, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_delete(name, is_folder, rel, user):
r = await delete_item(name, is_folder, rel, user)
return r[0], *await w_scan_directory(rel)
async def w_upload(files, rel, user):
r = await upload_to_current_dir(files, rel, user)
return r[0], *await w_scan_directory(rel)
def w_list_users():
users = load_users()
if not users: return "No users configured yet."
lines = []
for u, r in sorted(users.items()):
icon = {"admin": "🛡️", "editor": "✏️", "viewer": "👁️"}.get(r, "❓")
lines.append(f"{icon} {u} — {r.upper()}")
return " ".join(lines)
def w_add_user(name, role, user):
if not has_perm(user, "users_manage"): return "⛔ Only admins can manage users"
if not name: return "⚠️ Enter a username"
users = load_users(); users[name.strip()] = role; save_users(users)
return f"✅ Added {name.strip()} as {role}"
def w_send_cmd(cmd, user): return send_console_command(cmd, user)
def w_get_resources(): return json.dumps(get_system_resources())
def w_get_players(): return ", ".join(get_online_players()) or "No players online"
def w_download_file(filepath, rel, user):
if not has_perm(user, "files_read"): return "ERROR:Permission denied"
if not filepath: return ""
target = get_safe_path(os.path.join(rel, filepath) if rel else filepath)
if not os.path.isfile(target): return ""
try:
with open(target, "rb") as f:
data = base64.b64encode(f.read()).decode()
return f"{filepath}|{data}"
except: return ""
def w_load_settings():
props_path = os.path.join(WORKDIR, "server.properties")
if not os.path.exists(props_path): return "{}"
try:
props = {}
with open(props_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
k, v = line.split("=", 1); props[k.strip()] = v.strip()
return json.dumps(props)
except: return "{}"
def w_save_settings(data_str, user):
if not has_perm(user, "files_write"): return "⛔ Permission denied"
props_path = os.path.join(WORKDIR, "server.properties")
try:
props = json.loads(data_str)
with open(props_path, "w") as f:
for k, v in sorted(props.items()): f.write(f"{k}={v}\n")
return "✅ Settings saved successfully!"
except Exception as e: return f"❌ Error saving: {e}"
def w_load_cloud_config():
return json.dumps(load_hf_config())
def w_save_cloud_config(data_str, user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to manage cloud sync"
try:
cfg = json.loads(data_str)
success, msg = save_hf_config(cfg)
return msg
except Exception as e: return f"❌ Error saving cloud config: {e}"
def w_cloud_restore(user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to perform cloud restore"
success, msg = init_dataset_and_pull()
return msg
def w_cloud_backup(user):
if not has_perm(user, "cloud_manage"): return "⛔ You don't have permission to perform cloud backup"
success, msg = push_to_dataset()
return msg
_b_start.click(fn=control_server, inputs=[gr.State("START"), current_user], outputs=_status)
_b_stop.click(fn=control_server, inputs=[gr.State("STOP"), current_user], outputs=_status)
_b_restart.click(fn=control_server, inputs=[gr.State("RESTART"), current_user], outputs=_status)
_b_refresh_con.click(fn=get_logs, outputs=_console)
_b_refresh_dir.click(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
_b_enter_folder.click(fn=w_enter_folder, inputs=[_v_folder, current_path_state], outputs=[current_path_state, _path, _folders, _files])
_b_select_file.click(fn=w_load_file, inputs=[_v_file, current_path_state], outputs=[_editor, _editor_title, _editor_fpath, _editor_offset, _editor_has_more])
_b_save_file.click(fn=save_file_content, inputs=[_v_file, _v_content, current_path_state, current_user], outputs=_op)
_b_create.click(fn=w_create, inputs=[_v_name, _v_type, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_rename.click(fn=w_rename, inputs=[_v_old, _v_name, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_delete.click(fn=w_delete, inputs=[_v_old, _v_del_folder, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_upload.click(fn=w_upload, inputs=[_v_upload, current_path_state, current_user], outputs=[_op, _path, _folders, _files])
_b_add_user.click(fn=w_add_user, inputs=[_v_uname, _v_role, current_user], outputs=_op)
_b_refresh_users.click(fn=w_list_users, outputs=_users)
_b_periodic_status.click(fn=get_server_status, outputs=_status)
_b_send_cmd.click(fn=w_send_cmd, inputs=[_v_cmd, current_user], outputs=_cmd_result)
_b_refresh_res.click(fn=w_get_resources, outputs=_resources)
_b_refresh_players.click(fn=w_get_players, outputs=_players)
_b_download_file.click(fn=w_download_file, inputs=[_v_download_path, current_path_state, current_user], outputs=_download_data)
_b_load_settings.click(fn=w_load_settings, outputs=_settings)
_b_save_settings.click(fn=w_save_settings, inputs=[_v_settings_data, current_user], outputs=_cmd_result)
_b_cloud_restore.click(fn=w_cloud_restore, inputs=[current_user], outputs=_op)
_b_cloud_backup.click(fn=w_cloud_backup, inputs=[current_user], outputs=_op)
_b_load_cloud_config.click(fn=w_load_cloud_config, outputs=_cloud_config_json)
_b_save_cloud_config.click(fn=w_save_cloud_config, inputs=[_v_cloud_config_data, current_user], outputs=_op)
demo.load(fn=get_logs, outputs=_console)
demo.load(fn=get_server_status, outputs=_status)
demo.load(fn=w_scan_directory, inputs=current_path_state, outputs=[_path, _folders, _files])
demo.load(fn=w_list_users, outputs=_users)
demo.load(fn=w_load_cloud_config, outputs=_cloud_config_json)
demo.queue(default_concurrency_limit=3)
log("[SYSTEM] Pterodactyl MC Panel initialized with Live Dataset Sync.")
demo.launch()