@fudrouter/fsrouter 0.6.99 → 0.6.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/routes/db/route.js +141 -97
- package/package.json +1 -1
- package/public/.vite/manifest.json +90 -90
- package/public/assets/EndpointPageClient.B1pL25ws.js +6 -0
- package/public/assets/Loading.C1s7rolj.js +1 -0
- package/public/assets/NoAuthProxyCard.CrFQ6U88.js +89 -0
- package/public/assets/index.2a3mqmx-.js +89 -0
- package/public/assets/index.Blvwx0HS.css +1 -0
- package/public/assets/page.1w2ClxJa.js +64 -0
- package/public/assets/page.B1WuiJNL.js +1 -0
- package/public/assets/page.B5pzcN8j.js +1 -0
- package/public/assets/page.B6HgqxIh.js +1 -0
- package/public/assets/page.B9nZj2bM.js +1 -0
- package/public/assets/page.BAjMOHLc.js +6 -0
- package/public/assets/page.BBAy2gwn.js +1 -0
- package/public/assets/page.BFCJJrr_.js +1 -0
- package/public/assets/page.BH4eYGdz.js +1 -0
- package/public/assets/page.BMlo83oM.js +1 -0
- package/public/assets/page.BPMtBhKd.js +1 -0
- package/public/assets/page.BWmMDNX0.js +5 -0
- package/public/assets/page.B_rBjC41.js +1 -0
- package/public/assets/page.Bj0MFcSW.js +1 -0
- package/public/assets/page.Bs99xyvS.js +23 -0
- package/public/assets/page.BuU3QCO4.js +1 -0
- package/public/assets/page.C-4fur3O.js +4 -0
- package/public/assets/page.C0iX9Fnf.js +1 -0
- package/public/assets/page.C0qR8Y5R.js +1 -0
- package/public/assets/page.CAAgcGqN.js +1 -0
- package/public/assets/page.CCTU0LkA.js +1 -0
- package/public/assets/page.CS8il6ZP.js +2 -0
- package/public/assets/page.CTDNnugg.js +1 -0
- package/public/assets/page.CaujBCOC.js +1 -0
- package/public/assets/page.CcYMJEkz.js +2 -0
- package/public/assets/page.CfKwOBap.js +4 -0
- package/public/assets/page.Ct47P4hB.js +1 -0
- package/public/assets/page.CtZhMjzD.js +31 -0
- package/public/assets/page.D6H2uDVR.js +1 -0
- package/public/assets/page.D7rC6efy.js +1 -0
- package/public/assets/page.DCY06u9A.js +5 -0
- package/public/assets/page.DFzXiFFW.js +1 -0
- package/public/assets/page.DOgtsHmp.js +1 -0
- package/public/assets/page.DiFYTx8K.js +1 -0
- package/public/assets/page.Dm8AHdlz.js +1 -0
- package/public/assets/page.QIyytaMO.js +1 -0
- package/public/assets/page.jbj6FFm0.js +6 -0
- package/public/index.html +4 -4
package/dist/routes/db/route.js
CHANGED
|
@@ -55,117 +55,161 @@ export async function GET(req, res) {
|
|
|
55
55
|
return res.status(500).json({ error: "Failed to generate backup: " + error.message });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
//
|
|
58
|
+
// ─── SSE helpers for streaming restore progress ───────────────────────────────
|
|
59
|
+
function sseInit(res) {
|
|
60
|
+
res.writeHead(200, {
|
|
61
|
+
"Content-Type": "text/event-stream",
|
|
62
|
+
"Cache-Control": "no-cache, no-transform",
|
|
63
|
+
"Connection": "keep-alive",
|
|
64
|
+
"X-Accel-Buffering": "no"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function sseSend(res, event, data) {
|
|
68
|
+
res.write(`event: ${event}\n`);
|
|
69
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
70
|
+
}
|
|
71
|
+
// POST /api/db - Restore database & secrets (streaming progress)
|
|
59
72
|
export async function POST_handler(req, res) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// Switch to SSE immediately so the client sees progress from the start.
|
|
74
|
+
sseInit(res);
|
|
75
|
+
const log = (msg, level = "info") => sseSend(res, "log", { message: msg, level, ts: new Date().toISOString() });
|
|
76
|
+
const progress = (percent, label) => {
|
|
77
|
+
sseSend(res, "progress", { percent, label });
|
|
78
|
+
log(label);
|
|
79
|
+
};
|
|
80
|
+
// Run restore asynchronously; don't await the whole thing on the request.
|
|
81
|
+
(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const body = req.body;
|
|
84
|
+
progress(5, "Memvalidasi file backup...");
|
|
85
|
+
if (!body || body.signature !== "FUDROUTER_BACKUP") {
|
|
86
|
+
sseSend(res, "error", { message: "Invalid backup file signature." });
|
|
87
|
+
return res.end();
|
|
88
|
+
}
|
|
89
|
+
// Prefer logical import: raw SQLite replacement can lose rows when WAL/driver
|
|
90
|
+
// state differs between machines. Keep raw-file restore below for old backups.
|
|
91
|
+
const hasLogical = !!body.database;
|
|
92
|
+
if (hasLogical) {
|
|
93
|
+
progress(15, "Menutup koneksi database aktif...");
|
|
94
|
+
const db0 = await getAdapter();
|
|
95
|
+
if (db0 && typeof db0.close === "function") {
|
|
96
|
+
try {
|
|
97
|
+
db0.close();
|
|
98
|
+
}
|
|
99
|
+
catch { /* ignore */ }
|
|
75
100
|
}
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
progress(25, "Mengimpor data logical (provider, connections, proxy, api keys, combos)...");
|
|
102
|
+
await importDb(body.database);
|
|
103
|
+
progress(40, "Flush WAL ke file database utama (checkpoint)...");
|
|
104
|
+
const db = await getAdapter();
|
|
105
|
+
if (db && typeof db.checkpoint === "function") {
|
|
106
|
+
try {
|
|
107
|
+
db.checkpoint();
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
log(`checkpoint gagal: ${e.message}`, "warn");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (db && typeof db.exec === "function") {
|
|
114
|
+
try {
|
|
115
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
116
|
+
}
|
|
117
|
+
catch { /* ignore */ }
|
|
78
118
|
}
|
|
79
119
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
120
|
+
else {
|
|
121
|
+
progress(15, "Menutup koneksi database aktif (raw restore)...");
|
|
122
|
+
const db = await getAdapter();
|
|
123
|
+
if (db && typeof db.close === "function") {
|
|
124
|
+
try {
|
|
125
|
+
db.close();
|
|
126
|
+
}
|
|
127
|
+
catch { /* ignore */ }
|
|
83
128
|
}
|
|
84
|
-
catch { /* ignore */ }
|
|
85
129
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// 2. Remove active -wal and -shm files to prevent corruption/mismatch.
|
|
93
|
-
// On Windows these files can be locked by the OS/AV/SQLite even after
|
|
94
|
-
// db.close() (EBUSY). Use a retry + rename fallback so restore never
|
|
95
|
-
// hard-fails on a transient lock.
|
|
96
|
-
const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
|
|
97
|
-
const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
|
|
98
|
-
const safeUnlink = (file) => {
|
|
99
|
-
if (!fs.existsSync(file))
|
|
100
|
-
return;
|
|
101
|
-
// Give the DB a moment to fully release the file handle.
|
|
102
|
-
const attempts = process.platform === "win32" ? 12 : 3;
|
|
103
|
-
for (let i = 0; i < attempts; i++) {
|
|
104
|
-
try {
|
|
105
|
-
fs.unlinkSync(file);
|
|
130
|
+
// 2. Remove active -wal and -shm files to prevent corruption/mismatch.
|
|
131
|
+
progress(50, "Membersihkan file WAL/SHM lock...");
|
|
132
|
+
const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
|
|
133
|
+
const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
|
|
134
|
+
const safeUnlink = (file) => {
|
|
135
|
+
if (!fs.existsSync(file))
|
|
106
136
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
const attempts = process.platform === "win32" ? 12 : 3;
|
|
138
|
+
for (let i = 0; i < attempts; i++) {
|
|
139
|
+
try {
|
|
140
|
+
fs.unlinkSync(file);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
|
|
114
145
|
try {
|
|
115
|
-
|
|
146
|
+
const tmp = `${file}.old-${Date.now()}`;
|
|
147
|
+
fs.renameSync(file, tmp);
|
|
148
|
+
try {
|
|
149
|
+
fs.unlinkSync(tmp);
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
return;
|
|
116
153
|
}
|
|
117
|
-
catch {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
154
|
+
catch {
|
|
155
|
+
try {
|
|
156
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
157
|
+
}
|
|
158
|
+
catch { /* noop */ }
|
|
159
|
+
continue;
|
|
124
160
|
}
|
|
125
|
-
catch { /* noop */ }
|
|
126
|
-
continue;
|
|
127
161
|
}
|
|
162
|
+
throw e;
|
|
128
163
|
}
|
|
129
|
-
throw e;
|
|
130
164
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (b64Data) {
|
|
150
|
-
// Ensure directory exists
|
|
151
|
-
const dir = path.dirname(item.path);
|
|
152
|
-
if (!fs.existsSync(dir)) {
|
|
153
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
165
|
+
log(`Tidak bisa menghapus ${file} (terkunci) — melanjutkan.`, "warn");
|
|
166
|
+
};
|
|
167
|
+
safeUnlink(walFile);
|
|
168
|
+
safeUnlink(shmFile);
|
|
169
|
+
// 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
|
|
170
|
+
// logical import with a stale/incompatible SQLite file.
|
|
171
|
+
const filesToRestore = [
|
|
172
|
+
{ key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
|
|
173
|
+
{ key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
|
|
174
|
+
{ key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
|
|
175
|
+
{ key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
|
|
176
|
+
];
|
|
177
|
+
let idx = 0;
|
|
178
|
+
const total = filesToRestore.length;
|
|
179
|
+
for (const item of filesToRestore) {
|
|
180
|
+
if (hasLogical && item.key === "db/data.sqlite") {
|
|
181
|
+
idx++;
|
|
182
|
+
continue; // logical import already applied the DB
|
|
154
183
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
184
|
+
const b64Data = body.files?.[item.key];
|
|
185
|
+
const pct = 60 + Math.round((idx / total) * 35);
|
|
186
|
+
if (b64Data) {
|
|
187
|
+
progress(pct, `Menimpa file: ${item.key} ...`);
|
|
188
|
+
const dir = path.dirname(item.path);
|
|
189
|
+
if (!fs.existsSync(dir))
|
|
190
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
191
|
+
fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
|
|
192
|
+
log(`✓ Di-timpa: ${item.key}`, "info");
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
log(`Lewati (tidak ada di backup): ${item.key}`, "warn");
|
|
196
|
+
}
|
|
197
|
+
idx++;
|
|
158
198
|
}
|
|
199
|
+
progress(100, "Restore selesai. Server akan merestart...");
|
|
200
|
+
sseSend(res, "done", { success: true, message: "Restore berhasil. Server merestart..." });
|
|
201
|
+
// 4. Shut down process so PM2 restarts it (applies restored data cleanly)
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
try {
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
206
|
+
catch { /* ignore */ }
|
|
207
|
+
}, 1200);
|
|
159
208
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
catch (error) {
|
|
168
|
-
console.error("Restore error:", error);
|
|
169
|
-
return res.status(500).json({ error: "Failed to restore backup: " + error.message });
|
|
170
|
-
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error("Restore error:", error);
|
|
211
|
+
sseSend(res, "error", { message: "Restore gagal: " + (error?.message || error) });
|
|
212
|
+
res.end();
|
|
213
|
+
}
|
|
214
|
+
})();
|
|
171
215
|
}
|