Path: blob/master/src/packages/file-server/btrfs/test/subvolume.test.ts
1543 views
import { before, after, fs, sudo } from "./setup";1import { mkdir } from "fs/promises";2import { join } from "path";3import { wait } from "@cocalc/backend/conat/test/util";4import { randomBytes } from "crypto";5import { type Subvolume } from "../subvolume";67beforeAll(before);89describe("setting and getting quota of a subvolume", () => {10let vol: Subvolume;11it("set the quota of a subvolume to 5 M", async () => {12vol = await fs.subvolumes.get("q");13await vol.quota.set("5M");1415const { size, used } = await vol.quota.get();16expect(size).toBe(5 * 1024 * 1024);17expect(used).toBe(0);18});1920it("get directory listing", async () => {21const v = await vol.fs.ls("");22expect(v).toEqual([]);23});2425it("write a file and check usage goes up", async () => {26const buf = randomBytes(4 * 1024 * 1024);27await vol.fs.writeFile("buf", buf);28await wait({29until: async () => {30await sudo({ command: "sync" });31const { used } = await vol.quota.usage();32return used > 0;33},34});35const { used } = await vol.quota.usage();36expect(used).toBeGreaterThan(0);3738const v = await vol.fs.ls("");39// size is potentially random, reflecting compression40expect(v).toEqual([{ name: "buf", mtime: v[0].mtime, size: v[0].size }]);41});4243it("fail to write a 50MB file (due to quota)", async () => {44const buf2 = randomBytes(50 * 1024 * 1024);45expect(async () => {46await vol.fs.writeFile("buf2", buf2);47}).rejects.toThrow("write");48});49});5051describe("the filesystem operations", () => {52let vol: Subvolume;5354it("creates a volume and get empty listing", async () => {55vol = await fs.subvolumes.get("fs");56expect(await vol.fs.ls("")).toEqual([]);57});5859it("error listing non-existent path", async () => {60vol = await fs.subvolumes.get("fs");61expect(async () => {62await vol.fs.ls("no-such-path");63}).rejects.toThrow("ENOENT");64});6566it("creates a text file to it", async () => {67await vol.fs.writeFile("a.txt", "hello");68const ls = await vol.fs.ls("");69expect(ls).toEqual([{ name: "a.txt", mtime: ls[0].mtime, size: 5 }]);70});7172it("read the file we just created as utf8", async () => {73expect(await vol.fs.readFile("a.txt", "utf8")).toEqual("hello");74});7576it("read the file we just created as a binary buffer", async () => {77expect(await vol.fs.readFile("a.txt")).toEqual(Buffer.from("hello"));78});7980it("stat the file we just created", async () => {81const s = await vol.fs.stat("a.txt");82expect(s.size).toBe(5);83expect(Math.abs(s.mtimeMs - Date.now())).toBeLessThan(60_000);84});8586let origStat;87it("snapshot filesystem and see file is in snapshot", async () => {88await vol.snapshots.create("snap");89const s = await vol.fs.ls(vol.snapshots.path("snap"));90expect(s).toEqual([{ name: "a.txt", mtime: s[0].mtime, size: 5 }]);9192const stat = await vol.fs.stat("a.txt");93origStat = stat;94expect(stat.mtimeMs / 1000).toBeCloseTo(s[0].mtime ?? 0);95});9697it("unlink (delete) our file", async () => {98await vol.fs.unlink("a.txt");99expect(await vol.fs.ls("")).toEqual([]);100});101102it("snapshot still exists", async () => {103expect(await vol.fs.exists(vol.snapshots.path("snap", "a.txt")));104});105106it("copy file from snapshot and note it has the same mode as before (so much nicer than what happens with zfs)", async () => {107await vol.fs.copyFile(vol.snapshots.path("snap", "a.txt"), "a.txt");108const stat = await vol.fs.stat("a.txt");109expect(stat.mode).toEqual(origStat.mode);110});111112it("create and copy a folder", async () => {113await vol.fs.mkdir("my-folder");114await vol.fs.writeFile("my-folder/foo.txt", "foo");115await vol.fs.cp("my-folder", "folder2", { recursive: true });116expect(await vol.fs.readFile("folder2/foo.txt", "utf8")).toEqual("foo");117});118119it("append to a file", async () => {120await vol.fs.writeFile("b.txt", "hell");121await vol.fs.appendFile("b.txt", "-o");122expect(await vol.fs.readFile("b.txt", "utf8")).toEqual("hell-o");123});124125it("make a file readonly, then change it back", async () => {126await vol.fs.writeFile("c.txt", "hi");127await vol.fs.chmod("c.txt", "440");128expect(async () => {129await vol.fs.appendFile("c.txt", " there");130}).rejects.toThrow("EACCES");131await vol.fs.chmod("c.txt", "660");132await vol.fs.appendFile("c.txt", " there");133});134135it("realpath of a symlink", async () => {136await vol.fs.writeFile("real.txt", "i am real");137await vol.fs.symlink("real.txt", "link.txt");138expect(await vol.fs.realpath("link.txt")).toBe("real.txt");139});140141it("watch for changes", async () => {142await vol.fs.writeFile("w.txt", "hi");143const ac = new AbortController();144const { signal } = ac;145const watcher = vol.fs.watch("w.txt", { signal });146vol.fs.appendFile("w.txt", " there");147// @ts-ignore148const { value, done } = await watcher.next();149expect(done).toBe(false);150expect(value).toEqual({ eventType: "change", filename: "w.txt" });151ac.abort();152153expect(async () => {154// @ts-ignore155await watcher.next();156}).rejects.toThrow("aborted");157});158159it("rename a file", async () => {160await vol.fs.writeFile("old", "hi");161await vol.fs.rename("old", "new");162expect(await vol.fs.readFile("new", "utf8")).toEqual("hi");163});164165it("create and remove a directory", async () => {166await vol.fs.mkdir("path");167await vol.fs.rmdir("path");168});169170it("create a directory recursively and remove", async () => {171await vol.fs.mkdir("path/to/stuff", { recursive: true });172await vol.fs.rm("path", { recursive: true });173});174});175176describe("test snapshots", () => {177let vol: Subvolume;178179it("creates a volume and write a file to it", async () => {180vol = await fs.subvolumes.get("snapper");181expect(await vol.snapshots.hasUnsavedChanges()).toBe(false);182await vol.fs.writeFile("a.txt", "hello");183expect(await vol.snapshots.hasUnsavedChanges()).toBe(true);184});185186it("snapshot the volume", async () => {187expect(await vol.snapshots.ls()).toEqual([]);188await vol.snapshots.create("snap1");189expect((await vol.snapshots.ls()).map((x) => x.name)).toEqual(["snap1"]);190expect(await vol.snapshots.hasUnsavedChanges()).toBe(false);191});192193it("create a file see that we know there are unsaved changes", async () => {194await vol.fs.writeFile("b.txt", "world");195await sudo({ command: "sync" });196expect(await vol.snapshots.hasUnsavedChanges()).toBe(true);197});198199it("delete our file, but then read it in a snapshot", async () => {200await vol.fs.unlink("a.txt");201const b = await vol.fs.readFile(202vol.snapshots.path("snap1", "a.txt"),203"utf8",204);205expect(b).toEqual("hello");206});207208it("verifies snapshot exists", async () => {209expect(await vol.snapshots.exists("snap1")).toBe(true);210expect(await vol.snapshots.exists("snap2")).toBe(false);211});212213it("lock our snapshot and confirm it prevents deletion", async () => {214await vol.snapshots.lock("snap1");215expect(async () => {216await vol.snapshots.delete("snap1");217}).rejects.toThrow("locked");218});219220it("unlock our snapshot and delete it", async () => {221await vol.snapshots.unlock("snap1");222await vol.snapshots.delete("snap1");223expect(await vol.snapshots.exists("snap1")).toBe(false);224expect(await vol.snapshots.ls()).toEqual([]);225});226});227228describe.only("test bup backups", () => {229let vol: Subvolume;230it("creates a volume", async () => {231vol = await fs.subvolumes.get("bup-test");232await vol.fs.writeFile("a.txt", "hello");233});234235it("create a bup backup", async () => {236await vol.bup.save();237});238239it("list bup backups of this vol -- there are 2, one for the date and 'latest'", async () => {240const v = await vol.bup.ls();241expect(v.length).toBe(2);242const t = (v[0].mtime ?? 0) * 1000;243expect(Math.abs(t.valueOf() - Date.now())).toBeLessThan(10_000);244});245246it("confirm a.txt is in our backup", async () => {247const x = await vol.bup.ls("latest");248expect(x).toEqual([249{ name: "a.txt", size: 5, mtime: x[0].mtime, isdir: false },250]);251});252253it("restore a.txt from our backup", async () => {254await vol.fs.writeFile("a.txt", "hello2");255await vol.bup.restore("latest/a.txt");256expect(await vol.fs.readFile("a.txt", "utf8")).toEqual("hello");257});258259it("prune bup backups does nothing since we have so few", async () => {260await vol.bup.prune();261expect((await vol.bup.ls()).length).toBe(2);262});263264it("add a directory and back up", async () => {265await mkdir(join(vol.path, "mydir"));266await vol.fs.writeFile(join("mydir", "file.txt"), "hello3");267expect((await vol.fs.ls("mydir"))[0].name).toBe("file.txt");268await vol.bup.save();269const x = await vol.bup.ls("latest");270expect(x).toEqual([271{ name: "a.txt", size: 5, mtime: x[0].mtime, isdir: false },272{ name: "mydir", size: 0, mtime: x[1].mtime, isdir: true },273]);274expect(Math.abs((x[0].mtime ?? 0) * 1000 - Date.now())).toBeLessThan(27560_000,276);277});278279it("change file in the directory, then restore from backup whole dir", async () => {280await vol.fs.writeFile(join("mydir", "file.txt"), "changed");281await vol.bup.restore("latest/mydir");282expect(await vol.fs.readFile(join("mydir", "file.txt"), "utf8")).toEqual(283"hello3",284);285});286287it("most recent snapshot has a backup before the restore", async () => {288const s = await vol.snapshots.ls();289const recent = s.slice(-1)[0].name;290const p = vol.snapshots.path(recent, "mydir", "file.txt");291expect(await vol.fs.readFile(p, "utf8")).toEqual("changed");292});293});294295afterAll(after);296297298