Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/file-server/btrfs/subvolume-snapshots.ts
1539 views
1
import { type Subvolume } from "./subvolume";
2
import { btrfs } from "./util";
3
import getLogger from "@cocalc/backend/logger";
4
import { join } from "path";
5
import { type DirectoryListingEntry } from "@cocalc/util/types";
6
import { SnapshotCounts, updateRollingSnapshots } from "./snapshots";
7
8
export const SNAPSHOTS = ".snapshots";
9
const logger = getLogger("file-server:btrfs:subvolume-snapshots");
10
11
export class SubvolumeSnapshots {
12
public readonly snapshotsDir: string;
13
14
constructor(public subvolume: Subvolume) {
15
this.snapshotsDir = join(this.subvolume.path, SNAPSHOTS);
16
}
17
18
path = (snapshot?: string, ...segments) => {
19
if (!snapshot) {
20
return SNAPSHOTS;
21
}
22
return join(SNAPSHOTS, snapshot, ...segments);
23
};
24
25
private makeSnapshotsDir = async () => {
26
if (await this.subvolume.fs.exists(SNAPSHOTS)) {
27
return;
28
}
29
await this.subvolume.fs.mkdir(SNAPSHOTS);
30
await this.subvolume.fs.chmod(SNAPSHOTS, "0550");
31
};
32
33
create = async (name?: string) => {
34
if (name?.startsWith(".")) {
35
throw Error("snapshot name must not start with '.'");
36
}
37
name ??= new Date().toISOString();
38
logger.debug("create", { name, subvolume: this.subvolume.name });
39
await this.makeSnapshotsDir();
40
await btrfs({
41
args: [
42
"subvolume",
43
"snapshot",
44
"-r",
45
this.subvolume.path,
46
join(this.snapshotsDir, name),
47
],
48
});
49
};
50
51
ls = async (): Promise<DirectoryListingEntry[]> => {
52
await this.makeSnapshotsDir();
53
return await this.subvolume.fs.ls(SNAPSHOTS, { hidden: false });
54
};
55
56
lock = async (name: string) => {
57
if (await this.subvolume.fs.exists(this.path(name))) {
58
this.subvolume.fs.writeFile(this.path(`.${name}.lock`), "");
59
} else {
60
throw Error(`snapshot ${name} does not exist`);
61
}
62
};
63
64
unlock = async (name: string) => {
65
await this.subvolume.fs.rm(this.path(`.${name}.lock`));
66
};
67
68
exists = async (name: string) => {
69
return await this.subvolume.fs.exists(this.path(name));
70
};
71
72
delete = async (name) => {
73
if (await this.subvolume.fs.exists(this.path(`.${name}.lock`))) {
74
throw Error(`snapshot ${name} is locked`);
75
}
76
await btrfs({
77
args: ["subvolume", "delete", join(this.snapshotsDir, name)],
78
});
79
};
80
81
// update the rolling snapshots schedule
82
update = async (counts?: Partial<SnapshotCounts>) => {
83
return await updateRollingSnapshots({ snapshots: this, counts });
84
};
85
86
// has newly written changes since last snapshot
87
hasUnsavedChanges = async (): Promise<boolean> => {
88
const s = await this.ls();
89
if (s.length == 0) {
90
// more than just the SNAPSHOTS directory?
91
const v = await this.subvolume.fs.ls("", { hidden: true });
92
if (v.length == 0 || (v.length == 1 && v[0].name == SNAPSHOTS)) {
93
return false;
94
}
95
return true;
96
}
97
const pathGen = await getGeneration(this.subvolume.path);
98
const snapGen = await getGeneration(
99
join(this.snapshotsDir, s[s.length - 1].name),
100
);
101
return snapGen < pathGen;
102
};
103
}
104
105
async function getGeneration(path: string): Promise<number> {
106
const { stdout } = await btrfs({
107
args: ["subvolume", "show", path],
108
verbose: false,
109
});
110
return parseInt(stdout.split("Generation:")[1].split("\n")[0].trim());
111
}
112
113