Spaces:
Running
Running
| 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"] | |