Skip to content

Commit f22c75f

Browse files
committed
btrfs: subvolumes refactor
1 parent 2fe725a commit f22c75f

12 files changed

+202
-166
lines changed

src/packages/file-server/btrfs/filesystem.ts

Lines changed: 27 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
/*
2-
A BTRFS Filesystem
2+
BTRFS Filesystem
33
44
DEVELOPMENT:
55
66
Start node, then:
77
88
DEBUG="cocalc:*file-server*" DEBUG_CONSOLE=yes node
99
10-
a = require('@cocalc/file-server/storage-btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})
10+
a = require('@cocalc/file-server/btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})
1111
1212
*/
1313

1414
import refCache from "@cocalc/util/refcache";
15-
import { exists, isdir, listdir, mkdirp, rmdir, sudo } from "./util";
16-
import { subvolume, type Subvolume } from "./subvolume";
17-
import { SNAPSHOTS } from "./subvolume-snapshots";
18-
import { join, normalize } from "path";
15+
import { exists, mkdirp, btrfs, sudo } from "./util";
16+
import { join } from "path";
17+
import { Subvolumes } from "./subvolumes";
1918

2019
// default size of btrfs filesystem if creating an image file.
2120
const DEFAULT_FILESYSTEM_SIZE = "10G";
@@ -25,8 +24,6 @@ export const DEFAULT_SUBVOLUME_SIZE = "1G";
2524

2625
const MOUNT_ERROR = "wrong fs type, bad option, bad superblock";
2726

