A simple, zero-configuration script to quickly boot FreeBSD ISO images using QEMU
1import { createId } from "@paralleldrive/cuid2";
2import { Data, Effect } from "effect";
3import type { DeleteResult, InsertResult } from "kysely";
4import { VOLUME_DIR } from "./constants.ts";
5import { ctx } from "./context.ts";
6import type { Image, Volume } from "./db.ts";
7
8export class VolumeError extends Data.TaggedError("VolumeError")<{
9 message?: unknown;
10}> {}
11
12export const listVolumes = () =>
13 Effect.tryPromise({
14 try: () => ctx.db.selectFrom("volumes").selectAll().execute(),
15 catch: (error) =>
16 new VolumeError({
17 message: error instanceof Error ? error.message : String(error),
18 }),
19 });
20
21export const getVolume = (
22 id: string,
23): Effect.Effect<Volume | undefined, VolumeError, never> =>
24 Effect.tryPromise({
25 try: () =>
26 ctx.db
27 .selectFrom("volumes")
28 .selectAll()
29 .where((eb) =>
30 eb.or([
31 eb("name", "=", id),
32 eb("id", "=", id),
33 eb("path", "=", id),
34 ])
35 )
36 .executeTakeFirst(),
37 catch: (error) =>
38 new VolumeError({
39 message: error instanceof Error ? error.message : String(error),
40 }),
41 });
42
43export const saveVolume = (
44 volume: Volume,
45): Effect.Effect<InsertResult[], VolumeError, never> =>
46 Effect.tryPromise({
47 try: () =>
48 ctx.db.insertInto("volumes")
49 .values(volume)
50 .execute(),
51 catch: (error) =>
52 new VolumeError({
53 message: error instanceof Error ? error.message : String(error),
54 }),
55 });
56
57export const deleteVolume = (
58 id: string,
59): Effect.Effect<DeleteResult[], VolumeError, never> =>
60 Effect.tryPromise({
61 try: () =>
62 ctx.db.deleteFrom("volumes").where((eb) =>
63 eb.or([
64 eb("name", "=", id),
65 eb("id", "=", id),
66 ])
67 ).execute(),
68 catch: (error) =>
69 new VolumeError({
70 message: error instanceof Error ? error.message : String(error),
71 }),
72 });
73
74export const createVolume = (
75 name: string,
76 baseImage: Image,
77 size?: string,
78): Effect.Effect<Volume, VolumeError, never> =>
79 Effect.tryPromise({
80 try: async () => {
81 const path = `${VOLUME_DIR}/${name}.qcow2`;
82
83 if (!(await Deno.stat(path).catch(() => false))) {
84 await Deno.mkdir(VOLUME_DIR, { recursive: true });
85 const qemu = new Deno.Command("qemu-img", {
86 args: [
87 "create",
88 "-F",
89 "raw",
90 "-f",
91 "qcow2",
92 "-b",
93 baseImage.path,
94 path,
95 ...(size ? [size] : []),
96 ],
97 stdout: "inherit",
98 stderr: "inherit",
99 })
100 .spawn();
101 const status = await qemu.status;
102 if (!status.success) {
103 throw new Error(
104 `Failed to create volume: qemu-img exited with code ${status.code}`,
105 );
106 }
107 }
108
109 ctx.db.insertInto("volumes").values({
110 id: createId(),
111 name,
112 path,
113 baseImageId: baseImage.id,
114 }).execute();
115 const volume = await ctx.db
116 .selectFrom("volumes")
117 .selectAll()
118 .where("name", "=", name)
119 .executeTakeFirst();
120 if (!volume) {
121 throw new Error("Failed to create volume");
122 }
123 return volume;
124 },
125 catch: (error) =>
126 new VolumeError({
127 message: error instanceof Error ? error.message : String(error),
128 }),
129 });