Spaces:
Running
Running
Update Dockerfile
Browse files- 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 |
-
#
|
| 24 |
RUN cat > /app/download_server.js << 'EOF'
|
| 25 |
-
const http
|
| 26 |
-
const fs
|
| 27 |
-
const path
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
const
|
| 31 |
-
const
|
| 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 |
-
|
| 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 |
-
// ──
|
| 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 |
-
// ──
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
});
|
| 121 |
|
| 122 |
-
// 只监听回环地址更安全;若需外部访问下载,再改为 0.0.0.0 并务必用强 TOKEN
|
| 123 |
server.listen(PORT, '0.0.0.0', () => {
|
| 124 |
-
console.log(`[
|
| 125 |
});
|
| 126 |
EOF
|
| 127 |
|
| 128 |
-
#
|
| 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 |
-
# ──
|
| 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
|
| 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
|
| 169 |
fi
|
| 170 |
done) &
|
| 171 |
|
| 172 |
-
# ──
|
| 173 |
node /app/download_server.js &
|
| 174 |
|
| 175 |
-
# ── 前台
|
| 176 |
-
exec env PORT=
|
| 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"]
|