Path: blob/master/src/packages/file-server/btrfs/filesystem.ts
1539 views
/*1BTRFS Filesystem23DEVELOPMENT:45Start node, then:67DEBUG="cocalc:*file-server*" DEBUG_CONSOLE=yes node89a = require('@cocalc/file-server/btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})1011*/1213import refCache from "@cocalc/util/refcache";14import { mkdirp, btrfs, sudo } from "./util";15import { join } from "path";16import { Subvolumes } from "./subvolumes";17import { mkdir } from "fs/promises";18import { exists } from "@cocalc/backend/misc/async-utils-node";19import { executeCode } from "@cocalc/backend/execute-code";2021// default size of btrfs filesystem if creating an image file.22const DEFAULT_FILESYSTEM_SIZE = "10G";2324// default for newly created subvolumes25export const DEFAULT_SUBVOLUME_SIZE = "1G";2627const MOUNT_ERROR = "wrong fs type, bad option, bad superblock";2829export interface Options {30// the underlying block device.31// If this is a file (or filename) ending in .img, then it's a sparse file mounted as a loopback device.32// If this starts with "/dev" then it is a raw block device.33device: string;34// formatIfNeeded -- DANGEROUS! if true, format the device or image,35// if it doesn't mount with an error containing "wrong fs type,36// bad option, bad superblock". Never use this in production. Useful37// for testing and dev.38formatIfNeeded?: boolean;39// where the btrfs filesystem is mounted40mount: string;4142// default size of newly created subvolumes43defaultSize?: string | number;44defaultFilesystemSize?: string | number;45}4647export class Filesystem {48public readonly opts: Options;49public readonly bup: string;50public readonly subvolumes: Subvolumes;5152constructor(opts: Options) {53opts = {54defaultSize: DEFAULT_SUBVOLUME_SIZE,55defaultFilesystemSize: DEFAULT_FILESYSTEM_SIZE,56...opts,57};58this.opts = opts;59this.bup = join(this.opts.mount, "bup");60this.subvolumes = new Subvolumes(this);61}6263init = async () => {64await mkdirp([this.opts.mount]);65await this.initDevice();66await this.mountFilesystem();67await btrfs({68args: ["quota", "enable", "--simple", this.opts.mount],69});70await this.initBup();71};7273unmount = async () => {74await sudo({75command: "umount",76args: [this.opts.mount],77err_on_exit: true,78});79};8081close = () => {};8283private initDevice = async () => {84if (!isImageFile(this.opts.device)) {85// raw block device -- nothing to do86return;87}88if (!(await exists(this.opts.device))) {89await sudo({90command: "truncate",91args: ["-s", `${this.opts.defaultFilesystemSize}`, this.opts.device],92});93}94};9596info = async (): Promise<{ [field: string]: string }> => {97const { stdout } = await btrfs({98args: ["subvolume", "show", this.opts.mount],99});100const obj: { [field: string]: string } = {};101for (const x of stdout.split("\n")) {102const i = x.indexOf(":");103if (i == -1) continue;104obj[x.slice(0, i).trim()] = x.slice(i + 1).trim();105}106return obj;107};108109private mountFilesystem = async () => {110try {111await this.info();112// already mounted113return;114} catch {}115const { stderr, exit_code } = await this._mountFilesystem();116if (exit_code) {117if (stderr.includes(MOUNT_ERROR)) {118if (this.opts.formatIfNeeded) {119await this.formatDevice();120const { stderr, exit_code } = await this._mountFilesystem();121if (exit_code) {122throw Error(stderr);123} else {124return;125}126}127}128throw Error(stderr);129}130};131132private formatDevice = async () => {133await sudo({ command: "mkfs.btrfs", args: [this.opts.device] });134};135136private _mountFilesystem = async () => {137const args: string[] = isImageFile(this.opts.device) ? ["-o", "loop"] : [];138args.push(139"-o",140"compress=zstd",141"-o",142"noatime",143"-o",144"space_cache=v2",145"-o",146"autodefrag",147this.opts.device,148"-t",149"btrfs",150this.opts.mount,151);152{153const { stderr, exit_code } = await sudo({154command: "mount",155args,156err_on_exit: false,157});158if (exit_code) {159return { stderr, exit_code };160}161}162const { stderr, exit_code } = await sudo({163command: "chown",164args: [165`${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`,166this.opts.mount,167],168err_on_exit: false,169});170return { stderr, exit_code };171};172173private initBup = async () => {174if (!(await exists(this.bup))) {175await mkdir(this.bup);176}177await executeCode({178command: "bup",179args: ["init"],180env: { BUP_DIR: this.bup },181});182};183}184185function isImageFile(name: string) {186if (name.startsWith("/dev")) {187return false;188}189// TODO: could probably check os for a device with given name?190return name.endsWith(".img");191}192193const cache = refCache<Options & { noCache?: boolean }, Filesystem>({194name: "btrfs-filesystems",195createObject: async (options: Options) => {196const filesystem = new Filesystem(options);197await filesystem.init();198return filesystem;199},200});201202export async function filesystem(203options: Options & { noCache?: boolean },204): Promise<Filesystem> {205return await cache(options);206}207208209