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