Path: blob/master/src/packages/file-server/zfs/backup.ts
1447 views
/*1Make backups using bup.2*/34import { bupFilesystemMountpoint, filesystemSnapshotMountpoint } from "./names";5import { get, getRecent, set } from "./db";6import { exec } from "./util";7import getLogger from "@cocalc/backend/logger";8import { exists } from "@cocalc/backend/misc/async-utils-node";9import { join } from "path";10import { mountFilesystem } from "./properties";11import { split } from "@cocalc/util/misc";12import { BUP_INTERVAL_MS } from "./config";13import { primaryKey, type PrimaryKey } from "./types";14import { createSnapshot } from "./snapshots";1516const logger = getLogger("file-server:zfs:backup");1718const EXCLUDES = [".conda", ".npm", "cache", ".julia", ".local/share/pnpm"];1920export async function createBackup(21fs: PrimaryKey,22): Promise<{ BUP_DIR: string }> {23const pk = primaryKey(fs);24logger.debug("createBackup", pk);25const filesystem = get(pk);26await mountFilesystem(pk);27const snapshot = await createSnapshot({ ...filesystem, ifChanged: true });28const mountpoint = filesystemSnapshotMountpoint({ ...filesystem, snapshot });29const excludes: string[] = [];30for (const path of EXCLUDES) {31excludes.push("--exclude");32excludes.push(join(mountpoint, path));33}34logger.debug("createBackup: index", pk);35const BUP_DIR = bupFilesystemMountpoint(filesystem);36if (!(await exists(BUP_DIR))) {37await exec({38verbose: true,39command: "sudo",40args: ["mkdir", "-p", BUP_DIR],41what: { ...pk, desc: "make bup repo" },42});43await exec({44verbose: true,45command: "sudo",46args: ["bup", "-d", BUP_DIR, "init"],47what: { ...pk, desc: "bup init" },48});49}50await exec({51verbose: true,52env: { BUP_DIR },53command: "sudo",54args: [55"--preserve-env",56"bup",57"index",58...excludes,59"-x",60mountpoint,61"--no-check-device",62],63what: { ...pk, desc: "creating bup index" },64});65logger.debug("createBackup: save", pk);66await exec({67verbose: true,68env: { BUP_DIR },69command: "sudo",70args: [71"--preserve-env",72"bup",73"save",74"-q",75"--strip",76"-n",77"master",78mountpoint,79],80what: { ...pk, desc: "save new bup snapshot" },81});8283const { stdout } = await exec({84env: { BUP_DIR },85command: "sudo",86args: ["--preserve-env", "bup", "ls", "master"],87what: { ...pk, desc: "getting name of backup" },88});89const v = split(stdout);90const last_bup_backup = v[v.length - 2];91logger.debug("createBackup: created ", { last_bup_backup });92set({ ...pk, last_bup_backup });9394// prune-older --unsafe --keep-all-for 8d --keep-dailies-for 4w --keep-monthlies-for 6m --keep-yearlies-for 10y95logger.debug("createBackup: prune", pk);96await exec({97verbose: true,98env: { BUP_DIR },99command: "sudo",100args: [101"--preserve-env",102"bup",103"prune-older",104"--unsafe",105"--keep-all-for",106"8d",107"--keep-dailies-for",108"4w",109"--keep-monthlies-for",110"6m",111"--keep-yearlies-for",112"5y",113],114what: { ...pk, desc: "save new bup snapshot" },115});116117return { BUP_DIR };118}119120// Go through ALL filesystems with last_edited >= cutoff and make a bup121// backup if they are due.122// cutoff = a Date (default = 1 week ago)123export async function maintainBackups(cutoff?: Date) {124logger.debug("backupActiveFilesystems: getting...");125const v = getRecent({ cutoff });126logger.debug(127`backupActiveFilesystems: considering ${v.length} filesystems`,128cutoff,129);130let i = 0;131for (const { archived, last_edited, last_bup_backup, ...pk } of v) {132if (archived || !last_edited) {133continue;134}135const age =136new Date(last_edited).valueOf() - bupToDate(last_bup_backup).valueOf();137if (age < BUP_INTERVAL_MS) {138// there's a new backup already139continue;140}141try {142await createBackup(pk);143} catch (err) {144logger.debug(`backupActiveFilesystems: error -- ${err}`);145}146i += 1;147if (i % 10 == 0) {148logger.debug(`backupActiveFilesystems: ${i}/${v.length}`);149}150}151}152153function bupToDate(dateString?: string): Date {154if (!dateString) {155return new Date(0);156}157// Extract components using regular expression158const match = dateString.match(159/^(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})$/,160);161162if (match) {163const [_, year, month, day, hour, minute, second] = match; // Destructure components164return new Date(165parseInt(year),166parseInt(month) - 1,167parseInt(day),168parseInt(hour),169parseInt(minute),170parseInt(second),171);172} else {173throw Error("Invalid bup date format");174}175}176177178