mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: toggle auto remount
This commit is contained in:
@@ -256,7 +256,8 @@ export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses];
|
|||||||
|
|
||||||
export type UpdateVolumeData = {
|
export type UpdateVolumeData = {
|
||||||
body?: {
|
body?: {
|
||||||
config:
|
autoRemount?: boolean;
|
||||||
|
config?:
|
||||||
| {
|
| {
|
||||||
backend: "directory";
|
backend: "directory";
|
||||||
}
|
}
|
||||||
@@ -308,6 +309,7 @@ export type UpdateVolumeResponses = {
|
|||||||
200: {
|
200: {
|
||||||
message: string;
|
message: string;
|
||||||
volume: {
|
volume: {
|
||||||
|
autoRemount: boolean;
|
||||||
config:
|
config:
|
||||||
| {
|
| {
|
||||||
backend: "directory";
|
backend: "directory";
|
||||||
@@ -339,9 +341,12 @@ export type UpdateVolumeResponses = {
|
|||||||
username?: string;
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
|
lastError: string;
|
||||||
|
lastHealthCheck: number;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
type: string;
|
status: "error" | "mounted" | "unknown" | "unmounted";
|
||||||
|
type: "directory" | "nfs" | "smb" | "webdav";
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ type Props = {
|
|||||||
toggle: (v: boolean) => void;
|
toggle: (v: boolean) => void;
|
||||||
enabledLabel: string;
|
enabledLabel: string;
|
||||||
disabledLabel: string;
|
disabledLabel: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel }: Props) => {
|
export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -19,7 +20,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel }: Props) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>{isOn ? enabledLabel : disabledLabel}</span>
|
<span>{isOn ? enabledLabel : disabledLabel}</span>
|
||||||
<Switch checked={isOn} onCheckedChange={toggle} />
|
<Switch disabled={disabled} checked={isOn} onCheckedChange={toggle} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { HeartIcon } from "lucide-react";
|
import { HeartIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { healthCheckVolumeMutation } from "~/api-client/@tanstack/react-query.gen";
|
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/api-client/@tanstack/react-query.gen";
|
||||||
import { OnOff } from "~/components/onoff";
|
import { OnOff } from "~/components/onoff";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||||
@@ -28,6 +28,15 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleAutoRemount = useMutation({
|
||||||
|
...updateVolumeMutation(),
|
||||||
|
onSuccess: (d) => {
|
||||||
|
toast.success("Volume updated", {
|
||||||
|
description: `Auto remount is now ${d.volume.autoRemount ? "enabled" : "paused"}.`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="flex-1 flex flex-col h-full">
|
<Card className="flex-1 flex flex-col h-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -46,7 +55,15 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
)}
|
)}
|
||||||
<span className="flex justify-between items-center gap-2">
|
<span className="flex justify-between items-center gap-2">
|
||||||
<span className="text-sm">Remount on error</span>
|
<span className="text-sm">Remount on error</span>
|
||||||
<OnOff isOn={volume.autoRemount} toggle={() => {}} enabledLabel="Enabled" disabledLabel="Paused" />
|
<OnOff
|
||||||
|
isOn={volume.autoRemount}
|
||||||
|
toggle={() =>
|
||||||
|
toggleAutoRemount.mutate({ path: { name: volume.name }, body: { autoRemount: !volume.autoRemount } })
|
||||||
|
}
|
||||||
|
disabled={toggleAutoRemount.isPending}
|
||||||
|
enabledLabel="Enabled"
|
||||||
|
disabledLabel="Paused"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{volume.status !== "unmounted" && (
|
{volume.status !== "unmounted" && (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const startup = async () => {
|
|||||||
const volumes = await db.query.volumesTable.findMany({
|
const volumes = await db.query.volumesTable.findMany({
|
||||||
where: or(
|
where: or(
|
||||||
eq(volumesTable.status, "mounted"),
|
eq(volumesTable.status, "mounted"),
|
||||||
and(eq(volumesTable.autoRemount, 1), eq(volumesTable.status, "error")),
|
and(eq(volumesTable.autoRemount, true), eq(volumesTable.status, "error")),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,10 +28,8 @@ export const startup = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const volume of volumes) {
|
for (const volume of volumes) {
|
||||||
const { error } = await volumeService.checkHealth(volume.name);
|
const { status } = await volumeService.checkHealth(volume.name);
|
||||||
if (error && volume.autoRemount) {
|
if (status === "error" && volume.autoRemount) {
|
||||||
// TODO: retry with backoff based on last health check time
|
|
||||||
// Until we reach the max backoff and it'll try every 10 minutes
|
|
||||||
await volumeService.mountVolume(volume.name);
|
await volumeService.mountVolume(volume.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
updateVolumeBody,
|
updateVolumeBody,
|
||||||
updateVolumeDto,
|
updateVolumeDto,
|
||||||
healthCheckDto,
|
healthCheckDto,
|
||||||
|
type UpdateVolumeResponseDto,
|
||||||
} from "./volume.dto";
|
} from "./volume.dto";
|
||||||
import { volumeService } from "./volume.service";
|
import { volumeService } from "./volume.service";
|
||||||
|
|
||||||
@@ -86,19 +87,17 @@ export const volumeController = new Hono()
|
|||||||
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
.put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
|
||||||
const { name } = c.req.param();
|
const { name } = c.req.param();
|
||||||
const body = c.req.valid("json");
|
const body = c.req.valid("json");
|
||||||
const res = await volumeService.updateVolume(name, body.config);
|
const res = await volumeService.updateVolume(name, body);
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
message: "Volume updated",
|
message: "Volume updated",
|
||||||
volume: {
|
volume: {
|
||||||
name: res.volume.name,
|
...res.volume,
|
||||||
path: res.volume.path,
|
|
||||||
type: res.volume.type,
|
|
||||||
createdAt: res.volume.createdAt.getTime(),
|
createdAt: res.volume.createdAt.getTime(),
|
||||||
updatedAt: res.volume.updatedAt.getTime(),
|
updatedAt: res.volume.updatedAt.getTime(),
|
||||||
config: res.volume.config,
|
lastHealthCheck: res.volume.lastHealthCheck.getTime(),
|
||||||
},
|
},
|
||||||
};
|
} satisfies UpdateVolumeResponseDto;
|
||||||
|
|
||||||
return c.json(response, 200);
|
return c.json(response, 200);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -139,19 +139,15 @@ export const getVolumeDto = describeRoute({
|
|||||||
* Update a volume
|
* Update a volume
|
||||||
*/
|
*/
|
||||||
export const updateVolumeBody = type({
|
export const updateVolumeBody = type({
|
||||||
config: volumeConfigSchema,
|
autoRemount: "boolean?",
|
||||||
|
config: volumeConfigSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type UpdateVolumeBody = typeof updateVolumeBody.infer;
|
||||||
|
|
||||||
export const updateVolumeResponse = type({
|
export const updateVolumeResponse = type({
|
||||||
message: "string",
|
message: "string",
|
||||||
volume: type({
|
volume: volumeSchema,
|
||||||
name: "string",
|
|
||||||
path: "string",
|
|
||||||
type: "string",
|
|
||||||
createdAt: "number",
|
|
||||||
updatedAt: "number",
|
|
||||||
config: volumeConfigSchema,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateVolumeDto = describeRoute({
|
export const updateVolumeDto = describeRoute({
|
||||||
@@ -174,6 +170,8 @@ export const updateVolumeDto = describeRoute({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection
|
* Test connection
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { volumesTable } from "../../db/schema";
|
|||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||||
import { createVolumeBackend } from "../backends/backend";
|
import { createVolumeBackend } from "../backends/backend";
|
||||||
|
import type { UpdateVolumeBody } from "./volume.dto";
|
||||||
|
|
||||||
const listVolumes = async () => {
|
const listVolumes = async () => {
|
||||||
const volumes = await db.query.volumesTable.findMany({});
|
const volumes = await db.query.volumesTable.findMany({});
|
||||||
@@ -114,7 +115,7 @@ const getVolume = async (name: string) => {
|
|||||||
return { volume, statfs };
|
return { volume, statfs };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVolume = async (name: string, backendConfig: BackendConfig) => {
|
const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
||||||
const existing = await db.query.volumesTable.findFirst({
|
const existing = await db.query.volumesTable.findFirst({
|
||||||
where: eq(volumesTable.name, name),
|
where: eq(volumesTable.name, name),
|
||||||
});
|
});
|
||||||
@@ -126,10 +127,10 @@ const updateVolume = async (name: string, backendConfig: BackendConfig) => {
|
|||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({
|
.set({
|
||||||
config: backendConfig,
|
config: volumeData.config,
|
||||||
type: backendConfig.backend,
|
type: volumeData.config?.backend,
|
||||||
|
autoRemount: volumeData.autoRemount,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
status: "unmounted",
|
|
||||||
})
|
})
|
||||||
.where(eq(volumesTable.name, name))
|
.where(eq(volumesTable.name, name))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
Reference in New Issue
Block a user