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";131415function 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}181920export 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}