Path: blob/master/src/packages/file-server/btrfs/subvolumes.ts
1539 views
import { type Filesystem } from "./filesystem";1import { subvolume, type Subvolume } from "./subvolume";2import getLogger from "@cocalc/backend/logger";3import { SNAPSHOTS } from "./subvolume-snapshots";4import { exists } from "@cocalc/backend/misc/async-utils-node";5import { join, normalize } from "path";6import { btrfs, isdir } from "./util";7import { chmod, rename, rm } from "node:fs/promises";8import { executeCode } from "@cocalc/backend/execute-code";910const RESERVED = new Set(["bup", SNAPSHOTS]);1112const logger = getLogger("file-server:btrfs:subvolumes");1314export class Subvolumes {15constructor(public filesystem: Filesystem) {}1617get = async (name: string): Promise<Subvolume> => {18if (RESERVED.has(name)) {19throw Error(`${name} is reserved`);20}21return await subvolume({ filesystem: this.filesystem, name });22};2324// create a subvolume by cloning an existing one.25clone = async (source: string, dest: string) => {26logger.debug("clone ", { source, dest });27if (RESERVED.has(dest)) {28throw Error(`${dest} is reserved`);29}30if (!(await exists(join(this.filesystem.opts.mount, source)))) {31throw Error(`subvolume ${source} does not exist`);32}33if (await exists(join(this.filesystem.opts.mount, dest))) {34throw Error(`subvolume ${dest} already exists`);35}36await btrfs({37args: [38"subvolume",39"snapshot",40join(this.filesystem.opts.mount, source),41join(this.filesystem.opts.mount, source, dest),42],43});44await rename(45join(this.filesystem.opts.mount, source, dest),46join(this.filesystem.opts.mount, dest),47);48const snapdir = join(this.filesystem.opts.mount, dest, SNAPSHOTS);49if (await exists(snapdir)) {50await chmod(snapdir, "0700");51await rm(snapdir, {52recursive: true,53force: true,54});55}56const src = await this.get(source);57const dst = await this.get(dest);58const { size } = await src.quota.get();59if (size) {60await dst.quota.set(size);61}62return dst;63};6465delete = async (name: string) => {66await btrfs({67args: ["subvolume", "delete", join(this.filesystem.opts.mount, name)],68});69};7071list = async (): Promise<string[]> => {72const { stdout } = await btrfs({73args: ["subvolume", "list", this.filesystem.opts.mount],74});75return stdout76.split("\n")77.map((x) => x.split(" ").slice(-1)[0])78.filter((x) => x)79.sort();80};8182rsync = async ({83src,84target,85args = ["-axH"],86timeout = 5 * 60 * 1000,87}: {88src: string;89target: string;90args?: string[];91timeout?: number;92}): Promise<{ stdout: string; stderr: string; exit_code: number }> => {93let srcPath = normalize(join(this.filesystem.opts.mount, src));94if (!srcPath.startsWith(this.filesystem.opts.mount)) {95throw Error("suspicious source");96}97let targetPath = normalize(join(this.filesystem.opts.mount, target));98if (!targetPath.startsWith(this.filesystem.opts.mount)) {99throw Error("suspicious target");100}101if (!srcPath.endsWith("/") && (await isdir(srcPath))) {102srcPath += "/";103if (!targetPath.endsWith("/")) {104targetPath += "/";105}106}107return await executeCode({108command: "rsync",109args: [...args, srcPath, targetPath],110err_on_exit: false,111timeout: timeout / 1000,112});113};114}115116117