Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import threading | |
| import time | |
| import shutil | |
| from pathlib import Path | |
| from flask import Flask, jsonify, request, send_from_directory | |
| try: | |
| from dotenv import load_dotenv | |
| load_dotenv(override=True) | |
| except ImportError: | |
| pass | |
| try: | |
| from huggingface_hub import HfApi, snapshot_download, create_repo | |
| except ImportError: | |
| HfApi = None | |
| app = Flask(__name__, static_folder='frontend/dist') | |
| # Configuration | |
| DATA_DIR = Path("data") | |
| DATA_DIR.mkdir(parents=True, exist_ok=True) | |
| SKILLS_FILE = DATA_DIR / "skills.json" | |
| REPO_ID = os.getenv("HF_DB_REPO", "duqing2026/skill-tree-data") # Default repo, user should configure | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| # Default initial data | |
| DEFAULT_SKILLS = { | |
| "nodes": [ | |
| { | |
| "id": "1", | |
| "type": "input", | |
| "data": { | |
| "label": "开始", | |
| "status": "completed", | |
| "description": "这是你的技能树起点。", | |
| "links": [] | |
| }, | |
| "position": {"x": 250, "y": 0} | |
| }, | |
| { | |
| "id": "2", | |
| "data": { | |
| "label": "学习 Python", | |
| "status": "in_progress", | |
| "description": "掌握 Python 基础语法和常用库。", | |
| "links": [{"title": "Python 官网", "url": "https://www.python.org"}] | |
| }, | |
| "position": {"x": 100, "y": 100} | |
| }, | |
| { | |
| "id": "3", | |
| "data": { | |
| "label": "学习 React", | |
| "status": "pending", | |
| "description": "学习组件化开发和 Hooks。", | |
| "links": [{"title": "React 文档", "url": "https://react.dev"}] | |
| }, | |
| "position": {"x": 400, "y": 100} | |
| }, | |
| ], | |
| "edges": [ | |
| {"id": "e1-2", "source": "1", "target": "2"}, | |
| {"id": "e1-3", "source": "1", "target": "3"}, | |
| ] | |
| } | |
| class SyncManager: | |
| def __init__(self, repo_id, token, data_dir): | |
| self.repo_id = repo_id | |
| self.token = token | |
| self.data_dir = data_dir | |
| self.api = HfApi(token=token) if HfApi and token else None | |
| self.is_pushing = False | |
| self.status = "online" # online, offline, auth_error | |
| self.last_error = None | |
| # Ensure repo exists | |
| if self.api: | |
| try: | |
| # Check connection first with a lightweight call | |
| self.api.whoami() | |
| self.status = "online" | |
| print(f"Sync: Ensuring repository {self.repo_id} exists...") | |
| create_repo( | |
| repo_id=self.repo_id, | |
| token=self.token, | |
| repo_type="dataset", | |
| exist_ok=True, | |
| private=True | |
| ) | |
| except Exception as e: | |
| # Suppress full stack trace for connection errors | |
| error_str = str(e) | |
| self.last_error = error_str | |
| if "MaxRetryError" in error_str or "SSLError" in error_str or "ConnectionError" in error_str: | |
| print(f"Sync: Warning - Could not connect to Hugging Face (Offline Mode). Sync disabled.") | |
| self.status = "offline" | |
| self.api = None # Disable sync for this session | |
| elif "401" in error_str or "Invalid user token" in error_str: | |
| print(f"Sync: Error - Invalid HF_TOKEN. Sync disabled. Please check your .env file.") | |
| self.status = "auth_error" | |
| self.api = None | |
| else: | |
| print(f"Sync: Warning - Could not ensure repo exists: {e}") | |
| self.status = "error" | |
| def pull(self): | |
| if not self.api: | |
| print("Sync: Skipping pull (no API/Token)") | |
| return | |
| print("Sync: Pulling data...") | |
| try: | |
| temp_dir = self.data_dir.parent / "temp_data_sync" | |
| if temp_dir.exists(): shutil.rmtree(temp_dir) | |
| temp_dir.mkdir() | |
| snapshot_download( | |
| repo_id=self.repo_id, | |
| repo_type="dataset", | |
| local_dir=temp_dir, | |
| token=self.token, | |
| allow_patterns=["skills.json"] | |
| ) | |
| # Simple overwrite strategy for now, as this is a personal tool | |
| # In a real multi-user scenario, we'd need smart merging | |
| src = temp_dir / "skills.json" | |
| dst = self.data_dir / "skills.json" | |
| if src.exists(): | |
| shutil.copy2(src, dst) | |
| print("Sync: Data pulled successfully.") | |
| else: | |
| print("Sync: Remote has no skills.json.") | |
| # If remote is empty but we have local data, push it to initialize remote | |
| if (self.data_dir / "skills.json").exists(): | |
| print("Sync: Initializing remote repository with local data...") | |
| self.push() | |
| shutil.rmtree(temp_dir) | |
| except Exception as e: | |
| error_str = str(e) | |
| if "404" in error_str or "Repository Not Found" in error_str: | |
| print(f"Sync: Remote data not found (new repo?). Starting with local data.") | |
| # If remote is empty but we have local data, push it to initialize remote | |
| if (self.data_dir / "skills.json").exists(): | |
| print("Sync: Initializing remote repository with local data...") | |
| self.push() | |
| else: | |
| print(f"Sync: Pull failed: {e}") | |
| def push(self): | |
| if not self.api: return | |
| if self.is_pushing: return | |
| print("Sync: Pushing data...") | |
| self.is_pushing = True | |
| try: | |
| self.api.upload_file( | |
| path_or_fileobj=self.data_dir / "skills.json", | |
| path_in_repo="skills.json", | |
| repo_id=self.repo_id, | |
| repo_type="dataset", | |
| commit_message=f"Update skills.json {int(time.time())}" | |
| ) | |
| print("Sync: Push successful.") | |
| except Exception as e: | |
| print(f"Sync: Push failed: {e}") | |
| finally: | |
| self.is_pushing = False | |
| sync_manager = SyncManager(REPO_ID, HF_TOKEN, DATA_DIR) | |
| # Initialize data | |
| if not SKILLS_FILE.exists(): | |
| # Try to pull first | |
| sync_manager.pull() | |
| # If still not exists, use default | |
| if not SKILLS_FILE.exists(): | |
| with open(SKILLS_FILE, "w") as f: | |
| json.dump(DEFAULT_SKILLS, f, indent=2) | |
| # New default data created, push it to remote | |
| print("Sync: Created default data. Pushing to remote...") | |
| sync_manager.push() | |
| else: | |
| # If local exists, try to pull (and auto-push if remote is empty via the catch block above) | |
| sync_manager.pull() | |
| def serve_index(): | |
| if os.path.exists(os.path.join(app.static_folder, "index.html")): | |
| return send_from_directory(app.static_folder, "index.html") | |
| return "Frontend not built. Please run `npm run build` in frontend directory." | |
| def serve_static(path): | |
| if os.path.exists(os.path.join(app.static_folder, path)): | |
| return send_from_directory(app.static_folder, path) | |
| return serve_index() | |
| def get_skills(): | |
| if SKILLS_FILE.exists(): | |
| with open(SKILLS_FILE, "r") as f: | |
| return jsonify(json.load(f)) | |
| return jsonify(DEFAULT_SKILLS) | |
| def save_skills(): | |
| data = request.json | |
| with open(SKILLS_FILE, "w") as f: | |
| json.dump(data, f, indent=2) | |
| # Trigger sync in background | |
| if sync_manager.api: | |
| threading.Thread(target=sync_manager.push).start() | |
| return jsonify({"status": "success"}) | |
| def get_sync_status(): | |
| return jsonify({ | |
| "status": sync_manager.status, | |
| "repo_id": sync_manager.repo_id, | |
| "last_error": sync_manager.last_error | |
| }) | |
| if __name__ == "__main__": | |
| port = int(os.getenv("PORT", 7860)) | |
| app.run(host="0.0.0.0", port=port) | |