Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| sync-client.py — Push config changes to your MC server's HuggingFace dataset | |
| from your local machine. | |
| The server's background watcher will detect the changes and apply them | |
| within 60 seconds. | |
| Usage: | |
| # Push a single config file | |
| python sync-client.py push server.properties | |
| # Push multiple files | |
| python sync-client.py push server.properties ops.json | |
| # Push an entire directory (e.g. plugin configs) | |
| python sync-client.py push plugins/ | |
| # Pull all server files to local directory | |
| python sync-client.py pull [output_dir] | |
| # List what's in the dataset | |
| python sync-client.py list | |
| # Watch for changes locally and auto-push | |
| python sync-client.py watch [directory] | |
| Environment variables: | |
| HF_DATASET_ID — Your HuggingFace dataset ID | |
| HF_TOKEN — Your HuggingFace write token | |
| """ | |
| import os | |
| import sys | |
| import shutil | |
| import tempfile | |
| import subprocess | |
| import time | |
| import json | |
| from pathlib import Path | |
| DATASET_ID = os.environ.get("HF_DATASET_ID", "applepie69/MCP-Server") | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| BRANCH = os.environ.get("SYNC_BRANCH", "main") | |
| REPO_URL = f"https://x-access-token:{HF_TOKEN}@huggingface.co/datasets/{DATASET_ID}" | |
| # The files that are considered "config" and safe to push | |
| CONFIG_FILES = { | |
| "server.properties", "ops.json", "whitelist.json", | |
| "banned-players.json", "banned-ips.json", "bukkit.yml", | |
| "spigot.yml", "paper-global.yml", "paper-world-defaults.yml", | |
| "commands.yml", "help.yml", | |
| } | |
| CONFIG_DIRS = {"plugins"} | |
| def run(cmd, **kwargs): | |
| """Run a command and return the result.""" | |
| return subprocess.run(cmd, shell=True, capture_output=True, text=True, **kwargs) | |
| def clone_repo(dest): | |
| """Clone the dataset repo to dest directory.""" | |
| if os.path.exists(dest): | |
| shutil.rmtree(dest) | |
| os.makedirs(dest, exist_ok=True) | |
| result = run(f'git clone --depth 1 --branch {BRANCH} "{REPO_URL}" "{dest}"') | |
| if result.returncode != 0: | |
| # Try without branch | |
| result = run(f'git clone --depth 1 "{REPO_URL}" "{dest}"') | |
| if result.returncode != 0: | |
| print(f"ERROR: Failed to clone dataset. Check HF_DATASET_ID and HF_TOKEN.") | |
| print(f" stderr: {result.stderr}") | |
| sys.exit(1) | |
| # Configure git | |
| run(f'cd "{dest}" && git config user.email "mc-sync@local" && git config user.name "MC Sync Client"') | |
| def push_to_repo(repo_dir, message="Config update from local"): | |
| """Commit and push changes in repo_dir.""" | |
| run(f'cd "{repo_dir}" && git add -A') | |
| result = run(f'cd "{repo_dir}" && git diff --cached --quiet') | |
| if result.returncode == 0: | |
| print("No changes to push.") | |
| return | |
| run(f'cd "{repo_dir}" && git commit -m "{message}"') | |
| result = run(f'cd "{repo_dir}" && git push origin HEAD:{BRANCH}') | |
| if result.returncode != 0: | |
| print(f"ERROR: Push failed: {result.stderr}") | |
| sys.exit(1) | |
| print(f"Pushed: {message}") | |
| def cmd_push(files): | |
| """Push local files to the dataset.""" | |
| if not DATASET_ID or not HF_TOKEN: | |
| print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") | |
| sys.exit(1) | |
| with tempfile.TemporaryDirectory() as tmp: | |
| print(f"Cloning dataset {DATASET_ID}...") | |
| clone_repo(tmp) | |
| for f in files: | |
| src = Path(f) | |
| if not src.exists(): | |
| print(f"WARNING: {f} not found, skipping.") | |
| continue | |
| dst = Path(tmp) / f | |
| if src.is_dir(): | |
| # Copy directory contents | |
| if dst.exists(): | |
| shutil.rmtree(dst) | |
| shutil.copytree(src, dst) | |
| print(f" Staged directory: {f}/") | |
| else: | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, dst) | |
| print(f" Staged file: {f}") | |
| push_to_repo(tmp, f"Config push — {', '.join(files)}") | |
| print("Done! Server will pick up changes within 60 seconds.") | |
| def cmd_pull(output_dir=None): | |
| """Pull all files from the dataset to a local directory.""" | |
| if not DATASET_ID or not HF_TOKEN: | |
| print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") | |
| sys.exit(1) | |
| out = Path(output_dir or "./mc-server-files") | |
| out.mkdir(parents=True, exist_ok=True) | |
| with tempfile.TemporaryDirectory() as tmp: | |
| print(f"Cloning dataset {DATASET_ID}...") | |
| clone_repo(tmp) | |
| # Copy everything except .git | |
| for item in Path(tmp).iterdir(): | |
| if item.name == ".git": | |
| continue | |
| dst = out / item.name | |
| if item.is_dir(): | |
| if dst.exists(): | |
| shutil.rmtree(dst) | |
| shutil.copytree(item, dst) | |
| else: | |
| shutil.copy2(item, dst) | |
| print(f"Pulled all files to {out}/") | |
| def cmd_list(): | |
| """List files in the dataset.""" | |
| if not DATASET_ID or not HF_TOKEN: | |
| print("ERROR: Set HF_DATASET_ID and HF_TOKEN environment variables.") | |
| sys.exit(1) | |
| with tempfile.TemporaryDirectory() as tmp: | |
| clone_repo(tmp) | |
| result = run(f'cd "{tmp}" && git ls-files') | |
| if result.stdout.strip(): | |
| print("Files in dataset:") | |
| for line in result.stdout.strip().split("\n"): | |
| print(f" {line}") | |
| else: | |
| print("Dataset is empty (no tracked files).") | |
| def cmd_watch(directory=None): | |
| """Watch a local directory for changes and auto-push.""" | |
| import hashlib | |
| watch_dir = Path(directory or ".") | |
| if not watch_dir.exists(): | |
| print(f"ERROR: Directory {watch_dir} does not exist.") | |
| sys.exit(1) | |
| print(f"Watching {watch_dir}/ for changes... (Ctrl+C to stop)") | |
| file_hashes = {} | |
| def get_hashes(): | |
| hashes = {} | |
| for f in watch_dir.rglob("*"): | |
| if f.is_file() and ".git" not in f.parts: | |
| rel = f.relative_to(watch_dir) | |
| try: | |
| hashes[str(rel)] = hashlib.md5(f.read_bytes()).hexdigest() | |
| except: | |
| pass | |
| return hashes | |
| file_hashes = get_hashes() | |
| while True: | |
| time.sleep(5) | |
| new_hashes = get_hashes() | |
| if new_hashes != file_hashes: | |
| changed = [] | |
| for k, v in new_hashes.items(): | |
| if file_hashes.get(k) != v: | |
| changed.append(k) | |
| for k in list(file_hashes.keys()): | |
| if k not in new_hashes: | |
| changed.append(f"{k} (deleted)") | |
| print(f"\nChanges detected: {', '.join(changed)}") | |
| print("Pushing to dataset...") | |
| try: | |
| with tempfile.TemporaryDirectory() as tmp: | |
| clone_repo(tmp) | |
| # Copy changed files | |
| for f in watch_dir.rglob("*"): | |
| if f.is_file() and ".git" not in f.parts: | |
| rel = f.relative_to(watch_dir) | |
| dst = Path(tmp) / rel | |
| dst.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(f, dst) | |
| push_to_repo(tmp, f"Auto-push: {', '.join(changed[:3])}{'...' if len(changed) > 3 else ''}") | |
| except Exception as e: | |
| print(f"Push failed: {e}") | |
| file_hashes = new_hashes | |
| def main(): | |
| if len(sys.argv) < 2: | |
| print(__doc__) | |
| sys.exit(0) | |
| cmd = sys.argv[1] | |
| if cmd == "push": | |
| if len(sys.argv) < 3: | |
| print("Usage: sync-client.py push <file1> [file2] ...") | |
| sys.exit(1) | |
| cmd_push(sys.argv[2:]) | |
| elif cmd == "pull": | |
| output = sys.argv[2] if len(sys.argv) > 2 else None | |
| cmd_pull(output) | |
| elif cmd == "list": | |
| cmd_list() | |
| elif cmd == "watch": | |
| directory = sys.argv[2] if len(sys.argv) > 2 else None | |
| cmd_watch(directory) | |
| else: | |
| print(f"Unknown command: {cmd}") | |
| print("Commands: push, pull, list, watch") | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() | |