@fudrouter/fsrouter 0.6.99 → 0.6.101
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/lib/db/driver.js +8 -0
- package/dist/routes/db/route.js +139 -98
- 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/lib/db/driver.js
CHANGED
|
@@ -79,6 +79,14 @@ export async function getAdapter() {
|
|
|
79
79
|
return state.initPromise;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Force re-open the underlying connection (used after an explicit close, e.g.
|
|
83
|
+
// during raw DB restore). Without this, getAdapter() would return the stale
|
|
84
|
+
// closed instance and every subsequent query throws "connection is not open".
|
|
85
|
+
export function resetAdapter() {
|
|
86
|
+
state.instance = null;
|
|
87
|
+
state.initPromise = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
export function getAdapterSync() {
|
|
83
91
|
if (!state.instance) throw new Error("[DB] adapter not initialized — await getAdapter() first");
|
|
84
92
|
return state.instance;
|
package/dist/routes/db/route.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAdapter } from "../../lib/db/driver.js";
|
|
1
|
+
import { getAdapter, resetAdapter } from "../../lib/db/driver.js";
|
|
2
2
|
import { exportDb, importDb } from "../../lib/db/index.js";
|
|
3
3
|
import { DATA_DIR } from "../../lib/dataDir.js";
|
|
4
4
|
import path from "path";
|
|
@@ -55,117 +55,158 @@ 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, "Mengimpor data logical (provider, connections, proxy, api keys, combos)...");
|
|
94
|
+
// Import on the live connection — do NOT close it first, or getAdapter()
|
|
95
|
+
// would keep returning the stale closed instance and every query throws
|
|
96
|
+
// "The database connection is not open".
|
|
97
|
+
await importDb(body.database);
|
|
98
|
+
progress(40, "Flush WAL ke file database utama (checkpoint)...");
|
|
99
|
+
const db = await getAdapter();
|
|
100
|
+
if (db && typeof db.checkpoint === "function") {
|
|
101
|
+
try {
|
|
102
|
+
db.checkpoint();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
log(`checkpoint gagal: ${e.message}`, "warn");
|
|
106
|
+
}
|
|
75
107
|
}
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
if (db && typeof db.exec === "function") {
|
|
109
|
+
try {
|
|
110
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
111
|
+
}
|
|
112
|
+
catch { /* ignore */ }
|
|
78
113
|
}
|
|
79
114
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
else {
|
|
116
|
+
progress(15, "Menutup koneksi database aktif (raw restore)...");
|
|
117
|
+
const db = await getAdapter();
|
|
118
|
+
if (db && typeof db.close === "function") {
|
|
119
|
+
try {
|
|
120
|
+
db.close();
|
|
121
|
+
}
|
|
122
|
+
catch { /* ignore */ }
|
|
83
123
|
}
|
|
84
|
-
|
|
124
|
+
// Reset cached instance so the next getAdapter() re-opens a fresh connection.
|
|
125
|
+
resetAdapter();
|
|
85
126
|
}
|
|
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);
|
|
127
|
+
// 2. Remove active -wal and -shm files to prevent corruption/mismatch.
|
|
128
|
+
progress(50, "Membersihkan file WAL/SHM lock...");
|
|
129
|
+
const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
|
|
130
|
+
const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
|
|
131
|
+
const safeUnlink = (file) => {
|
|
132
|
+
if (!fs.existsSync(file))
|
|
106
133
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
const attempts = process.platform === "win32" ? 12 : 3;
|
|
135
|
+
for (let i = 0; i < attempts; i++) {
|
|
136
|
+
try {
|
|
137
|
+
fs.unlinkSync(file);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
|
|
114
142
|
try {
|
|
115
|
-
|
|
143
|
+
const tmp = `${file}.old-${Date.now()}`;
|
|
144
|
+
fs.renameSync(file, tmp);
|
|
145
|
+
try {
|
|
146
|
+
fs.unlinkSync(tmp);
|
|
147
|
+
}
|
|
148
|
+
catch { /* ignore */ }
|
|
149
|
+
return;
|
|
116
150
|
}
|
|
117
|
-
catch {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
151
|
+
catch {
|
|
152
|
+
try {
|
|
153
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
154
|
+
}
|
|
155
|
+
catch { /* noop */ }
|
|
156
|
+
continue;
|
|
124
157
|
}
|
|
125
|
-
catch { /* noop */ }
|
|
126
|
-
continue;
|
|
127
158
|
}
|
|
159
|
+
throw e;
|
|
128
160
|
}
|
|
129
|
-
throw e;
|
|
130
161
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
|
|
162
|
+
log(`Tidak bisa menghapus ${file} (terkunci) — melanjutkan.`, "warn");
|
|
163
|
+
};
|
|
164
|
+
safeUnlink(walFile);
|
|
165
|
+
safeUnlink(shmFile);
|
|
166
|
+
// 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
|
|
167
|
+
// logical import with a stale/incompatible SQLite file.
|
|
168
|
+
const filesToRestore = [
|
|
169
|
+
{ key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
|
|
170
|
+
{ key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
|
|
171
|
+
{ key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
|
|
172
|
+
{ key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
|
|
173
|
+
];
|
|
174
|
+
let idx = 0;
|
|
175
|
+
const total = filesToRestore.length;
|
|
176
|
+
for (const item of filesToRestore) {
|
|
177
|
+
if (hasLogical && item.key === "db/data.sqlite") {
|
|
178
|
+
idx++;
|
|
179
|
+
continue; // logical import already applied the DB
|
|
180
|
+
}
|
|
181
|
+
const b64Data = body.files?.[item.key];
|
|
182
|
+
const pct = 60 + Math.round((idx / total) * 35);
|
|
183
|
+
if (b64Data) {
|
|
184
|
+
progress(pct, `Menimpa file: ${item.key} ...`);
|
|
185
|
+
const dir = path.dirname(item.path);
|
|
186
|
+
if (!fs.existsSync(dir))
|
|
187
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
|
+
fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
|
|
189
|
+
log(`✓ Di-timpa: ${item.key}`, "info");
|
|
154
190
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
191
|
+
else {
|
|
192
|
+
log(`Lewati (tidak ada di backup): ${item.key}`, "warn");
|
|
193
|
+
}
|
|
194
|
+
idx++;
|
|
158
195
|
}
|
|
196
|
+
progress(100, "Restore selesai. Server akan merestart...");
|
|
197
|
+
sseSend(res, "done", { success: true, message: "Restore berhasil. Server merestart..." });
|
|
198
|
+
// 4. Shut down process so PM2 restarts it (applies restored data cleanly)
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
try {
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
catch { /* ignore */ }
|
|
204
|
+
}, 1200);
|
|
159
205
|
}
|
|
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
|
-
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error("Restore error:", error);
|
|
208
|
+
sseSend(res, "error", { message: "Restore gagal: " + (error?.message || error) });
|
|
209
|
+
res.end();
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
171
212
|
}
|