javaeeduke commited on
Commit
d468049
·
verified ·
1 Parent(s): a5388dd

Update Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +36 -83
Dockerfile CHANGED
@@ -1,80 +1,40 @@
1
  FROM node:22-alpine
2
 
3
- # 运行时需要 sqlite CLI 做一致性备份;不安装 sqlite-dev(构建依赖,生产不需要)
4
  RUN apk add --no-cache sqlite
5
 
6
  WORKDIR /app
7
  RUN npm install -g omniroute
8
 
9
- # 基础网络与环境配置
10
  ENV PORT=7860
11
- ENV OMNIROUTE_PORT=7860
12
  ENV HOST=0.0.0.0
13
  ENV NODE_ENV=production
14
 
15
- # ⚠️ 不要在 Dockerfile 里写密码/令牌。
16
- # 在 HF Space → Settings → Variables and secrets 中设置:
17
- # INITIAL_PASSWORD (Secret)
18
- # DOWNLOAD_TOKEN (Secret)
19
- # 未设置 DOWNLOAD_TOKEN 时下载服务会拒绝启动(见 download_server.js)。
20
-
21
  EXPOSE 7860
22
 
23
- # 写入下载服务脚本 Node.js,无需额外依赖
24
  RUN cat > /app/download_server.js << 'EOF'
25
- const http = require('http');
26
- const fs = require('fs');
27
- const path = require('path');
28
- const crypto = require('crypto');
29
-
30
- const PORT = 7861;
31
- const TOKEN = process.env.DOWNLOAD_TOKEN;
32
-
33
- // 强制要求设置令牌,否则拒绝启动,避免使用默认值导致数据泄露
34
- //if (!TOKEN || TOKEN.length < 16) {
35
- // console.error('[download-server] 致命错误: 必须设置 DOWNLOAD_TOKEN 且长度 >= 16。已退出。');
36
- // process.exit(1);
37
- //}
38
-
39
- // 允许下载的安全目录白名单
40
- const ALLOWED_DIRS = ['/data', '/root/.omniroute'];
41
-
42
- // 恒定时间比较,防止时序攻击且不泄露长度
43
- function tokenValid(provided) {
44
- const a = Buffer.from(String(provided));
45
- const b = Buffer.from(TOKEN);
46
- // 先对两个 buffer 做固定开销的 hash,再比较,规避长度差异
47
- const ha = crypto.createHash('sha256').update(a).digest();
48
- const hb = crypto.createHash('sha256').update(b).digest();
49
- return crypto.timingSafeEqual(ha, hb);
50
- }
51
 
52
  function safeResolvePath(filename) {
53
- // 只允许纯文件名,拒绝任何路径分隔符(防路径穿越)
54
- if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
55
- return null;
56
- }
57
  for (const dir of ALLOWED_DIRS) {
58
  const fullPath = path.join(dir, filename);
59
- if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
60
- return fullPath;
61
- }
62
  }
63
  return null;
64
  }
65
 
