Path: blob/master/src/packages/file-server/btrfs/subvolume-snapshots.ts
1539 views
import { type Subvolume } from "./subvolume";1import { btrfs } from "./util";2import getLogger from "@cocalc/backend/logger";3import { join } from "path";4import { type DirectoryListingEntry } from "@cocalc/util/types";5import { SnapshotCounts, updateRollingSnapshots } from "./snapshots";67export const SNAPSHOTS = ".snapshots";8const logger = getLogger("file-server:btrfs:subvolume-snapshots");910export class SubvolumeSnapshots {11public readonly snapshotsDir: string;1213constructor(public subvolume: Subvolume) {14this.snapshotsDir = join(this.subvolume.path, SNAPSHOTS);15}1617path = (snapshot?: string, ...segments) => {18if (!snapshot) {19return SNAPSHOTS;20}21return join(SNAPSHOTS, snapshot, ...segments);22};2324private makeSnapshotsDir = async () => {25if (await this.subvolume.fs.exists(SNAPSHOTS)) {26return;27}28await this.subvolume.fs.mkdir(SNAPSHOTS);29await this.subvolume.fs.chmod(SNAPSHOTS, "0550");30};3132create = async (name?: string) => {33if (name?.startsWith(".")) {34throw Error("snapshot name must not start with '.'");35}36name ??= new Date().toISOString();37logger.debug("create", { name, subvolume: this.subvolume.name });38await this.makeSnapshotsDir();39await btrfs({40args: [41"subvolume",42"snapshot",43"-r",44this.subvolume.path,45join(this.snapshotsDir, name),46],47});48};4950ls = async (): Promise<DirectoryListingEntry[]> => {51await this.makeSnapshotsDir();52return await this.subvolume.fs.ls(SNAPSHOTS, { hidden: false });53};5455lock = async (name: string) => {56if (await this.subvolume.fs.exists(this.path(name))) {57this.subvolume.fs.writeFile(this.path(`.${name}.lock`), "");58} else {59throw Error(`snapshot ${name} does not exist`);60}61};6263unlock = async (name: string) => {64await this.subvolume.fs.rm(this.path(`.${name}.lock`));65};6667exists = async (name: string) => {68return await this.subvolume.fs.exists(this.path(name));69};7071delete = async (name) => {72if (await this.subvolume.fs.exists(this.path(`.${name}.lock`))) {73throw Error(`snapshot ${name} is locked`);74}75await btrfs({76args: ["subvolume", "delete", join(this.snapshotsDir, name)],77});78};7980// update the rolling snapshots schedule81update = async (counts?: Partial<SnapshotCounts>) => {82return await updateRollingSnapshots({ snapshots: this, counts });83};8485// has newly written changes since last snapshot86hasUnsavedChanges = async (): Promise<boolean> => {87const s = await this.ls();88if (s.length == 0) {89// more than just the SNAPSHOTS directory?90const v = await this.subvolume.fs.ls("", { hidden: true });91if (v.length == 0 || (v.length == 1 && v[0].name == SNAPSHOTS)) {92return false;93}94return true;95}96const pathGen = await getGeneration(this.subvolume.path);97const snapGen = await getGeneration(98join(this.snapshotsDir, s[s.length - 1].name),99);100return snapGen < pathGen;101};102}103104async function getGeneration(path: string): Promise<number> {105const { stdout } = await btrfs({106args: ["subvolume", "show", path],107verbose: false,108});109return parseInt(stdout.split("Generation:")[1].split("\n")[0].trim());110}111112113