om / Dockerfile
javaeeduke's picture
Update Dockerfile
0216deb verified
Raw
History Blame
6.32 kB
FROM node:22-alpine
# 运行时需要 sqlite CLI 做一致性备份;不安装 sqlite-dev(构建依赖,生产不需要)
RUN apk add --no-cache sqlite
WORKDIR /app
RUN npm install -g omniroute
# 基础网络与环境配置
ENV PORT=7860
ENV OMNIROUTE_PORT=7860
ENV HOST=0.0.0.0
ENV NODE_ENV=production
# ⚠️ 不要在 Dockerfile 里写密码/令牌。
# 在 HF Space → Settings → Variables and secrets 中设置:
# INITIAL_PASSWORD (Secret)
# DOWNLOAD_TOKEN (Secret)
# 未设置 DOWNLOAD_TOKEN 时下载服务会拒绝启动(见 download_server.js)。
EXPOSE 7860
# 写入下载服务脚本(纯 Node.js,无需额外依赖)
RUN cat > /app/download_server.js << 'EOF'
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const PORT = 7861;
const TOKEN = process.env.DOWNLOAD_TOKEN;
// 强制要求设置令牌,否则拒绝启动,避免使用默认值导致数据泄露
if (!TOKEN || TOKEN.length < 16) {
console.error('[download-server] 致命错误: 必须设置 DOWNLOAD_TOKEN 且长度 >= 16。已退出。');
process.exit(1);
}
// 允许下载的安全目录白名单
const ALLOWED_DIRS = ['/data', '/root/.omniroute'];
// 恒定时间比较,防止时序攻击且不泄露长度
function tokenValid(provided) {
const a = Buffer.from(String(provided));
const b = Buffer.from(TOKEN);
// 先对两个 buffer 做固定开销的 hash,再比较,规避长度差异
const ha = crypto.createHash('sha256').update(a).digest();
const hb = crypto.createHash('sha256').update(b).digest();
return crypto.timingSafeEqual(ha, hb);
}
function safeResolvePath(filename) {
// 只允许纯文件名,拒绝任何路径分隔符(防路径穿越)
if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return null;
}
for (const dir of ALLOWED_DIRS) {
const fullPath = path.join(dir, filename);
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
return fullPath;
}
}
return null;
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const token = url.searchParams.get('token') || '';
const route = url.pathname;
if (!tokenValid(token)) {
res.writeHead(403, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Forbidden: invalid token' }));
return;
}
// ── GET /list ─────────────────────────────────────────────
if (route === '/list') {
const result = {};
for (const dir of ALLOWED_DIRS) {
try {
result[dir] = fs.readdirSync(dir).map(name => {
const stat = fs.statSync(path.join(dir, name));
return { name, size: stat.size, mtime: stat.mtime };
});
} catch (_) {
result[dir] = 'directory not found';
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result, null, 2));
return;
}
// ── GET /download?token=...&file=omni_storage.sqlite ──────
if (route === '/download') {
const filename = url.searchParams.get('file') || '';
const fullPath = safeResolvePath(filename);
if (!fullPath) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'File not found' })); // 不回显用户输入
return;
}
const stat = fs.statSync(fullPath);
res.writeHead(200, {
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename="${path.basename(fullPath)}"`,
'Content-Length': stat.size,
});
fs.createReadStream(fullPath).pipe(res);
console.log(`[download] ${fullPath} (${stat.size} bytes)`);
return;
}
// ── 404 ───────────────────────────────────────────────────
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unknown route', routes: ['/list', '/download'] }));
});
// 只监听回环地址更安全;若需外部访问下载,再改为 0.0.0.0 并务必用强 TOKEN
server.listen(PORT, '0.0.0.0', () => {
console.log(`[download-server] listening on ${PORT}, routes: /list /download?file=<name>`);
});
EOF
# 写入启动脚本,避免把复杂逻辑塞进 CMD
RUN cat > /app/entrypoint.sh << 'EOF'
#!/bin/sh
set -u
echo "=== 存储诊断开始 ==="
ls -lah /data 2>&1 || echo "/data 目录不存在"
ls -lah /root/.omniroute 2>&1 || echo "OmniRoute 目录不存在"
df -h
echo "=== 存储诊断结束 ==="
mkdir -p /root/.omniroute /data
# ── 开机从持久化存储恢复数据 ──
if [ -f /data/omni_storage.sqlite ]; then
cp /data/omni_storage.sqlite /root/.omniroute/storage.sqlite && echo "✅ 恢复 storage.sqlite 成功"
else
echo "⚠️ /data/omni_storage.sqlite 不存在,跳过恢复"
fi
if [ -f /data/omni_settings.json ]; then
cp /data/omni_settings.json /root/.omniroute/settings.json && echo "✅ 恢复 settings.json 成功"
else
echo "⚠️ /data/omni_settings.json 不存在,跳过恢复"
fi
# ── 后台:每 60 秒一致性备份到 /data ──
(while true; do
sleep 60
if [ -f /root/.omniroute/storage.sqlite ]; then
# 用 sqlite3 .backup 做一致性快照,避免拷到写入中途的损坏状态
if sqlite3 /root/.omniroute/storage.sqlite ".backup '/data/omni_storage.sqlite.tmp'" 2>/dev/null; then
mv -f /data/omni_storage.sqlite.tmp /data/omni_storage.sqlite
echo "💾 [backup] storage.sqlite → /data"
else
echo "⚠️ [backup] sqlite 备份失败"
fi
fi
if [ -f /root/.omniroute/settings.json ]; then
cp /root/.omniroute/settings.json /data/omni_settings.json.tmp &&
mv -f /data/omni_settings.json.tmp /data/omni_settings.json &&
echo "💾 [backup] settings.json → /data"
fi
done) &
# ── 后台:启动下载服务(7861 端口)──
node /app/download_server.js &
# ── 前台:启动主程序 ──
exec env PORT=7860 omniroute
EOF
RUN chmod +x /app/entrypoint.sh
CMD ["/app/entrypoint.sh"]