@fudrouter/fsrouter 0.6.104 → 0.6.106
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/index.js +13 -0
- package/dist/lib/db/restoreRunner.js +136 -0
- package/dist/routes/db/raw/route.js +40 -0
- package/dist/routes/db/restore-path/route.js +28 -0
- package/dist/routes/db/route.js +3 -175
- package/package.json +4 -2
- package/public/.vite/manifest.json +86 -86
- package/public/assets/EndpointPageClient.Y4DlHkTu.js +6 -0
- package/public/assets/Loading.DFQ28pFE.js +1 -0
- package/public/assets/NoAuthProxyCard.CZTUlj4t.js +89 -0
- package/public/assets/index.AIBoioDI.js +89 -0
- package/public/assets/index.C--zNQ3O.css +1 -0
- package/public/assets/page.4cDBHbw-.js +6 -0
- package/public/assets/page.BKEq519M.js +4 -0
- package/public/assets/page.BP6GAYzJ.js +1 -0
- package/public/assets/page.BSn6cIN6.js +1 -0
- package/public/assets/page.BZ_cZfHH.js +1 -0
- package/public/assets/page.B_eGnZ87.js +1 -0
- package/public/assets/page.BeKSN8d5.js +1 -0
- package/public/assets/page.BrKiDQss.js +1 -0
- package/public/assets/page.C5AiGRKH.js +1 -0
- package/public/assets/page.C8zACKBM.js +1 -0
- package/public/assets/page.CJLP_zJ7.js +1 -0
- package/public/assets/page.CSrGxSfO.js +1 -0
- package/public/assets/page.CZDbQNsn.js +1 -0
- package/public/assets/page.CdAUv8An.js +2 -0
- package/public/assets/page.Ce-kykhG.js +1 -0
- package/public/assets/page.Cff3z2jC.js +1 -0
- package/public/assets/page.CinUcqhI.js +23 -0
- package/public/assets/page.Cl4BXH-q.js +6 -0
- package/public/assets/page.Cq_2LsWB.js +2 -0
- package/public/assets/page.CudzOdqp.js +1 -0
- package/public/assets/page.CyElgSss.js +5 -0
- package/public/assets/page.D9BzRCo6.js +1 -0
- package/public/assets/page.DCn1Ir9m.js +1 -0
- package/public/assets/page.DE5z4wT0.js +7 -0
- package/public/assets/page.DOB5QnMp.js +1 -0
- package/public/assets/page.DPthpQJn.js +1 -0
- package/public/assets/page.DReP7jhx.js +1 -0
- package/public/assets/page.Dd_cOSCu.js +1 -0
- package/public/assets/page.DsT3YjTA.js +31 -0
- package/public/assets/page.Dw5UwTfS.js +1 -0
- package/public/assets/page.F2ClRfJd.js +1 -0
- package/public/assets/page.NPv76Z9i.js +64 -0
- package/public/assets/page.SGPaUlyq.js +1 -0
- package/public/assets/page.hoGohFe7.js +1 -0
- package/public/assets/page.mbHhHec7.js +5 -0
- package/public/assets/page.q9OWaAVY.js +1 -0
- package/public/assets/page.zM1PvG2b.js +1 -0
- package/public/index.html +4 -4
package/dist/lib/db/index.js
CHANGED
|
@@ -80,6 +80,12 @@ export async function exportDb() {
|
|
|
80
80
|
proxyPools: db.all(`SELECT * FROM proxyPools`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, isActive: r.isActive === 1, testStatus: r.testStatus, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
81
81
|
apiKeys: db.all(`SELECT * FROM apiKeys`).map((r) => ({ id: r.id, key: r.key, name: r.name, machineId: r.machineId, isActive: r.isActive === 1, createdAt: r.createdAt })),
|
|
82
82
|
combos: db.all(`SELECT * FROM combos`).map((r) => ({ id: r.id, name: r.name, kind: r.kind, models: parseJson(r.models, []), createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
83
|
+
codebuddyAccounts: db.all(`SELECT * FROM codebuddyAccounts`).map((r) => ({
|
|
84
|
+
id: r.id, email: r.email, password: r.password, profileDir: r.profileDir,
|
|
85
|
+
fsmailAlias: r.fsmailAlias, signupMethod: r.signupMethod, apiKey: r.apiKey,
|
|
86
|
+
apiKeyStatus: r.apiKeyStatus, lastError: r.lastError, lastRunAt: r.lastRunAt,
|
|
87
|
+
createdAt: r.createdAt, provider: r.provider, canvaEnrolled: r.canvaEnrolled === 1,
|
|
88
|
+
})),
|
|
83
89
|
modelAliases: {},
|
|
84
90
|
customModels: [],
|
|
85
91
|
mitmAlias: {},
|
|
@@ -108,6 +114,7 @@ export async function importDb(payload) {
|
|
|
108
114
|
db.run(`DELETE FROM proxyPools`);
|
|
109
115
|
db.run(`DELETE FROM apiKeys`);
|
|
110
116
|
db.run(`DELETE FROM combos`);
|
|
117
|
+
db.run(`DELETE FROM codebuddyAccounts`);
|
|
111
118
|
db.run(`DELETE FROM kv WHERE scope IN ('modelAliases', 'customModels', 'mitmAlias', 'pricing')`);
|
|
112
119
|
|
|
113
120
|
// Settings
|
|
@@ -148,6 +155,12 @@ export async function importDb(payload) {
|
|
|
148
155
|
[c.id, c.name, c.kind || null, stringifyJson(c.models || []), c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
|
149
156
|
);
|
|
150
157
|
}
|
|
158
|
+
for (const a of payload.codebuddyAccounts || []) {
|
|
159
|
+
db.run(
|
|
160
|
+
`INSERT OR REPLACE INTO codebuddyAccounts(id, email, password, profileDir, fsmailAlias, signupMethod, apiKey, apiKeyStatus, lastError, lastRunAt, createdAt, provider, canvaEnrolled) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
161
|
+
[a.id, a.email || null, a.password || null, a.profileDir || null, a.fsmailAlias || null, a.signupMethod || 'google', a.apiKey || null, a.apiKeyStatus || 'pending', a.lastError || null, a.lastRunAt || null, a.createdAt || new Date().toISOString(), a.provider || 'codebuddy', a.canvaEnrolled === false ? 0 : 1]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
151
164
|
for (const [a, m] of Object.entries(payload.modelAliases || {})) {
|
|
152
165
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('modelAliases', ?, ?)`, [a, stringifyJson(m)]);
|
|
153
166
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getAdapter, resetAdapter } from "./driver.js";
|
|
2
|
+
import { importDb } from "./index.js";
|
|
3
|
+
import { DATA_DIR } from "../dataDir.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
|
|
7
|
+
// Streaming restore runner shared by POST /api/db and POST /api/db/restore-path.
|
|
8
|
+
// Sends SSE events: progress {percent,label}, log {message,level}, error, done.
|
|
9
|
+
export function runRestoreStream(res, body) {
|
|
10
|
+
res.writeHead(200, {
|
|
11
|
+
"Content-Type": "text/event-stream",
|
|
12
|
+
"Cache-Control": "no-cache, no-transform",
|
|
13
|
+
"Connection": "keep-alive",
|
|
14
|
+
"X-Accel-Buffering": "no"
|
|
15
|
+
});
|
|
16
|
+
const sseSend = (event, data) => {
|
|
17
|
+
res.write(`event: ${event}\n`);
|
|
18
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
19
|
+
};
|
|
20
|
+
const log = (msg, level = "info") => sseSend("log", { message: msg, level, ts: new Date().toISOString() });
|
|
21
|
+
const progress = (percent, label) => {
|
|
22
|
+
sseSend("progress", { percent, label });
|
|
23
|
+
log(label);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
progress(5, "Memvalidasi file backup...");
|
|
29
|
+
if (!body || body.signature !== "FUDROUTER_BACKUP") {
|
|
30
|
+
sseSend("error", { message: "Invalid backup file signature." });
|
|
31
|
+
return res.end();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasLogical = !!body.database;
|
|
35
|
+
if (hasLogical) {
|
|
36
|
+
progress(15, "Mengimpor data logical (provider, connections, proxy, api keys, combos, akun automation)...");
|
|
37
|
+
await importDb(body.database);
|
|
38
|
+
|
|
39
|
+
progress(40, "Flush WAL ke file database utama (checkpoint)...");
|
|
40
|
+
const db = await getAdapter();
|
|
41
|
+
if (db && typeof db.checkpoint === "function") {
|
|
42
|
+
try { db.checkpoint(); } catch (e) { log(`checkpoint gagal: ${e.message}`, "warn"); }
|
|
43
|
+
}
|
|
44
|
+
if (db && typeof db.exec === "function") {
|
|
45
|
+
try { db.exec("PRAGMA wal_checkpoint(TRUNCATE)"); } catch { /* ignore */ }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
progress(50, "Menutup koneksi DB agar file WAL/SHM tidak terkunci...");
|
|
49
|
+
if (db && typeof db.exec === "function") {
|
|
50
|
+
try { db.exec("PRAGMA journal_mode=DELETE"); } catch { /* ignore */ }
|
|
51
|
+
}
|
|
52
|
+
if (db && typeof db.close === "function") {
|
|
53
|
+
try { db.close(); } catch { /* ignore */ }
|
|
54
|
+
}
|
|
55
|
+
resetAdapter();
|
|
56
|
+
} else {
|
|
57
|
+
progress(15, "Menutup koneksi database aktif (raw restore)...");
|
|
58
|
+
const db = await getAdapter();
|
|
59
|
+
if (db && typeof db.close === "function") {
|
|
60
|
+
try { db.close(); } catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
resetAdapter();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
progress(50, "Membersihkan file WAL/SHM lock...");
|
|
66
|
+
const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
|
|
67
|
+
const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
|
|
68
|
+
|
|
69
|
+
const safeUnlink = (file) => {
|
|
70
|
+
if (!fs.existsSync(file)) return;
|
|
71
|
+
const attempts = process.platform === "win32" ? 12 : 3;
|
|
72
|
+
for (let i = 0; i < attempts; i++) {
|
|
73
|
+
try {
|
|
74
|
+
fs.unlinkSync(file);
|
|
75
|
+
return;
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
|
|
78
|
+
try {
|
|
79
|
+
const tmp = `${file}.old-${Date.now()}`;
|
|
80
|
+
fs.renameSync(file, tmp);
|
|
81
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
82
|
+
return;
|
|
83
|
+
} catch {
|
|
84
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); } catch { /* noop */ }
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw e;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
log(`Tidak bisa menghapus ${file} (terkunci) — melanjutkan.`, "warn");
|
|
92
|
+
};
|
|
93
|
+
safeUnlink(walFile);
|
|
94
|
+
safeUnlink(shmFile);
|
|
95
|
+
|
|
96
|
+
const filesToRestore = [
|
|
97
|
+
{ key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
|
|
98
|
+
{ key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
|
|
99
|
+
{ key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
|
|
100
|
+
{ key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
let idx = 0;
|
|
104
|
+
const total = filesToRestore.length;
|
|
105
|
+
for (const item of filesToRestore) {
|
|
106
|
+
if (hasLogical && item.key === "db/data.sqlite") {
|
|
107
|
+
idx++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const b64Data = body.files?.[item.key];
|
|
111
|
+
const pct = 60 + Math.round((idx / total) * 35);
|
|
112
|
+
if (b64Data) {
|
|
113
|
+
progress(pct, `Menimpa file: ${item.key} ...`);
|
|
114
|
+
const dir = path.dirname(item.path);
|
|
115
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
116
|
+
fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
|
|
117
|
+
log(`✓ Di-timpa: ${item.key}`, "info");
|
|
118
|
+
} else {
|
|
119
|
+
log(`Lewati (tidak ada di backup): ${item.key}`, "warn");
|
|
120
|
+
}
|
|
121
|
+
idx++;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
progress(100, "Restore selesai. Server akan merestart...");
|
|
125
|
+
sseSend("done", { success: true, message: "Restore berhasil. Server merestart..." });
|
|
126
|
+
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
try { process.exit(0); } catch { /* ignore */ }
|
|
129
|
+
}, 1200);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error("Restore error:", error);
|
|
132
|
+
sseSend("error", { message: "Restore gagal: " + (error?.message || error) });
|
|
133
|
+
res.end();
|
|
134
|
+
}
|
|
135
|
+
})();
|
|
136
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getAdapter } from "../../../lib/db/driver.js";
|
|
2
|
+
import { DATA_DIR } from "../../../lib/dataDir.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
// GET /api/db/raw - Backup the raw SQLite database file (copy as-is)
|
|
7
|
+
export async function GET(req, res) {
|
|
8
|
+
try {
|
|
9
|
+
const db = await getAdapter();
|
|
10
|
+
// Flush WAL into the main file so the copied .sqlite is self-contained.
|
|
11
|
+
if (db && typeof db.checkpoint === "function") {
|
|
12
|
+
try {
|
|
13
|
+
db.checkpoint();
|
|
14
|
+
}
|
|
15
|
+
catch { /* ignore */ }
|
|
16
|
+
}
|
|
17
|
+
if (db && typeof db.exec === "function") {
|
|
18
|
+
try {
|
|
19
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore */ }
|
|
22
|
+
}
|
|
23
|
+
const sqlitePath = path.join(DATA_DIR, "db", "data.sqlite");
|
|
24
|
+
if (!fs.existsSync(sqlitePath)) {
|
|
25
|
+
return res.status(404).json({ error: "Database file not found." });
|
|
26
|
+
}
|
|
27
|
+
res.setHeader("Content-Disposition", 'attachment; filename="fsrouter-db-raw.sqlite"');
|
|
28
|
+
res.setHeader("Content-Type", "application/octet-stream");
|
|
29
|
+
const stat = fs.statSync(sqlitePath);
|
|
30
|
+
res.setHeader("Content-Length", stat.size);
|
|
31
|
+
const stream = fs.createReadStream(sqlitePath);
|
|
32
|
+
stream.on("error", (e) => { console.error("Raw backup stream error:", e); });
|
|
33
|
+
stream.pipe(res);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error("Raw backup error:", error);
|
|
37
|
+
if (!res.headersSent)
|
|
38
|
+
res.status(500).json({ error: "Failed to generate raw backup: " + error.message });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { runRestoreStream } from "../../../lib/db/restoreRunner.js";
|
|
3
|
+
export const dynamic = "force-dynamic";
|
|
4
|
+
// POST /api/db/restore-path - Restore from a file path on the server itself
|
|
5
|
+
// (e.g. user pasted C:\Users\fud\backups\fsrouter-backup.fud, or a raw .sqlite)
|
|
6
|
+
export async function POST(req, res) {
|
|
7
|
+
const { path: filePath } = req.body || {};
|
|
8
|
+
if (!filePath || typeof filePath !== "string") {
|
|
9
|
+
return res.status(400).json({ error: "Field 'path' wajib diisi." });
|
|
10
|
+
}
|
|
11
|
+
if (!fs.existsSync(filePath)) {
|
|
12
|
+
return res.status(404).json({ error: `File tidak ditemukan: ${filePath}` });
|
|
13
|
+
}
|
|
14
|
+
let body;
|
|
15
|
+
try {
|
|
16
|
+
if (filePath.endsWith(".sqlite")) {
|
|
17
|
+
const b64 = fs.readFileSync(filePath).toString("base64");
|
|
18
|
+
body = { signature: "FUDROUTER_BACKUP", version: 1, files: { "db/data.sqlite": b64 } };
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
body = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return res.status(400).json({ error: "Gagal membaca/mem-parse file: " + e.message });
|
|
26
|
+
}
|
|
27
|
+
runRestoreStream(res, body);
|
|
28
|
+
}
|
package/dist/routes/db/route.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { DATA_DIR } from "../../lib/dataDir.js";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import fs from "fs";
|
|
1
|
+
import { exportDb } from "../../lib/db/index.js";
|
|
2
|
+
import { runRestoreStream } from "../../lib/db/restoreRunner.js";
|
|
6
3
|
export const dynamic = "force-dynamic";
|
|
7
4
|
// GET /api/db - Backup database & secrets
|
|
8
5
|
export async function GET(req, res) {
|
|
@@ -55,176 +52,7 @@ export async function GET(req, res) {
|
|
|
55
52
|
return res.status(500).json({ error: "Failed to generate backup: " + error.message });
|
|
56
53
|
}
|
|
57
54
|
}
|
|
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
55
|
// POST /api/db - Restore database & secrets (streaming progress)
|
|
72
56
|
export async function POST_handler(req, res) {
|
|
73
|
-
|
|
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.
|
|
95
|
-
await importDb(body.database);
|
|
96
|
-
progress(40, "Flush WAL ke file database utama (checkpoint)...");
|
|
97
|
-
const db = await getAdapter();
|
|
98
|
-
if (db && typeof db.checkpoint === "function") {
|
|
99
|
-
try {
|
|
100
|
-
db.checkpoint();
|
|
101
|
-
}
|
|
102
|
-
catch (e) {
|
|
103
|
-
log(`checkpoint gagal: ${e.message}`, "warn");
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (db && typeof db.exec === "function") {
|
|
107
|
-
try {
|
|
108
|
-
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
109
|
-
}
|
|
110
|
-
catch { /* ignore */ }
|
|
111
|
-
}
|
|
112
|
-
progress(50, "Menutup koneksi DB agar file WAL/SHM tidak terkunci...");
|
|
113
|
-
// Switch journal mode to DELETE so SQLite folds the WAL back into the
|
|
114
|
-
// main db file and deletes -wal/-shm itself — avoids leaving a locked
|
|
115
|
-
// WAL that Windows can't unlink (which previously dropped restored rows).
|
|
116
|
-
if (db && typeof db.exec === "function") {
|
|
117
|
-
try {
|
|
118
|
-
db.exec("PRAGMA journal_mode=DELETE");
|
|
119
|
-
}
|
|
120
|
-
catch { /* ignore */ }
|
|
121
|
-
}
|
|
122
|
-
// Close the connection so Windows releases the -wal/-shm file locks.
|
|
123
|
-
// Logical data is already flushed to the main DB via the checkpoint above.
|
|
124
|
-
if (db && typeof db.close === "function") {
|
|
125
|
-
try {
|
|
126
|
-
db.close();
|
|
127
|
-
}
|
|
128
|
-
catch { /* ignore */ }
|
|
129
|
-
}
|
|
130
|
-
// Clear the cached instance so getAdapter() re-opens fresh on next boot.
|
|
131
|
-
resetAdapter();
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
progress(15, "Menutup koneksi database aktif (raw restore)...");
|
|
135
|
-
const db = await getAdapter();
|
|
136
|
-
if (db && typeof db.close === "function") {
|
|
137
|
-
try {
|
|
138
|
-
db.close();
|
|
139
|
-
}
|
|
140
|
-
catch { /* ignore */ }
|
|
141
|
-
}
|
|
142
|
-
// Reset cached instance so the next getAdapter() re-opens a fresh connection.
|
|
143
|
-
resetAdapter();
|
|
144
|
-
}
|
|
145
|
-
// 2. Remove active -wal and -shm files to prevent corruption/mismatch.
|
|
146
|
-
progress(50, "Membersihkan file WAL/SHM lock...");
|
|
147
|
-
const walFile = path.join(DATA_DIR, "db", "data.sqlite-wal");
|
|
148
|
-
const shmFile = path.join(DATA_DIR, "db", "data.sqlite-shm");
|
|
149
|
-
const safeUnlink = (file) => {
|
|
150
|
-
if (!fs.existsSync(file))
|
|
151
|
-
return;
|
|
152
|
-
const attempts = process.platform === "win32" ? 12 : 3;
|
|
153
|
-
for (let i = 0; i < attempts; i++) {
|
|
154
|
-
try {
|
|
155
|
-
fs.unlinkSync(file);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
catch (e) {
|
|
159
|
-
if (e.code === "EBUSY" || e.code === "EPERM" || e.code === "ETXTBSY") {
|
|
160
|
-
try {
|
|
161
|
-
const tmp = `${file}.old-${Date.now()}`;
|
|
162
|
-
fs.renameSync(file, tmp);
|
|
163
|
-
try {
|
|
164
|
-
fs.unlinkSync(tmp);
|
|
165
|
-
}
|
|
166
|
-
catch { /* ignore */ }
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
catch {
|
|
170
|
-
try {
|
|
171
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
172
|
-
}
|
|
173
|
-
catch { /* noop */ }
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
throw e;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
log(`Tidak bisa menghapus ${file} (terkunci) — melanjutkan.`, "warn");
|
|
181
|
-
};
|
|
182
|
-
safeUnlink(walFile);
|
|
183
|
-
safeUnlink(shmFile);
|
|
184
|
-
// 3. Restore secrets/raw DB only for legacy backups. Do not overwrite the
|
|
185
|
-
// logical import with a stale/incompatible SQLite file.
|
|
186
|
-
const filesToRestore = [
|
|
187
|
-
{ key: "db/data.sqlite", path: path.join(DATA_DIR, "db", "data.sqlite") },
|
|
188
|
-
{ key: "machine-id", path: path.join(DATA_DIR, "machine-id") },
|
|
189
|
-
{ key: "jwt-secret", path: path.join(DATA_DIR, "jwt-secret") },
|
|
190
|
-
{ key: "auth/cli-secret", path: path.join(DATA_DIR, "auth", "cli-secret") }
|
|
191
|
-
];
|
|
192
|
-
let idx = 0;
|
|
193
|
-
const total = filesToRestore.length;
|
|
194
|
-
for (const item of filesToRestore) {
|
|
195
|
-
if (hasLogical && item.key === "db/data.sqlite") {
|
|
196
|
-
idx++;
|
|
197
|
-
continue; // logical import already applied the DB
|
|
198
|
-
}
|
|
199
|
-
const b64Data = body.files?.[item.key];
|
|
200
|
-
const pct = 60 + Math.round((idx / total) * 35);
|
|
201
|
-
if (b64Data) {
|
|
202
|
-
progress(pct, `Menimpa file: ${item.key} ...`);
|
|
203
|
-
const dir = path.dirname(item.path);
|
|
204
|
-
if (!fs.existsSync(dir))
|
|
205
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
206
|
-
fs.writeFileSync(item.path, Buffer.from(b64Data, "base64"));
|
|
207
|
-
log(`✓ Di-timpa: ${item.key}`, "info");
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
log(`Lewati (tidak ada di backup): ${item.key}`, "warn");
|
|
211
|
-
}
|
|
212
|
-
idx++;
|
|
213
|
-
}
|
|
214
|
-
progress(100, "Restore selesai. Server akan merestart...");
|
|
215
|
-
sseSend(res, "done", { success: true, message: "Restore berhasil. Server merestart..." });
|
|
216
|
-
// 4. Shut down process so PM2 restarts it (applies restored data cleanly)
|
|
217
|
-
setTimeout(() => {
|
|
218
|
-
try {
|
|
219
|
-
process.exit(0);
|
|
220
|
-
}
|
|
221
|
-
catch { /* ignore */ }
|
|
222
|
-
}, 1200);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
console.error("Restore error:", error);
|
|
226
|
-
sseSend(res, "error", { message: "Restore gagal: " + (error?.message || error) });
|
|
227
|
-
res.end();
|
|
228
|
-
}
|
|
229
|
-
})();
|
|
57
|
+
runRestoreStream(res, req.body);
|
|
230
58
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fudrouter/fsrouter",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.106",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "9Router v2 — Express Backend (API, SSE, DB, Auth, MITM)",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"bcryptjs": "^3.0.3",
|
|
24
|
-
"better-sqlite3": "^12.11.1",
|
|
25
24
|
"confbox": "^0.2.4",
|
|
26
25
|
"cookie-parser": "^1.4.7",
|
|
27
26
|
"cors": "^2.8.5",
|
|
@@ -44,5 +43,8 @@
|
|
|
44
43
|
"@/*": "./dist/*",
|
|
45
44
|
"open-sse": "./open-sse/index.js",
|
|
46
45
|
"open-sse/*": "./open-sse/*"
|
|
46
|
+
},
|
|
47
|
+
"optionalDependencies": {
|
|
48
|
+
"better-sqlite3": "^12.11.1"
|
|
47
49
|
}
|
|
48
50
|
}
|