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"]