28-
const RESERVED = new Set(["bup", "recv", "streams", SNAPSHOTS]);
29-
3027
export interface Options {
3128
// the underlying block device.
3229
// If this is a file (or filename) ending in .img, then it's a sparse file mounted as a loopback device.
@@ -40,9 +37,6 @@ export interface Options {
4037
// where the btrfs filesystem is mounted
4138
mount: string;
4239

43-
// all subvolumes will have this owner
44-
uid?: number;
45-
4640
// default size of newly created subvolumes
4741
defaultSize?: string | number;
4842
defaultFilesystemSize?: string | number;
@@ -52,6 +46,7 @@ export class Filesystem {
5246
public readonly opts: Options;
5347
public readonly bup: string;
5448
public readonly streams: string;
49+
public readonly subvolumes: Subvolumes;
5550

5651
constructor(opts: Options) {
5752
opts = {
@@ -62,6 +57,7 @@ export class Filesystem {
6257
this.opts = opts;
6358
this.bup = join(this.opts.mount, "bup");
6459
this.streams = join(this.opts.mount, "streams");
60+
this.subvolumes = new Subvolumes(this);
6561
}
6662

6763
init = async () => {
@@ -71,8 +67,7 @@ export class Filesystem {
7167
await this.initDevice();
7268
await this.mountFilesystem();
7369
await sudo({ command: "chmod", args: ["a+rx", this.opts.mount] });
74-
await sudo({
75-
command: "btrfs",
70+
await btrfs({
7671
args: ["quota", "enable", "--simple", this.opts.mount],
7772
});
7873
await sudo({
@@ -95,8 +90,7 @@ export class Filesystem {
9590
};
9691

9792
info = async (): Promise<{ [field: string]: string }> => {
98-
const { stdout } = await sudo({
99-
command: "btrfs",
93+
const { stdout } = await btrfs({
10094
args: ["subvolume", "show", this.opts.mount],
10195
});
10296
const obj: { [field: string]: string } = {};
@@ -108,7 +102,6 @@ export class Filesystem {
108102
return obj;
109103
};
110104

111-
//
112105
private mountFilesystem = async () => {
113106
try {
114107
await this.info();
@@ -148,11 +141,25 @@ export class Filesystem {
148141
"btrfs",
149142
this.opts.mount,
150143
);
151-
return await sudo({
152-
command: "mount",
153-
args,
144+
{
145+
const { stderr, exit_code } = await sudo({
146+
command: "mount",
147+
args,
148+
err_on_exit: false,
149+
});
150+
if (exit_code) {
151+
return { stderr, exit_code };
152+
}
153+
}
154+
const { stderr, exit_code } = await sudo({
155+
command: "chown",
156+
args: [
157+
`${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`,
158+
this.opts.mount,
159+
],
154160
err_on_exit: false,
155161
});
162+
return { stderr, exit_code };
156163
};
157164

158165
unmount = async () => {
@@ -167,108 +174,7 @@ export class Filesystem {
167174
await sudo({ command: "mkfs.btrfs", args: [this.opts.device] });
168175
};
169176

170-
close = () => {
171-
// nothing, yet
172-
};
173-
174-
subvolume = async (name: string): Promise<Subvolume> => {
175-
if (RESERVED.has(name)) {
176-
throw Error(`${name} is reserved`);
177-
}
178-
return await subvolume({ filesystem: this, name });
179-
};
180-
181-
// create a subvolume by cloning an existing one.
182-
cloneSubvolume = async (source: string, name: string) => {
183-
if (RESERVED.has(name)) {
184-
throw Error(`${name} is reserved`);
185-
}
186-
if (!(await exists(join(this.opts.mount, source)))) {
187-
throw Error(`subvolume ${source} does not exist`);
188-
}
189-
if (await exists(join(this.opts.mount, name))) {
190-
throw Error(`subvolume ${name} already exists`);
191-
}
192-
await sudo({
193-
command: "btrfs",
194-
args: [
195-
"subvolume",
196-
"snapshot",
197-
join(this.opts.mount, source),
198-
join(this.opts.mount, source, name),
199-
],
200-
});
201-
await sudo({
202-
command: "mv",
203-
args: [join(this.opts.mount, source, name), join(this.opts.mount, name)],
204-
});
205-
const snapdir = join(this.opts.mount, name, SNAPSHOTS);
206-
if (await exists(snapdir)) {
207-
const snapshots = await listdir(snapdir);
208-
await rmdir(
209-
snapshots.map((x) => join(this.opts.mount, name, SNAPSHOTS, x)),
210-
);
211-
}
212-
const src = await this.subvolume(source);
213-
const vol = await this.subvolume(name);
214-
const { size } = await src.quota.get();
215-
if (size) {
216-
await vol.quota.set(size);
217-
}
218-
return vol;
219-
};
220-
221-
deleteSubvolume = async (name: string) => {
222-
await sudo({
223-
command: "btrfs",
224-
args: ["subvolume", "delete", join(this.opts.mount, name)],
225-
});
226-
};
227-
228-
list = async (): Promise<string[]> => {
229-
const { stdout } = await sudo({
230-
command: "btrfs",
231-
args: ["subvolume", "list", this.opts.mount],
232-
});
233-
return stdout
234-
.split("\n")
235-
.map((x) => x.split(" ").slice(-1)[0])
236-
.filter((x) => x)
237-
.sort();
238-
};
239-
240-
rsync = async ({
241-
src,
242-
target,
243-
args = ["-axH"],
244-
timeout = 5 * 60 * 1000,
245-
}: {
246-
src: string;
247-
target: string;
248-
args?: string[];
249-
timeout?: number;
250-
}): Promise<{ stdout: string; stderr: string; exit_code: number }> => {
251-
let srcPath = normalize(join(this.opts.mount, src));
252-
if (!srcPath.startsWith(this.opts.mount)) {
253-
throw Error("suspicious source");
254-
}
255-
let targetPath = normalize(join(this.opts.mount, target));
256-
if (!targetPath.startsWith(this.opts.mount)) {
257-
throw Error("suspicious target");
258-
}
259-
if (!srcPath.endsWith("/") && (await isdir(srcPath))) {
260-
srcPath += "/";
261-
if (!targetPath.endsWith("/")) {
262-
targetPath += "/";
263-
}
264-
}
265-
return await sudo({
266-
command: "rsync",
267-
args: [...args, srcPath, targetPath],
268-
err_on_exit: false,
269-
timeout: timeout / 1000,
270-
});
271-
};
177+
close = () => {};
272178
}
273179

274180
function isImageFile(name: string) {

src/packages/file-server/btrfs/snapshots.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type SubvolumeSnapshots } from "./subvolume-snapshots";
22
import getLogger from "@cocalc/backend/logger";
33

4-
const logger = getLogger("file-server:storage-btrfs:snapshots");
4+
const logger = getLogger("file-server:btrfs:snapshots");
55

66
const DATE_REGEXP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
77

src/packages/file-server/btrfs/subvolume-bup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import getLogger from "@cocalc/backend/logger";
2727

2828
const BUP_SNAPSHOT = "temp-bup-snapshot";
2929

30-
const logger = getLogger("file-server:storage-btrfs:subvolume-bup");
30+
const logger = getLogger("file-server:btrfs:subvolume-bup");
3131

3232
export class SubvolumeBup {
3333
constructor(private subvolume: Subvolume) {}

src/packages/file-server/btrfs/subvolume-quota.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type Subvolume } from "./subvolume";
22
import { btrfs } from "./util";
33
import getLogger from "@cocalc/backend/logger";
44

5-
const logger = getLogger("file-server:storage-btrfs:subvolume-quota");
5+
const logger = getLogger("file-server:btrfs:subvolume-quota");
66

77
export class SubvolumeQuota {
88
constructor(public subvolume: Subvolume) {}

src/packages/file-server/btrfs/subvolume-snapshots.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { type DirectoryListingEntry } from "@cocalc/util/types";
66
import { SnapshotCounts, updateRollingSnapshots } from "./snapshots";
77

88
export const SNAPSHOTS = ".snapshots";
9-
const logger = getLogger("file-server:storage-btrfs:subvolume-snapshots");
9+
const logger = getLogger("file-server:btrfs:subvolume-snapshots");
1010

1111
export class SubvolumeSnapshots {
1212
public readonly snapshotsDir: string;

src/packages/file-server/btrfs/subvolume.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SubvolumeSnapshots } from "./subvolume-snapshots";
1212
import { SubvolumeQuota } from "./subvolume-quota";
1313
import getLogger from "@cocalc/backend/logger";
1414

15-
const logger = getLogger("file-server:storage-btrfs:subvolume");
15+
const logger = getLogger("file-server:btrfs:subvolume");
1616

1717
interface Options {
1818
filesystem: Filesystem;
@@ -62,15 +62,16 @@ export class Subvolume {
6262
delete this.path;
6363
// @ts-ignore
6464
delete this.snapshotsDir;
65+
for (const sub of ["fs", "bup", "snapshots", "quota"]) {
66+
this[sub].close?.();
67+
delete this[sub];
68+
}
6569
};
6670

6771
private chown = async (path: string) => {
68-
if (!this.filesystem.opts.uid) {
69-
return;
70-
}
7172
await sudo({
7273
command: "chown",
73-
args: [`${this.filesystem.opts.uid}:${this.filesystem.opts.uid}`, path],
74+
args: [`${process.getuid?.() ?? 0}:${process.getgid?.() ?? 0}`, path],
7475
});
7576
};
7677

@@ -84,6 +85,7 @@ export class Subvolume {
8485

8586
const cache = refCache<Options & { noCache?: boolean }, Subvolume>({
8687
name: "btrfs-subvolumes",
88+
createKey: ({ name }) => name,
8789
createObject: async (options: Options) => {
8890
const subvolume = new Subvolume(options);
8991
await subvolume.init();

0 commit comments

Comments
 (0)