SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%
5.6 KB · 179 lines typescript
Raw Blame History
1import { cp, rm, mkdir, stat, readdir } from "fs/promises";2import { existsSync, mkdirSync } from "fs";3import path from "path";4import { prisma } from "./prisma";56const SNAPSHOT_DIR = process.env.SNAPSHOT_DIR || "./snapshots";7const UPLOAD_DIR = process.env.UPLOAD_DIR || "./uploads";8const FILES_DIR = path.join(UPLOAD_DIR, "files");9const MAX_SNAPSHOTS = 7;1011if (!existsSync(SNAPSHOT_DIR)) mkdirSync(SNAPSHOT_DIR, { recursive: true });1213function getTodayKey(): string {14  return new Date().toISOString().split("T")[0]; // YYYY-MM-DD15}1617function buildLabel(dateStr: string): string {18  // dateStr = YYYY-MM-DD, parse in local noon to avoid timezone shifts19  const [y, m, d] = dateStr.split("-").map(Number);20  const date = new Date(y, m - 1, d, 12);21  return date.toLocaleDateString("fr-FR", {22    weekday: "long",23    day: "numeric",24    month: "long",25    year: "numeric",26  });27}2829async function getDirSize(dir: string): Promise<number> {30  if (!existsSync(dir)) return 0;31  let total = 0;32  const entries = await readdir(dir, { withFileTypes: true });33  for (const entry of entries) {34    const full = path.join(dir, entry.name);35    if (entry.isDirectory()) {36      total += await getDirSize(full);37    } else {38      const s = await stat(full);39      total += s.size;40    }41  }42  return total;43}4445export async function createSnapshot(): Promise<{ id: string; snapshotDate: string }> {46  const snapshotDate = getTodayKey();47  const snapshotPath = path.resolve(path.join(SNAPSHOT_DIR, snapshotDate));48  const label = buildLabel(snapshotDate);4950  // Upsert the DB record (one per day — re-snapshot replaces the old one)51  let record = await prisma.snapshot.findUnique({ where: { snapshotDate } });5253  // Remove existing snapshot dir if present54  if (existsSync(snapshotPath)) {55    await rm(snapshotPath, { recursive: true, force: true });56  }57  mkdirSync(snapshotPath, { recursive: true });5859  if (record) {60    record = await prisma.snapshot.update({61      where: { id: record.id },62      data: { status: "pending", error: null, label, path: snapshotPath, createdAt: new Date() },63    });64  } else {65    record = await prisma.snapshot.create({66      data: { label, snapshotDate, path: snapshotPath, status: "pending", size: BigInt(0) },67    });68  }6970  try {71    // Copy uploads/files72    const filesSnapshot = path.join(snapshotPath, "files");73    mkdirSync(filesSnapshot, { recursive: true });74    if (existsSync(FILES_DIR)) {75      await cp(FILES_DIR, filesSnapshot, { recursive: true });76    }7778    // Copy SQLite DB79    const dbPath = path.resolve("./prisma/dev.db");80    const dbSnapshotDir = path.join(snapshotPath, "database");81    mkdirSync(dbSnapshotDir, { recursive: true });82    if (existsSync(dbPath)) {83      await cp(dbPath, path.join(dbSnapshotDir, "dev.db"));84    }8586    const size = await getDirSize(snapshotPath);8788    await prisma.snapshot.update({89      where: { id: record.id },90      data: { status: "completed", size: BigInt(size) },91    });9293    // Keep only the last MAX_SNAPSHOTS days94    await pruneOldSnapshots();9596    return { id: record.id, snapshotDate };97  } catch (error) {98    await prisma.snapshot.update({99      where: { id: record.id },100      data: { status: "failed", error: (error as Error).message },101    });102    throw error;103  }104}105106export async function listSnapshots() {107  const snapshots = await prisma.snapshot.findMany({108    orderBy: { snapshotDate: "desc" },109    take: MAX_SNAPSHOTS,110  });111  return snapshots.map((s) => ({ ...s, size: Number(s.size) }));112}113114export async function deleteSnapshot(id: string): Promise<boolean> {115  const snapshot = await prisma.snapshot.findUnique({ where: { id } });116  if (!snapshot) return false;117  if (existsSync(snapshot.path)) {118    await rm(snapshot.path, { recursive: true, force: true });119  }120  await prisma.snapshot.delete({ where: { id } });121  return true;122}123124export async function restoreSnapshot(id: string): Promise<void> {125  const snapshot = await prisma.snapshot.findUnique({ where: { id } });126  if (!snapshot || snapshot.status !== "completed") {127    throw new Error("Snapshot introuvable ou incomplet");128  }129  if (!existsSync(snapshot.path)) {130    throw new Error("Fichiers du snapshot manquants sur le disque");131  }132133  const filesSnapshot = path.join(snapshot.path, "files");134  const dbSnapshot = path.join(snapshot.path, "database", "dev.db");135136  // Restore files dir137  if (existsSync(filesSnapshot)) {138    if (existsSync(FILES_DIR)) {139      await rm(FILES_DIR, { recursive: true, force: true });140    }141    await mkdir(FILES_DIR, { recursive: true });142    await cp(filesSnapshot, FILES_DIR, { recursive: true });143  }144145  // Restore SQLite DB146  if (existsSync(dbSnapshot)) {147    const dbPath = path.resolve("./prisma/dev.db");148    await prisma.$disconnect();149    await cp(dbSnapshot, dbPath);150    // Prisma reconnects automatically on next query151  }152}153154async function pruneOldSnapshots(): Promise<void> {155  const all = await prisma.snapshot.findMany({ orderBy: { snapshotDate: "desc" } });156  const toDelete = all.slice(MAX_SNAPSHOTS);157  for (const snapshot of toDelete) {158    if (existsSync(snapshot.path)) {159      await rm(snapshot.path, { recursive: true, force: true });160    }161    await prisma.snapshot.delete({ where: { id: snapshot.id } });162  }163}164165/**166 * Called by the server on an hourly tick.167 * Creates a snapshot only if none exists for today yet.168 * Returns true if a snapshot was created.169 */170export async function createDailySnapshotIfNeeded(): Promise<boolean> {171  const today = getTodayKey();172  const existing = await prisma.snapshot.findUnique({173    where: { snapshotDate: today },174  });175  if (existing?.status === "completed") return false;176  await createSnapshot();177  return true;178}179