Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/file-server/btrfs/test/subvolume.test.ts
1543 views
1
import { before, after, fs, sudo } from "./setup";
2
import { mkdir } from "fs/promises";
3
import { join } from "path";
4
import { wait } from "@cocalc/backend/conat/test/util";
5
import { randomBytes } from "crypto";
6
import { type Subvolume } from "../subvolume";
7
8
beforeAll(before);
9
10
describe("setting and getting quota of a subvolume", () => {
11
let vol: Subvolume;
12
it("set the quota of a subvolume to 5 M", async () => {
13
vol = await fs.subvolumes.get("q");
14
await vol.quota.set("5M");
15
16
const { size, used } = await vol.quota.get();
17
expect(size).toBe(5 * 1024 * 1024);
18
expect(used).toBe(0);
19
});
20
21
it("get directory listing", async () => {
22
const v = await vol.fs.ls("");
23
expect(v).toEqual([]);
24
});
25
26
it("write a file and check usage goes up", async () => {
27
const buf = randomBytes(4 * 1024 * 1024);
28
await vol.fs.writeFile("buf", buf);
29
await wait({
30
until: async () => {
31
await sudo({ command: "sync" });
32
const { used } = await vol.quota.usage();
33
return used > 0;
34
},
35
});
36
const { used } = await vol.quota.usage();
37
expect(used).toBeGreaterThan(0);
38
39
const v = await vol.fs.ls("");
40
// size is potentially random, reflecting compression
41
expect(v).toEqual([{ name: "buf", mtime: v[0].mtime, size: v[0].size }]);
42
});
43
44
it("fail to write a 50MB file (due to quota)", async () => {
45
const buf2 = randomBytes(50 * 1024 * 1024);
46
expect(async () => {
47
await vol.fs.writeFile("buf2", buf2);
48
}).rejects.toThrow("write");
49
});
50
});
51
52
describe("the filesystem operations", () => {
53
let vol: Subvolume;
54
55
it("creates a volume and get empty listing", async () => {
56
vol = await fs.subvolumes.get("fs");
57
expect(await vol.fs.ls("")).toEqual([]);
58
});
59
60
it("error listing non-existent path", async () => {
61
vol = await fs.subvolumes.get("fs");
62
expect(async () => {
63
await vol.fs.ls("no-such-path");
64
}).rejects.toThrow("ENOENT");
65
});
66
67
it("creates a text file to it", async () => {
68
await vol.fs.writeFile("a.txt", "hello");
69
const ls = await vol.fs.ls("");
70
expect(ls).toEqual([{ name: "a.txt", mtime: ls[0].mtime, size: 5 }]);
71
});
72
73
it("read the file we just created as utf8", async () => {
74
expect(await vol.fs.readFile("a.txt", "utf8")).toEqual("hello");
75
});
76
77
it("read the file we just created as a binary buffer", async () => {
78
expect(await vol.fs.readFile("a.txt")).toEqual(Buffer.from("hello"));
79
});
80
81
it("stat the file we just created", async () => {
82
const s = await vol.fs.stat("a.txt");
83
expect(s.size).toBe(5);
84
expect(Math.abs(s.mtimeMs - Date.now())).toBeLessThan(60_000);
85
});
86
87
let origStat;
88
it("snapshot filesystem and see file is in snapshot", async () => {
89
await vol.snapshots.create("snap");
90
const s = await vol.fs.ls(vol.snapshots.path("snap"));
91
expect(s).toEqual([{ name: "a.txt", mtime: s[0].mtime, size: 5 }]);
92
93
const stat = await vol.fs.stat("a.txt");
94
origStat = stat;
95
expect(stat.mtimeMs / 1000).toBeCloseTo(s[0].mtime ?? 0);
96
});
97
98
it("unlink (delete) our file", async () => {
99
await vol.fs.unlink("a.txt");
100
expect(await vol.fs.ls("")).toEqual([]);
101
});
102
103
it("snapshot still exists", async () => {
104
expect(await vol.fs.exists(vol.snapshots.path("snap", "a.txt")));
105
});
106
107
it("copy file from snapshot and note it has the same mode as before (so much nicer than what happens with zfs)", async () => {
108
await vol.fs.copyFile(vol.snapshots.path("snap", "a.txt"), "a.txt");
109
const stat = await vol.fs.stat("a.txt");
110
expect(stat.mode).toEqual(origStat.mode);
111
});
112
113
it("create and copy a folder", async () => {
114
await vol.fs.mkdir("my-folder");
115
await vol.fs.writeFile("my-folder/foo.txt", "foo");
116
await vol.fs.cp("my-folder", "folder2", { recursive: true });
117
expect(await vol.fs.readFile("folder2/foo.txt", "utf8")).toEqual("foo");
118
});
119
120
it("append to a file", async () => {
121
await vol.fs.writeFile("b.txt", "hell");
122
await vol.fs.appendFile("b.txt", "-o");
123
expect(await vol.fs.readFile("b.txt", "utf8")).toEqual("hell-o");
124
});
125
126
it("make a file readonly, then change it back", async () => {
127
await vol.fs.writeFile("c.txt", "hi");
128
await vol.fs.chmod("c.txt", "440");
129
expect(async () => {
130
await vol.fs.appendFile("c.txt", " there");
131
}).rejects.toThrow("EACCES");
132
await vol.fs.chmod("c.txt", "660");
133
await vol.fs.appendFile("c.txt", " there");
134
});
135
136
it("realpath of a symlink", async () => {
137
await vol.fs.writeFile("real.txt", "i am real");
138
await vol.fs.symlink("real.txt", "link.txt");
139
expect(await vol.fs.realpath("link.txt")).toBe("real.txt");
140
});
141
142
it("watch for changes", async () => {
143
await vol.fs.writeFile("w.txt", "hi");
144
const ac = new AbortController();
145
const { signal } = ac;
146
const watcher = vol.fs.watch("w.txt", { signal });
147
vol.fs.appendFile("w.txt", " there");
148
// @ts-ignore
149
const { value, done } = await watcher.next();
150
expect(done).toBe(false);
151
expect(value).toEqual({ eventType: "change", filename: "w.txt" });
152
ac.abort();
153
154
expect(async () => {
155
// @ts-ignore
156
await watcher.next();
157
}).rejects.toThrow("aborted");
158
});
159
160
it("rename a file", async () => {
161
await vol.fs.writeFile("old", "hi");
162
await vol.fs.rename("old", "new");
163
expect(await vol.fs.readFile("new", "utf8")).toEqual("hi");
164
});
165
166
it("create and remove a directory", async () => {
167
await vol.fs.mkdir("path");
168
await vol.fs.rmdir("path");
169
});
170
171
it("create a directory recursively and remove", async () => {
172
await vol.fs.mkdir("path/to/stuff", { recursive: true });
173
await vol.fs.rm("path", { recursive: true });
174
});
175
});
176
177
describe("test snapshots", () => {
178
let vol: Subvolume;
179
180
it("creates a volume and write a file to it", async () => {
181
vol = await fs.subvolumes.get("snapper");
182
expect(await vol.snapshots.hasUnsavedChanges()).toBe(false);
183
await vol.fs.writeFile("a.txt", "hello");
184
expect(await vol.snapshots.hasUnsavedChanges()).toBe(true);
185
});
186
187
it("snapshot the volume", async () => {
188
expect(await vol.snapshots.ls()).toEqual([]);
189
await vol.snapshots.create("snap1");
190
expect((await vol.snapshots.ls()).map((x) => x.name)).toEqual(["snap1"]);
191
expect(await vol.snapshots.hasUnsavedChanges()).toBe(false);
192
});
193
194
it("create a file see that we know there are unsaved changes", async () => {
195
await vol.fs.writeFile("b.txt", "world");
196
await sudo({ command: "sync" });
197
expect(await vol.snapshots.hasUnsavedChanges()).toBe(true);
198
});
199
200
it("delete our file, but then read it in a snapshot", async () => {
201
await vol.fs.unlink("a.txt");
202
const b = await vol.fs.readFile(
203
vol.snapshots.path("snap1", "a.txt"),
204
"utf8",
205
);
206
expect(b).toEqual("hello");
207
});
208
209
it("verifies snapshot exists", async () => {
210
expect(await vol.snapshots.exists("snap1")).toBe(true);
211
expect(await vol.snapshots.exists("snap2")).toBe(false);
212
});
213
214
it("lock our snapshot and confirm it prevents deletion", async () => {
215
await vol.snapshots.lock("snap1");
216
expect(async () => {
217
await vol.snapshots.delete("snap1");
218
}).rejects.toThrow("locked");
219
});
220
221
it("unlock our snapshot and delete it", async () => {
222
await vol.snapshots.unlock("snap1");
223
await vol.snapshots.delete("snap1");
224
expect(await vol.snapshots.exists("snap1")).toBe(false);
225
expect(await vol.snapshots.ls()).toEqual([]);
226
});
227
});
228
229
describe.only("test bup backups", () => {
230
let vol: Subvolume;
231
it("creates a volume", async () => {
232
vol = await fs.subvolumes.get("bup-test");
233
await vol.fs.writeFile("a.txt", "hello");
234
});
235
236
it("create a bup backup", async () => {
237
await vol.bup.save();
238
});
239
240
it("list bup backups of this vol -- there are 2, one for the date and 'latest'", async () => {
241
const v = await vol.bup.ls();
242
expect(v.length).toBe(2);
243
const t = (v[0].mtime ?? 0) * 1000;
244
expect(Math.abs(t.valueOf() - Date.now())).toBeLessThan(10_000);
245
});
246
247
it("confirm a.txt is in our backup", async () => {
248
const x = await vol.bup.ls("latest");
249
expect(x).toEqual([
250
{ name: "a.txt", size: 5, mtime: x[0].mtime, isdir: false },
251
]);
252
});
253
254
it("restore a.txt from our backup", async () => {
255
await vol.fs.writeFile("a.txt", "hello2");
256
await vol.bup.restore("latest/a.txt");
257
expect(await vol.fs.readFile("a.txt", "utf8")).toEqual("hello");
258
});
259
260
it("prune bup backups does nothing since we have so few", async () => {
261
await vol.bup.prune();
262
expect((await vol.bup.ls()).length).toBe(2);
263
});
264
265
it("add a directory and back up", async () => {
266
await mkdir(join(vol.path, "mydir"));
267
await vol.fs.writeFile(join("mydir", "file.txt"), "hello3");
268
expect((await vol.fs.ls("mydir"))[0].name).toBe("file.txt");
269
await vol.bup.save();
270
const x = await vol.bup.ls("latest");
271
expect(x).toEqual([
272
{ name: "a.txt", size: 5, mtime: x[0].mtime, isdir: false },
273
{ name: "mydir", size: 0, mtime: x[1].mtime, isdir: true },
274
]);
275
expect(Math.abs((x[0].mtime ?? 0) * 1000 - Date.now())).toBeLessThan(
276
60_000,
277
);
278
});
279
280
it("change file in the directory, then restore from backup whole dir", async () => {
281
await vol.fs.writeFile(join("mydir", "file.txt"), "changed");
282
await vol.bup.restore("latest/mydir");
283
expect(await vol.fs.readFile(join("mydir", "file.txt"), "utf8")).toEqual(
284
"hello3",
285
);
286
});
287
288
it("most recent snapshot has a backup before the restore", async () => {
289
const s = await vol.snapshots.ls();
290
const recent = s.slice(-1)[0].name;
291
const p = vol.snapshots.path(recent, "mydir", "file.txt");
292
expect(await vol.fs.readFile(p, "utf8")).toEqual("changed");
293
});
294
});
295
296
afterAll(after);
297
298