File size: 6,155 Bytes
5fd4643
e35a36c
 
7e3c809
 
6523d00
5fd4643
e35a36c
cfafa7b
ed69bdb
 
1df45a1
ed69bdb
ee37362
 
ed69bdb
 
 
1aa22df
ed69bdb
 
 
 
 
1df45a1
 
ed69bdb
 
 
 
 
ee37362
1df45a1
ee37362
1df45a1
 
ee37362
 
cfafa7b
ed69bdb
1df45a1
ed69bdb
 
cfafa7b
 
 
 
 
 
 
 
 
 
 
ed69bdb
 
 
 
1df45a1
 
ed69bdb
 
 
 
 
 
1df45a1
ed69bdb
 
 
 
 
 
 
 
 
 
 
 
 
 
1df45a1
ed69bdb
1df45a1
ed69bdb
 
 
cfafa7b
 
 
 
 
 
 
ed69bdb
 
1df45a1
ed69bdb
 
 
 
1df45a1
 
 
ed69bdb
 
 
 
 
 
1df45a1
 
ed69bdb
1df45a1
ed69bdb
 
 
 
1df45a1
ed69bdb
 
 
1df45a1
 
 
ed69bdb
 
 
 
 
 
 
 
 
cfafa7b
 
 
 
 
 
 
ed69bdb
7e3c809
1df45a1
 
 
 
 
 
 
cfafa7b
1df45a1
 
 
ed69bdb
 
 
 
 
 
 
 
 
 
cfafa7b
9642b8f
ed69bdb
 
7e3c809
ed69bdb
 
 
 
 
 
 
 
1df45a1
ed69bdb
 
 
 
 
7e3c809
 
1aa22df
ed69bdb
e5f80c5
1aa22df
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# 使用 lxmusic 官方镜像(Alpine 基础)
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/*

# ========= 备份恢复脚本 sync.py(修复 API 调用错误) =========
RUN cat > /usr/local/bin/sync.py << 'SYNC_EOF'
#!/usr/bin/env python3
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"]