om / Dockerfile
javaeeduke's picture
Update Dockerfile
1ff0537 verified
FROM node:22-alpine
RUN apk add --no-cache sqlite rclone
WORKDIR /app
RUN npm install -g omniroute
ENV PORT=7860
ENV HOST=0.0.0.0
ENV NODE_ENV=production
EXPOSE 7860
# ───────── 下载 + 备份 + 反向代理服务(监听 7860)─────────
RUN cat > /app/download_server.js << 'EOF'
const http = require('http');
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const PORT = 7860;
const UPSTREAM_PORT = 8860;
const ALLOWED_DIRS = ['/data', '/root/.omniroute'];
const GD_REMOTE = 'om:om-backup';
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;
}
function runBackup(cb) {
const src = '/root/.omniroute/storage.sqlite';
const tmp = '/data/omni_storage.sqlite';
if (!fs.existsSync(src)) return cb(new Error('storage.sqlite 不存在'));
execFile('sqlite3', [src, `.backup '${tmp}'`], (e1) => {
if (e1) return cb(new Error('sqlite 快照失败: ' + e1.message));
execFile('rclone', ['copyto', tmp, `${GD_REMOTE}/omni_storage.sqlite`], (e2) => {
if (e2) return cb(new Error('rclone 上传失败: ' + e2.message));
const setSrc = '/root/.omniroute/settings.json';
if (fs.existsSync(setSrc)) {
execFile('rclone', ['copyto', setSrc, `${GD_REMOTE}/omni_settings.json`], () => cb(null));
} else { cb(null); }
});
});
}
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`);
const route = url.pathname;
if (route === '/backup') {
runBackup((err) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: false, error: err.message }));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, msg: 'backup -> GDrive 成功', time: new Date().toISOString() }));
}
});
return;
}
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;
}
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);
return;
}
const proxyReq = http.request(
{ hostname: '127.0.0.1', port: UPSTREAM_PORT, path: req.url, method: req.method, headers: req.headers },
proxyRes => { res.writeHead(proxyRes.statusCode, proxyRes.headers); proxyRes.pipe(res); }
);
proxyReq.on('error', () => {
res.writeHead(502, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'upstream not ready' }));
});
req.pipe(proxyReq);
});
server.listen(PORT, '0.0.0.0', () => {
console.log(`[server] listening ${PORT}; routes /list /download /backup, proxy -> ${UPSTREAM_PORT}`);
});
EOF
# ───────── 启动脚本 ─────────
RUN cat > /app/entrypoint.sh << 'EOF'
#!/bin/sh
set -u
mkdir -p /root/.omniroute /data /root/.config/rclone
# rclone 配置
if [ -n "${RCLONE_CONF:-}" ]; then
printf '%s\n' "$RCLONE_CONF" > /root/.config/rclone/rclone.conf
echo "✅ 已写入 rclone 配置"
else
echo "⚠️ 未设置 RCLONE_CONF,Google Drive 不可用"
fi
# 固定加密 key
if [ -n "${STORAGE_ENCRYPTION_KEY:-}" ]; then
echo "STORAGE_ENCRYPTION_KEY=$STORAGE_ENCRYPTION_KEY" > /root/.omniroute/.env
echo "✅ 已写入固定 STORAGE_ENCRYPTION_KEY"
else
echo "⚠️ 未设置 STORAGE_ENCRYPTION_KEY,恢复的库可能解不开!"
fi
GD_REMOTE="om:om-backup"
# 开机从 Google Drive 恢复
if [ -n "${RCLONE_CONF:-}" ]; then
if rclone copyto "$GD_REMOTE/omni_storage.sqlite" /root/.omniroute/storage.sqlite --no-traverse 2>/dev/null; then
echo "✅ 从 GDrive 恢复 storage.sqlite"
rm -f /root/.omniroute/storage.sqlite-wal /root/.omniroute/storage.sqlite-shm
else
echo "⚠️ GDrive 无 storage 备份,跳过恢复"
fi
if rclone copyto "$GD_REMOTE/omni_settings.json" /root/.omniroute/settings.json --no-traverse 2>/dev/null; then
echo "✅ 从 GDrive 恢复 settings.json"
else
echo "⚠️ GDrive 无 settings 备份,跳过恢复"
fi
fi
node /app/download_server.js &
exec env PORT=8860 omniroute
EOF
RUN chmod +x /app/entrypoint.sh
CMD ["/app/entrypoint.sh"]