feat(settings): backup/restore de la base de données (amelioration #4)
- service dbBackup : createBackup (VACUUM INTO → archive .db cohérente), validateSqlite (header + integrity_check + schéma), prepareRestore (sauvegarde de sécurité auto + dépôt <db>.incoming) - swap hors-ligne au démarrage (db/client.ts) : aucune corruption d'une base ouverte ; restauration appliquée au redémarrage - routes GET /system/db/info|backup, POST /system/db/restore - lib api : dbInfo / dbBackup (download navigateur) / dbRestore (upload) - SettingsModal : onglet « Base de données » (taille, télécharger, restaurer avec confirmation Popup), icônes database/upload, styles DS variables only Testé end-to-end : backup 184 Ko valide, restore + safety .bak + swap au boot, fichier invalide rejeté. tsc 0 erreur · 91 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
// server/routes/db.ts
|
||||
import { Hono } from "hono";
|
||||
import { createBackup, prepareRestore, dbInfo } from "../services/dbBackup.js";
|
||||
|
||||
export const dbRoutes = new Hono();
|
||||
|
||||
// Métadonnées de la base (taille, date, restauration en attente).
|
||||
dbRoutes.get("/info", (c) => c.json(dbInfo()));
|
||||
|
||||
// Télécharge une archive cohérente de la base courante.
|
||||
dbRoutes.get("/backup", () => {
|
||||
const { buffer, filename } = createBackup();
|
||||
return new Response(new Uint8Array(buffer), {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
"Content-Length": String(buffer.length),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Restaure depuis une archive uploadée (corps brut). Appliquée au prochain démarrage.
|
||||
dbRoutes.post("/restore", async (c) => {
|
||||
try {
|
||||
const ab = await c.req.arrayBuffer();
|
||||
if (!ab.byteLength) return c.json({ error: "Archive vide" }, 400);
|
||||
const { safetyBackup } = prepareRestore(Buffer.from(ab));
|
||||
return c.json({
|
||||
ok: true,
|
||||
restartRequired: true,
|
||||
safetyBackup,
|
||||
message: "Restauration préparée. Redémarrez le serveur pour l'appliquer.",
|
||||
});
|
||||
} catch (err) {
|
||||
return c.json({ error: (err as Error).message }, 400);
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { Hono } from "hono";
|
||||
import { machinesRoutes } from "./machines.js";
|
||||
import { actionsRoutes } from "./actions.js";
|
||||
import { actionRequestsRoutes } from "./actionRequests.js";
|
||||
import { dbRoutes } from "./db.js";
|
||||
import { getServerCapabilities } from "../services/capabilities.js";
|
||||
import { getSystemMetrics, getSystemStatus } from "../services/system.js";
|
||||
|
||||
@@ -10,6 +11,7 @@ export const api = new Hono();
|
||||
api.get("/capabilities", (c) => c.json(getServerCapabilities()));
|
||||
api.get("/system/status", (c) => c.json(getSystemStatus()));
|
||||
api.get("/system/metrics", (c) => c.json(getSystemMetrics()));
|
||||
api.route("/system/db", dbRoutes);
|
||||
api.route("/machines", machinesRoutes);
|
||||
api.route("/machines", actionsRoutes);
|
||||
api.route("/", actionRequestsRoutes);
|
||||
|
||||
Reference in New Issue
Block a user