66
  const server = http.createServer((req, res) => {
67
  const url = new URL(req.url, `http://localhost:${PORT}`);
68
- const token = url.searchParams.get('token') || '';
69
  const route = url.pathname;
70
 
71
- if (!tokenValid(token)) {
72
- res.writeHead(403, { 'Content-Type': 'application/json' });
73
- res.end(JSON.stringify({ error: 'Forbidden: invalid token' }));
74
- return;
75
- }
76
-
77
- // ── GET /list ─────────────────────────────────────────────
78
  if (route === '/list') {
79
  const result = {};
80
  for (const dir of ALLOWED_DIRS) {
@@ -83,26 +43,22 @@ const server = http.createServer((req, res) => {
83
  const stat = fs.statSync(path.join(dir, name));
84
  return { name, size: stat.size, mtime: stat.mtime };
85
  });
86
- } catch (_) {
87
- result[dir] = 'directory not found';
88
- }
89
  }
90
  res.writeHead(200, { 'Content-Type': 'application/json' });
91
  res.end(JSON.stringify(result, null, 2));
92
  return;
93
  }
94
 
95
- // ── GET /download?token=...&file=omni_storage.sqlite ──────
96
  if (route === '/download') {
97
  const filename = url.searchParams.get('file') || '';
98
  const fullPath = safeResolvePath(filename);
99
-
100
  if (!fullPath) {
101
  res.writeHead(404, { 'Content-Type': 'application/json' });
102
- res.end(JSON.stringify({ error: 'File not found' })); // 不回显用户输入
103
  return;
104
  }
105
-
106
  const stat = fs.statSync(fullPath);
107
  res.writeHead(200, {
108
  'Content-Type': 'application/octet-stream',
@@ -114,67 +70,64 @@ const server = http.createServer((req, res) => {
114
  return;
115
  }
116
 
117
- // ── 404 ───────────────────────────────────────────────────
118
- res.writeHead(404, { 'Content-Type': 'application/json' });
119
- res.end(JSON.stringify({ error: 'Unknown route', routes: ['/list', '/download'] }));
 
 
 
 
 
 
 
120
  });
121
 
122
- // 只监听回环地址更安全;若需外部访问下载,再改为 0.0.0.0 并务必用强 TOKEN
123
  server.listen(PORT, '0.0.0.0', () => {
124
- console.log(`[download-server] listening on ${PORT}, routes: /list /download?file=<name>`);
125
  });
126
  EOF
127
 
128
- # 写入启动脚本,避免把复杂逻辑塞进 CMD
129
  RUN cat > /app/entrypoint.sh << 'EOF'
130
  #!/bin/sh
131
  set -u
132
 
133
- echo "=== 存储诊断开始 ==="
134
- ls -lah /data 2>&1 || echo "/data 目录不存在"
135
- ls -lah /root/.omniroute 2>&1 || echo "OmniRoute 目录不存在"
136
- df -h
137
- echo "=== 存储诊断结束 ==="
138
-
139
  mkdir -p /root/.omniroute /data
140
 
141
- # ── 开机从持久化存储恢复数据 ──
142
  if [ -f /data/omni_storage.sqlite ]; then
143
- cp /data/omni_storage.sqlite /root/.omniroute/storage.sqlite && echo "✅ 恢复 storage.sqlite 成功"
144
  else
145
  echo "⚠️ /data/omni_storage.sqlite 不存在,跳过恢复"
146
  fi
147
  if [ -f /data/omni_settings.json ]; then
148
- cp /data/omni_settings.json /root/.omniroute/settings.json && echo "✅ 恢复 settings.json 成功"
149
  else
150
  echo "⚠️ /data/omni_settings.json 不存在,跳过恢复"
151
  fi
152
 
153
- # ── 后台:每 60 秒一致性备份到 /data ──
154
  (while true; do
155
  sleep 60
156
  if [ -f /root/.omniroute/storage.sqlite ]; then
157
- # 用 sqlite3 .backup 做一致性快照,避免拷到写入中途的损坏状态
158
  if sqlite3 /root/.omniroute/storage.sqlite ".backup '/data/omni_storage.sqlite.tmp'" 2>/dev/null; then
159
  mv -f /data/omni_storage.sqlite.tmp /data/omni_storage.sqlite
160
- echo "💾 [backup] storage.sqlite → /data"
161
- else
162
- echo "⚠️ [backup] sqlite 备份失败"
163
  fi
164
  fi
165
  if [ -f /root/.omniroute/settings.json ]; then
166
  cp /root/.omniroute/settings.json /data/omni_settings.json.tmp &&
167
  mv -f /data/omni_settings.json.tmp /data/omni_settings.json &&
168
- echo "💾 [backup] settings.json → /data"
169
  fi
170
  done) &
171
 
172
- # ── 后台:启动下载服务(7861 端口)──
173
  node /app/download_server.js &
174
 
175
- # ── 前台启动主程序 ──
176
- exec env PORT=7860 omniroute
177
  EOF
178
  RUN chmod +x /app/entrypoint.sh
179
 
180
- CMD ["/app/entrypoint.sh"]
 
1
  FROM node:22-alpine
2
 
 
3
  RUN apk add --no-cache sqlite
4
 
5
  WORKDIR /app
6
  RUN npm install -g omniroute
7
 
 
8
  ENV PORT=7860
 
9
  ENV HOST=0.0.0.0
10
  ENV NODE_ENV=production
11
 
 
 
 
 
 
 
12
  EXPOSE 7860
13
 
14
+ # 下载 + 反向代理服务(监听 7860
15
  RUN cat > /app/download_server.js << 'EOF'
16
+ const http = require('http');
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const PORT = 7860; // HF 唯一暴露端口
21
+ const UPSTREAM_PORT = 8860; // omniroute 内部端口
22
+ const ALLOWED_DIRS = ['/data', '/root/.omniroute'];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  function safeResolvePath(filename) {
25
+ if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) return null;
 
 
 
26
  for (const dir of ALLOWED_DIRS) {
27
  const fullPath = path.join(dir, filename);
28
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) return fullPath;
 
 
29
  }
30
  return null;
31
  }
32
 
33
  const server = http.createServer((req, res) => {
34
  const url = new URL(req.url, `http://localhost:${PORT}`);
 
35
  const route = url.pathname;
36
 
37
+ // ── 列出可下载文件 ──
 
 
 
 
 
 
38
  if (route === '/list') {
39
  const result = {};
40
  for (const dir of ALLOWED_DIRS) {
 
43
  const stat = fs.statSync(path.join(dir, name));
44
  return { name, size: stat.size, mtime: stat.mtime };
45
  });
46
+ } catch (_) { result[dir] = 'directory not found'; }
 
 
47
  }
48
  res.writeHead(200, { 'Content-Type': 'application/json' });
49
  res.end(JSON.stringify(result, null, 2));
50
  return;
51
  }
