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