backups.ts

2.8 KB · raw

1import type { Context, Hono } from "hono";2import { html } from "../pages/html.ts";3import { formatSize } from "../pages/layout.ts";4import {5	attemptBackup,6	backupBytes,7	backups,8	backupTarball,9	makeBackup,10} from "../server/backup.ts";11import { setting } from "../server/db.ts";12import { type AdminContext, adminPage, formField, notice } from "./console.ts";1314/** `20260926T031500Z` as `2026-09-26 03:15:00 UTC`. */15function backupTime(name: string): string {16	return name.replace(/^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z$/, "$1-$2-$3 $4:$5:$6 UTC");17}1819/** §24's Backups section: every kept backup, back up now, the last failure, and how to restore. */20export function backupsPage(c: Context, ctx: AdminContext, error = ""): string {21	const kept = backups(ctx.dataDir).reverse();22	const newest = kept[0];23	const failure = setting(ctx.kepr, "backup_error");24	const body = html`${notice(error)}25${failure && !error && html`<p class="issues-error mono" role="alert">The last backup failed: ${failure}</p>`}26${27	newest28		? html`<p>Last backup: ${backupTime(newest)}. <a href="/admin/backup">download backup</a></p>`29		: html`<p>No backup yet.</p>`30}31<form method="post" action="/admin/backup">${formField(c)}32<button class="issues-submit" type="submit">back up now</button></form>33<p>Kepr keeps the last 14 daily backups of its database and assets.</p>34<ul class="admin-releases mono">${kept.map(35		(name) => html`<li><a href="/admin/backup?name=${name}">${backupTime(name)}</a>36<span class="muted">${formatSize(backupBytes(ctx.dataDir, name))}</span></li>`,37	)}</ul>38<section class="repo-section"><h2 class="section-label mono">restoring one</h2>39<div class="markdown"><ol>40<li>Stop Kepr.</li>41<li>Unpack the download: it holds a folder with kepr.db and assets/.</li>42<li>Put kepr.db and assets/ in the data directory named in kepr.toml, over the old ones.</li>43<li>Start Kepr. Its first sync rebuilds the mirrors, and everyone signs in again.</li>44</ol>45<p class="muted">Backups stay on this server. Keep a copy elsewhere, along with kepr.toml.</p>46<p class="muted">Sessions and bucket credentials are not included.</p></div></section>`;47	return adminPage(c, ctx, "backups", body);48}4950export function registerBackups(app: Hono, ctx: AdminContext): void {51	app.get("/admin/backup", (c) => {52		const backup = backupTarball(ctx.dataDir, c.req.query("name"));53		if (!backup) return c.notFound();54		return c.body(backup.bytes, 200, {55			"Content-Type": "application/gzip",56			"Content-Disposition": `attachment; filename="kepr-backup-${backup.name}.tar.gz"`,57		});58	});5960	app.post("/admin/backup", (c) => {61		const failure = attemptBackup(ctx.kepr, () => makeBackup(ctx.kepr, ctx.dataDir));62		if (failure) return c.html(backupsPage(c, ctx, `The backup failed: ${failure}`), 500);63		return c.redirect("/admin?section=backups&saved=1", 303);64	});65}