Spaces:
Running
Running
File size: 5,386 Bytes
1ff0537 9e8330a d468049 0d5747a d468049 0d5747a d468049 0d5747a 0216deb 9e8330a d468049 9e8330a d468049 9e8330a 0d5747a 1ff0537 0d5747a 9e8330a 0216deb 9e8330a 0d5747a 9e8330a d468049 9e8330a d468049 9e8330a 0216deb 9e8330a d468049 9e8330a 0d5747a 9e8330a 1ff0537 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | 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"]
|