| |
| FROM xcq0607/lxserver:latest |
|
|
| |
| RUN apk add --no-cache python3 py3-pip curl socat tar && \ |
| pip3 install --no-cache-dir --break-system-packages requests huggingface_hub && \ |
| rm -rf /var/cache/apk/* |
|
|
| |
| RUN cat > /usr/local/bin/sync.py << 'SYNC_EOF' |
| |
| import os, sys, tarfile, tempfile, time |
| from datetime import datetime, timedelta |
| from huggingface_hub import HfApi |
| from requests.exceptions import HTTPError |
|
|
| LX_DATA_BASE = "/server" |
| BACKUP_DIRS = ["data", "logs", "cache"] |
| CONFIG_FILES = ["config.json"] |
| BACKUP_PREFIX = "lxmusic_backup_" |
| KEEP_DAYS = 7 |
| RETRY_COUNT = 3 |
| RETRY_DELAY = 5 |
|
|
| def log(msg): print(f"[SYNC] {msg}") |
| def get_env(var): return os.getenv(var, "").strip() |
|
|
| def validate_hf_access(repo_id, token): |
| api = HfApi() |
| try: |
| api.repo_info(repo_id=repo_id, repo_type="dataset", token=token) |
| except HTTPError as e: |
| if e.response and e.response.status_code == 404: |
| log(f"仓库 {repo_id} 不存在,尝试创建...") |
| api.create_repo(repo_id=repo_id, repo_type="dataset", token=token, private=True) |
| log("仓库创建成功") |
| else: |
| raise PermissionError(f"访问仓库失败: {e}") |
| |
| test_file = tempfile.NamedTemporaryFile(mode="w", delete=False) |
| test_file.write("test") |
| test_file.close() |
| try: |
| api.upload_file( |
| path_or_fileobj=test_file.name, |
| path_in_repo=".write_test", |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| api.delete_file(path_in_repo=".write_test", repo_id=repo_id, repo_type="dataset", token=token) |
| log("写入权限验证通过") |
| except Exception as e: |
| raise PermissionError(f"写入权限不足: {e}") |
| finally: |
| os.unlink(test_file.name) |
|
|
| def restore(): |
| repo_id = get_env("HF_DATASET") |
| token = get_env("HF_TOKEN") |
| if not repo_id or not token: |
| log("未设置 HF_DATASET 或 HF_TOKEN,跳过恢复") |
| return False |
| try: |
| validate_hf_access(repo_id, token) |
| except Exception as e: |
| log(f"访问验证失败: {e}") |
| return False |
| api = HfApi() |
| try: |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| except Exception as e: |
| log(f"列出文件失败: {e}") |
| return False |
| candidates = [] |
| for i in range(KEEP_DAYS): |
| day = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d") |
| name = f"{BACKUP_PREFIX}{day}.tar.gz" |
| if name in files: |
| candidates.append(name) |
| if not candidates: |
| log("未找到最近备份") |
| return False |
| latest = sorted(candidates, reverse=True)[0] |
| log(f"找到备份: {latest},开始下载") |
| for attempt in range(RETRY_COUNT): |
| try: |
| local = api.hf_hub_download( |
| repo_id=repo_id, |
| filename=latest, |
| repo_type="dataset", |
| token=token, |
| resume=True |
| ) |
| break |
| except Exception as e: |
| log(f"下载失败 ({attempt+1}/{RETRY_COUNT}): {e}") |
| time.sleep(RETRY_DELAY) |
| else: |
| return False |
| try: |
| with tarfile.open(local, "r:gz") as tar: |
| tar.extractall(path=LX_DATA_BASE) |
| log("备份恢复成功") |
| return True |
| except Exception as e: |
| log(f"解压失败: {e}") |
| return False |
|
|
| def backup(): |
| repo_id = get_env("HF_DATASET") |
| token = get_env("HF_TOKEN") |
| if not repo_id or not token: |
| log("未设置备份环境变量,跳过备份") |
| return |
| try: |
| validate_hf_access(repo_id, token) |
| except Exception as e: |
| log(f"备份验证失败: {e}") |
| return |
| day = datetime.now().strftime("%Y-%m-%d") |
| backup_name = f"{BACKUP_PREFIX}{day}.tar.gz" |
| with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp: |
| tmp.close() |
| with tarfile.open(tmp.name, "w:gz") as tar: |
| for d in BACKUP_DIRS: |
| src = f"{LX_DATA_BASE}/{d}" |
| if os.path.exists(src): |
| tar.add(src, arcname=d) |
| for cfg in CONFIG_FILES: |
| src = f"{LX_DATA_BASE}/{cfg}" |
| if os.path.exists(src): |
| tar.add(src, arcname=cfg) |
| api = HfApi() |
| api.upload_file( |
| path_or_fileobj=tmp.name, |
| path_in_repo=backup_name, |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| log(f"备份 {backup_name} 上传成功") |
| |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| cutoff = datetime.now() - timedelta(days=KEEP_DAYS) |
| for f in files: |
| if f.startswith(BACKUP_PREFIX) and f.endswith(".tar.gz"): |
| date_str = f[len(BACKUP_PREFIX):-7] |
| try: |
| if datetime.strptime(date_str, "%Y-%m-%d") < cutoff: |
| api.delete_file(path_in_repo=f, repo_id=repo_id, repo_type="dataset", token=token) |
| log(f"删除旧备份: {f}") |
| except: pass |
| os.unlink(tmp.name) |
|
|
| if __name__ == "__main__": |
| if len(sys.argv) > 1 and sys.argv[1] == "backup": |
| backup() |
| else: |
| restore() |
| SYNC_EOF |
|
|
| RUN chmod +x /usr/local/bin/sync.py |
|
|
| |
| RUN printf '#!/bin/sh\n\ |
| set -e\n\ |
| \n\ |
| echo "===== LXMusic Server with Backup/Restore ====="\n\ |
| \n\ |
| mkdir -p /server/data /server/logs /server/cache\n\ |
| \n\ |
| python3 /usr/local/bin/sync.py restore\n\ |
| \n\ |
| (\n\ |
| while true; do\n\ |
| sleep 1800\n\ |
| echo "--- 定时备份 ---"\n\ |
| python3 /usr/local/bin/sync.py backup\n\ |
| done\n\ |
| ) &\n\ |
| echo "Background backup started (every 30 minutes)"\n\ |
| \n\ |
| cd /server\n\ |
| exec node index.js\n\ |
| ' > /start.sh && chmod +x /start.sh |
|
|
| EXPOSE 9527 |
| CMD ["/start.sh"] |