Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/file-server/btrfs/filesystem.ts
1539 views
1
/*
2
BTRFS Filesystem
3
4
DEVELOPMENT:
5
6
Start node, then:
7
8
DEBUG="cocalc:*file-server*" DEBUG_CONSOLE=yes node
9
10
a = require('@cocalc/file-server/btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})
11
12
*/
13
14
import refCache from "@cocalc/util/refcache";
15
import { mkdirp, btrfs, sudo } from "./util";
16
import { join } from "path";
17
import { Subvolumes } from "./subvolumes";
18
import { mkdir } from "fs/promises";
19
import { exists } from "@cocalc/backend/misc/async-utils-node";
20
import { executeCode } from "@cocalc/backend/execute-code";
21
22
// default size of btrfs filesystem if creating an image file.
23
const DEFAULT_FILESYSTEM_SIZE = "10G";
24
25
// default for newly created subvolumes
26
export const DEFAULT_SUBVOLUME_SIZE = "1G";
27
28
const MOUNT_ERROR = "wrong fs type, bad option, bad superblock";
29
30
export interface Options {
31
// the underlying block device.
32
// If this is a file (or filename) ending in .img, then it's a sparse file mounted as a loopback device.
33
// If this starts with "/dev" then it is a raw block device.
34
device: string;
35
// formatIfNeeded -- DANGEROUS! if true, format the device or image,
36
// if it doesn't mount with an error containing "wrong fs type,
37
// bad option, bad superblock". Never use this in production. Useful
38
// for testing and dev.
39
formatIfNeeded?: boolean;
40
// where the btrfs filesystem is mounted
41
mount: string;
42
43
// default size of newly created subvolumes
44
defaultSize?: string | number;
45
defaultFilesystemSize?: string | number;
46
}
47
48
export class Filesystem {
49
public readonly opts: Options;
50
public readonly bup: string;
51
public readonly subvolumes: Subvolumes;
52
53
constructor(opts: Options) {
54
opts = {
55
defaultSize: DEFAULT_SUBVOLUME_SIZE,
56
defaultFilesystemSize: DEFAULT_FILESYSTEM_SIZE,
57
...opts,
58
};
59
this.opts = opts;
60
this.bup = join(this.opts.mount, "bup");
61
this.subvolumes = new Subvolumes(this);
62
}
63
64
init = async () => {
65
await mkdirp([this.opts.mount]);
66
await this.initDevice();
67
await this.mountFilesystem();
68
await btrfs({
69
args: ["quota", "enable", "--simple", this.opts.mount],
70
});
71
await this.initBup();
72
};
73
74
unmount = async () => {
75
await sudo({
76
command: "umount",
77
args: [this.opts.mount],
78
err_on_exit: true,
79
});
80
};
81
82
close = () => {};
83
84
private initDevice = async () => {
85
if (!isImageFile(this.opts.device)) {
86
// raw block device -- nothing to do
87
return;
88
}
89
if (!(await exists(this.opts.device))) {
90
await sudo({
91
command: "truncate",
92
args: ["-s", `${this.opts.defaultFilesystemSize}`, this.opts.device],
93
});
94
}
95
};
96
97
info = async (): Promise<{ [field: string]: string }> => {
98
const { stdout } = await btrfs({
99
args: ["subvolume", "show", this.opts.mount],
100
});
101
const obj: { [field: string]: string } = {};
102
for (const x of stdout.split("\n")) {
103
const i = x.indexOf(":");
104
if (i == -1) continue;
105
obj[x.slice(0, i).trim()] = x.slice(i + 1).trim();
106
}
107
return obj;
108
};
109
110
private mountFilesystem = async () => {
111
try {
112
await this.info();
113
// already mounted
114
return;
115
} catch {}
116
const { stderr, exit_code } = await this._mountFilesystem();
117
if (exit_code) {
118
if (stderr.includes(MOUNT_ERROR)) {
119
if (this.opts.formatIfNeeded) {
120
await this.formatDevice();
121
const { stderr, exit_code } = await this._mountFilesystem();
122
if (exit_code) {
123
throw Error(stderr);
124
} else {
125
return;
126
}
127
}
128
}
129
throw Error(stderr);
130
}
131
};
132
133
private formatDevice = async () => {
134
await sudo({ command: "mkfs.btrfs", args: [this.opts.device] });
135
};
136
137
private _mountFilesystem = async () => {
138
const args: string[] = isImageFile(this.opts.device) ? ["-o", "loop"] : [];
139
args.push(
140
"-o",
141
"compress=zstd",
142
"-o",
143
"noatime",
144
"-o",
145
"space_cache=v2",
146
"-o",
147
"autodefrag",
148
this.opts.device,
149
"-t",
150
"btrfs",
151
this.opts.mount,
152
);
153
{
154
const { stderr, exit_code } = await sudo({
155
command: "mount",
156
args,
157
err_on_exit: false,
158
});
159
if (exit_code) {
160
return { stderr, exit_code };
161
}
162
}
163
const { stderr, exit_code } = await sudo({
164
command: "chown",
165
args: [
166
`${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`,
167
this.opts.mount,
168
],
169
err_on_exit: false,
170
});
171
return { stderr, exit_code };
172
};
173
174
private initBup = async () => {
175
if (!(await exists(this.bup))) {
176
await mkdir(this.bup);
177
}
178
await executeCode({
179
command: "bup",
180
args: ["init"],
181
env: { BUP_DIR: this.bup },
182
});
183
};
184
}
185
186
function isImageFile(name: string) {
187
if (name.startsWith("/dev")) {
188
return false;
189
}
190
// TODO: could probably check os for a device with given name?
191
return name.endsWith(".img");
192
}
193
194
const cache = refCache<Options & { noCache?: boolean }, Filesystem>({
195
name: "btrfs-filesystems",
196
createObject: async (options: Options) => {
197
const filesystem = new Filesystem(options);
198
await filesystem.init();
199
return filesystem;
200
},
201
});
202
203
export async function filesystem(
204
options: Options & { noCache?: boolean },
205
): Promise<Filesystem> {
206
return await cache(options);
207
}
208
209