Download services/storage/git_storage.py from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 8.93 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/services/storage/git_storage.py
- Command line
-
hf download hf://spaces/zlcy/study/services/storage/git_storage.py
-
curl -L -o git_storage.py https://huggingface.co/spaces/zlcy/study/resolve/main/services/storage/git_storage.py
8.93 kB
| from __future__ import annotations | |
| import json | |
| import shutil | |
| import tempfile | |
| from pathlib import Path | |
| from threading import Lock | |
| from typing import Any | |
| from git import Repo | |
| from git.exc import GitCommandError | |
| from services.storage.base import StorageBackend | |
| class GitStorageBackend(StorageBackend): | |
| """Git 私有仓库存储后端""" | |
| def __init__( | |
| self, | |
| repo_url: str, | |
| token: str, | |
| branch: str = "main", | |
| file_path: str = "accounts.json", | |
| auth_keys_file_path: str = "auth_keys.json", | |
| shop_state_file_path: str = "shop_state.json", | |
| local_cache_dir: Path | None = None, | |
| ): | |
| self.repo_url = repo_url | |
| self.token = token | |
| self.branch = branch | |
| self.file_path = file_path | |
| self.auth_keys_file_path = auth_keys_file_path | |
| self.shop_state_file_path = shop_state_file_path | |
| self._lock = Lock() | |
| # 本地缓存目录 | |
| if local_cache_dir is None: | |
| local_cache_dir = Path(tempfile.gettempdir()) / "chatgpt2api_git_cache" | |
| self.local_cache_dir = local_cache_dir | |
| self.local_cache_dir.mkdir(parents=True, exist_ok=True) | |
| # 构建带认证的 Git URL | |
| self.auth_repo_url = self._build_auth_url(repo_url, token) | |
| def _build_auth_url(repo_url: str, token: str) -> str: | |
| """构建带认证的 Git URL""" | |
| if not token: | |
| return repo_url | |
| # 支持 HTTPS 格式:https://github.com/user/repo.git | |
| if repo_url.startswith("https://"): | |
| # 插入 token | |
| return repo_url.replace("https://", f"https://{token}@") | |
| # 支持 git@ 格式:git@github.com:user/repo.git | |
| # 转换为 HTTPS 格式 | |
| if repo_url.startswith("git@"): | |
| repo_url = repo_url.replace("git@", "https://") | |
| repo_url = repo_url.replace(".com:", ".com/") | |
| return repo_url.replace("https://", f"https://{token}@") | |
| return repo_url | |
| def _safe_remove_repo(self, repo_path: Path) -> None: | |
| try: | |
| if repo_path.exists(): | |
| shutil.rmtree(repo_path) | |
| except Exception as exc: | |
| print(f"[git-storage] failed to remove broken cache {repo_path}: {exc}") | |
| def _clone_repo(self, repo_path: Path) -> Repo: | |
| for attempt in range(2): | |
| try: | |
| return Repo.clone_from( | |
| self.auth_repo_url, | |
| repo_path, | |
| branch=self.branch, | |
| ) | |
| except Exception: | |
| self._safe_remove_repo(repo_path) | |
| if attempt == 0: | |
| continue | |
| raise | |
| raise RuntimeError("git clone failed") | |
| def _clone_or_pull(self) -> Repo: | |
| """克隆或拉取仓库""" | |
| repo_path = self.local_cache_dir / "repo" | |
| if repo_path.exists() and (repo_path / ".git").exists(): | |
| # 仓库已存在,拉取最新代码 | |
| try: | |
| repo = Repo(repo_path) | |
| origin = repo.remote("origin") | |
| origin.pull(self.branch) | |
| return repo | |
| except GitCommandError: | |
| # 拉取失败,删除重新克隆 | |
| self._safe_remove_repo(repo_path) | |
| except Exception: | |
| self._safe_remove_repo(repo_path) | |
| elif repo_path.exists(): | |
| self._safe_remove_repo(repo_path) | |
| # 克隆仓库 | |
| return self._clone_repo(repo_path) | |
| def load_accounts(self) -> list[dict[str, Any]]: | |
| """从 Git 仓库加载账号数据""" | |
| try: | |
| return self._load_json_file(self.file_path) | |
| except Exception as e: | |
| print(f"[git-storage] load failed: {e}") | |
| raise | |
| def save_accounts(self, accounts: list[dict[str, Any]]) -> None: | |
| """保存账号数据到 Git 仓库""" | |
| try: | |
| self._save_json_file(self.file_path, accounts, "Update accounts data") | |
| except Exception as e: | |
| print(f"[git-storage] save failed: {e}") | |
| raise e | |
| def load_auth_keys(self) -> list[dict[str, Any]]: | |
| """从 Git 仓库加载鉴权密钥数据""" | |
| try: | |
| data = self._load_json_value(self.auth_keys_file_path) | |
| if isinstance(data, dict): | |
| data = data.get("items") | |
| return data if isinstance(data, list) else [] | |
| except Exception as e: | |
| print(f"[git-storage] load failed: {e}") | |
| raise | |
| def save_auth_keys(self, auth_keys: list[dict[str, Any]]) -> None: | |
| """保存鉴权密钥数据到 Git 仓库""" | |
| try: | |
| self._save_json_file(self.auth_keys_file_path, {"items": auth_keys}, "Update auth keys data") | |
| except Exception as e: | |
| print(f"[git-storage] save failed: {e}") | |
| raise e | |
| def load_shop_state(self) -> dict[str, Any]: | |
| try: | |
| data = self._load_json_value(self.shop_state_file_path) | |
| return data if isinstance(data, dict) else {} | |
| except Exception as e: | |
| print(f"[git-storage] load failed: {e}") | |
| raise | |
| def save_shop_state(self, state: dict[str, Any]) -> None: | |
| try: | |
| self._save_json_file(self.shop_state_file_path, state, "Update shop state data") | |
| except Exception as e: | |
| print(f"[git-storage] save failed: {e}") | |
| raise e | |
| def _load_json_file(self, file_path: str) -> list[dict[str, Any]]: | |
| data = self._load_json_value(file_path) | |
| return data if isinstance(data, list) else [] | |
| def _load_json_value(self, file_path: str) -> Any: | |
| with self._lock: | |
| repo = self._clone_or_pull() | |
| file_full_path = Path(repo.working_dir) / file_path | |
| if not file_full_path.exists(): | |
| return None | |
| try: | |
| content = file_full_path.read_text(encoding="utf-8") | |
| return json.loads(content) | |
| except (json.JSONDecodeError, UnicodeDecodeError) as e: | |
| print(f"[git-storage] Failed to load {file_path}: {e}, returning empty data") | |
| return None | |
| def _save_json_file(self, file_path: str, items: Any, message: str) -> None: | |
| with self._lock: | |
| payload = json.dumps(items, ensure_ascii=False, indent=2) + "\n" | |
| for attempt in range(2): | |
| repo = self._clone_or_pull() | |
| file_full_path = Path(repo.working_dir) / file_path | |
| file_full_path.parent.mkdir(parents=True, exist_ok=True) | |
| file_full_path.write_text(payload, encoding="utf-8") | |
| repo.index.add([file_path]) | |
| if not repo.is_dirty(): | |
| return | |
| try: | |
| # The data repository only stores JSON state; deployment volumes may | |
| # preserve non-executable sample hooks, so skip hooks for reliability. | |
| repo.index.commit(message, skip_hooks=True) | |
| repo.remote("origin").push(self.branch) | |
| return | |
| except GitCommandError: | |
| if attempt == 0: | |
| self._safe_remove_repo(self.local_cache_dir / "repo") | |
| continue | |
| raise | |
| def health_check(self) -> dict[str, Any]: | |
| """健康检查""" | |
| try: | |
| repo = self._clone_or_pull() | |
| return { | |
| "status": "healthy", | |
| "backend": "git", | |
| "repo_url": self._mask_token(self.repo_url), | |
| "branch": self.branch, | |
| "file_path": self.file_path, | |
| "auth_keys_file_path": self.auth_keys_file_path, | |
| "shop_state_file_path": self.shop_state_file_path, | |
| "last_commit": repo.head.commit.hexsha[:8], | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "unhealthy", | |
| "backend": "git", | |
| "error": str(e), | |
| } | |
| def get_backend_info(self) -> dict[str, Any]: | |
| """获取存储后端信息""" | |
| return { | |
| "type": "git", | |
| "description": "Git 私有仓库存储", | |
| "repo_url": self._mask_token(self.repo_url), | |
| "branch": self.branch, | |
| "file_path": self.file_path, | |
| "auth_keys_file_path": self.auth_keys_file_path, | |
| "shop_state_file_path": self.shop_state_file_path, | |
| } | |
| def _mask_token(url: str) -> str: | |
| """隐藏 URL 中的 token""" | |
| if "@" in url and "://" in url: | |
| protocol, rest = url.split("://", 1) | |
| if "@" in rest: | |
| _, host = rest.split("@", 1) | |
| return f"{protocol}://****@{host}" | |
| return url | |