Skip to content

Commit d663508

Browse files
committed
btrfs: unit testing bup integration
1 parent 1afa4ce commit d663508

File tree

3 files changed

+135
-6
lines changed

3 files changed

+135
-6
lines changed

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

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A subvolume
55
import { type Filesystem, DEFAULT_SUBVOLUME_SIZE } from "./filesystem";
66
import refCache from "@cocalc/util/refcache";
77
import { exists, listdir, mkdirp, sudo } from "./util";
8-
import { join } from "path";
8+
import { join, normalize } from "path";
99
import { updateRollingSnapshots, type SnapshotCounts } from "./snapshots";
1010
//import { human_readable_size } from "@cocalc/util/misc";
1111
import getLogger from "@cocalc/backend/logger";
@@ -292,6 +292,76 @@ export class Subvolume {
292292
.filter((x) => x);
293293
};
294294

295+
bupRestore = async (path: string) => {
296+
path = normalize(path);
297+
// outdir -- path relative to subvolume
298+
// path -- /branch/revision/path/to/dir
299+
await sudo({
300+
command: "bup",
301+
args: [
302+
"-d",
303+
this.filesystem.bup,
304+
"restore",
305+
"-C",
306+
this.path, //join(this.path, outdir),
307+
join(`/${this.name}`, path),
308+
"--quiet",
309+
],
310+
});
311+
};
312+
313+
bupLs = async (
314+
path: string,
315+
): Promise<
316+
{
317+
path: string;
318+
size: number;
319+
timestamp: number;
320+
isdir: boolean;
321+
}[]
322+
> => {
323+
path = normalize(path);
324+
const { stdout } = await sudo({
325+
command: "bup",
326+
args: [
327+
"-d",
328+
this.filesystem.bup,
329+
"ls",
330+
"--almost-all",
331+
"--file-type",
332+
"-l",
333+
join(`/${this.name}`, path),
334+
],
335+
});
336+
const v: {
337+
path: string;
338+
size: number;
339+
timestamp: number;
340+
isdir: boolean;
341+
}[] = [];
342+
for (const x of stdout.split("\n")) {
343+
// [-rw-------","6b851643360e435eb87ef9a6ab64a8b1/6b851643360e435eb87ef9a6ab64a8b1","5","2025-07-15","06:12","a.txt"]
344+
const w = x.split(/\s+/);
345+
if (w.length >= 6) {
346+
let isdir, path;
347+
if (w[5].endsWith("@") || w[5].endsWith("=") || w[5].endsWith("|")) {
348+
w[5] = w[5].slice(0, -1);
349+
}
350+
if (w[5].endsWith("/")) {
351+
isdir = true;
352+
path = w[5].slice(0, -1);
353+
} else {
354+
path = w[5];
355+
isdir = false;
356+
}
357+
const size = parseInt(w[2]);
358+
const timestamp = new Date(w[3] + "T" + w[4]).valueOf();
359+
v.push({ path, size, timestamp, isdir });
360+
}
361+
}
362+
return v;
363+
};
364+
295365
bupPrune = async ({
296366
dailies = "1w",
297367
monthlies = "4m",

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

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { before, after, fs, sudo } from "./setup";
2-
import { readFile, writeFile, unlink } from "fs/promises";
2+
import { mkdir, readFile, writeFile, unlink } from "fs/promises";
33
import { join } from "path";
44
import { wait } from "@cocalc/backend/conat/test/util";
55
import { randomBytes } from "crypto";
6+
import { parseBupTime } from "../util";
67

78
beforeAll(before);
89

@@ -89,9 +90,51 @@ describe("test snapshots", () => {
8990
});
9091
});
9192

92-
// describe("test bup backups", ()=>{
93-
// let vol;
94-
// it('creates a volume')
95-
// })
93+
describe("test bup backups", () => {
94+
let vol;
95+
it("creates a volume", async () => {
96+
vol = await fs.subvolume("bup-test");
97+
await writeFile(join(vol.path, "a.txt"), "hello");
98+
});
99+
100+
it("create a bup backup", async () => {
101+
await vol.createBupBackup();
102+
});
103+
104+
it("list bup backups of this vol -- there are 2, one for the date and 'latest'", async () => {
105+
const v = await vol.bupBackups();
106+
expect(v.length).toBe(2);
107+
const t = parseBupTime(v[0]);
108+
expect(Math.abs(t.valueOf() - Date.now())).toBeLessThan(10_000);
109+
});
110+
111+
it("confirm a.txt is in our backup", async () => {
112+
const x = await vol.bupLs("latest");
113+
expect(x).toEqual([
114+
{ path: "a.txt", size: 5, timestamp: x[0].timestamp, isdir: false },
115+
]);
116+
});
117+
118+
it("restore a.txt from our backup", async () => {
119+
await writeFile(join(vol.path, "a.txt"), "hello2");
120+
await vol.bupRestore("latest/a.txt");
121+
expect(await readFile(join(vol.path, "a.txt"), "utf8")).toEqual("hello");
122+
});
123+
124+
it("prune bup backups does nothing since we have so few", async () => {
125+
await vol.bupPrune();
126+
expect((await vol.bupBackups()).length).toBe(2);
127+
});
128+
129+
it("add a directory and back up", async () => {
130+
await mkdir(join(vol.path, "mydir"));
131+
await vol.createBupBackup();
132+
const x = await vol.bupLs("latest");
133+
expect(x).toEqual([
134+
{ path: "a.txt", size: 5, timestamp: x[0].timestamp, isdir: false },
135+
{ path: "mydir", size: 0, timestamp: x[1].timestamp, isdir: true },
136+
]);
137+
});
138+
});
96139

97140
afterAll(after);

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,19 @@ export async function isdir(path: string) {
6969
const { stdout } = await sudo({ command: "stat", args: ["-c", "%F", path] });
7070
return stdout.trim() == "directory";
7171
}
72+
73+
export function parseBupTime(s: string): Date {
74+
const [year, month, day, time] = s.split("-");
75+
const hours = time.slice(0, 2);
76+
const minutes = time.slice(2, 4);
77+
const seconds = time.slice(4, 6);
78+
79+
return new Date(
80+
Number(year),
81+
Number(month) - 1, // JS months are 0-based
82+
Number(day),
83+
Number(hours),
84+
Number(minutes),
85+
Number(seconds),
86+
);
87+
}

0 commit comments

Comments
 (0)