52
 
53
+ // ── 下载文件(无 token──
54
  if (route === '/download') {
55
  const filename = url.searchParams.get('file') || '';
56
  const fullPath = safeResolvePath(filename);
 
57
  if (!fullPath) {
58
  res.writeHead(404, { 'Content-Type': 'application/json' });
59
+ res.end(JSON.stringify({ error: 'File not found' }));
60
  return;
61
  }
 
62
  const stat = fs.statSync(fullPath);
63
  res.writeHead(200, {
64
  'Content-Type': 'application/octet-stream',
 
70
  return;
71
  }
72
 
73
+ // ── 其余请求反向代理给 omniroute (8860) ──
74
+ const proxyReq = http.request(
75
+ { hostname: '127.0.0.1', port: UPSTREAM_PORT, path: req.url, method: req.method, headers: req.headers },
76
+ proxyRes => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }
77
+ );
78
+ proxyReq.on('error', () => {
79
+ res.writeHead(502, { 'Content-Type': 'application/json' });
80
+ res.end(JSON.stringify({ error: 'upstream not ready' }));
81
+ });
82
+ req.pipe(proxyReq);
83
  });
84
 
 
85
  server.listen(PORT, '0.0.0.0', () => {
86
+ console.log(`[server] listening ${PORT}; routes /list /download, proxy -> ${UPSTREAM_PORT}`);
87
  });
88
  EOF
89
 
90
+ # 启动脚本
91
  RUN cat > /app/entrypoint.sh << 'EOF'
92
  #!/bin/sh
93
  set -u
94
 
 
 
 
 
 
 
95
  mkdir -p /root/.omniroute /data
96
 
97
+ # ── 开机从 /data 恢复 ──
98
  if [ -f /data/omni_storage.sqlite ]; then
99
+ cp /data/omni_storage.sqlite /root/.omniroute/storage.sqlite && echo "✅ 恢复 storage.sqlite"
100
  else
101
  echo "⚠️ /data/omni_storage.sqlite 不存在,跳过恢复"
102
  fi
103
  if [ -f /data/omni_settings.json ]; then
104
+ cp /data/omni_settings.json /root/.omniroute/settings.json && echo "✅ 恢复 settings.json"
105
  else
106
  echo "⚠️ /data/omni_settings.json 不存在,跳过恢复"
107
  fi
108
 
109
+ # ── 每 60 秒备份到 /data ──
110
  (while true; do
111
  sleep 60
112
  if [ -f /root/.omniroute/storage.sqlite ]; then
 
113
  if sqlite3 /root/.omniroute/storage.sqlite ".backup '/data/omni_storage.sqlite.tmp'" 2>/dev/null; then
114
  mv -f /data/omni_storage.sqlite.tmp /data/omni_storage.sqlite
115
+ echo "💾 [backup] storage.sqlite"
 
 
116
  fi
117
  fi
118
  if [ -f /root/.omniroute/settings.json ]; then
119
  cp /root/.omniroute/settings.json /data/omni_settings.json.tmp &&
120
  mv -f /data/omni_settings.json.tmp /data/omni_settings.json &&
121
+ echo "💾 [backup] settings.json"
122
  fi
123
  done) &
124
 
125
+ # ── 启动下载+代理服务(7860)──
126
  node /app/download_server.js &
127
 
128
+ # ── 前台启动 omniroute(内部 8860)──
129
+ exec env PORT=8860 omniroute
130
  EOF
131
  RUN chmod +x /app/entrypoint.sh
132
 
133
+ CMD ["/app/entrypoint.sh"]