mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
refactor: rclone system capability
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
browseFilesystem,
|
||||
listRepositories,
|
||||
createRepository,
|
||||
listRcloneRemotes,
|
||||
deleteRepository,
|
||||
getRepository,
|
||||
listSnapshots,
|
||||
@@ -29,7 +30,6 @@ import {
|
||||
listSnapshotFiles,
|
||||
restoreSnapshot,
|
||||
doctorRepository,
|
||||
listRcloneRemotes,
|
||||
listBackupSchedules,
|
||||
createBackupSchedule,
|
||||
deleteBackupSchedule,
|
||||
@@ -75,6 +75,7 @@ import type {
|
||||
ListRepositoriesData,
|
||||
CreateRepositoryData,
|
||||
CreateRepositoryResponse,
|
||||
ListRcloneRemotesData,
|
||||
DeleteRepositoryData,
|
||||
DeleteRepositoryResponse,
|
||||
GetRepositoryData,
|
||||
@@ -85,7 +86,6 @@ import type {
|
||||
RestoreSnapshotResponse,
|
||||
DoctorRepositoryData,
|
||||
DoctorRepositoryResponse,
|
||||
ListRcloneRemotesData,
|
||||
ListBackupSchedulesData,
|
||||
CreateBackupScheduleData,
|
||||
CreateBackupScheduleResponse,
|
||||
@@ -739,6 +739,27 @@ export const createRepositoryMutation = (
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) =>
|
||||
createQueryKey("listRcloneRemotes", options);
|
||||
|
||||
/**
|
||||
* List all configured rclone remotes on the host system
|
||||
*/
|
||||
export const listRcloneRemotesOptions = (options?: Options<ListRcloneRemotesData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await listRcloneRemotes({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: listRcloneRemotesQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a repository
|
||||
*/
|
||||
@@ -920,27 +941,6 @@ export const doctorRepositoryMutation = (
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) =>
|
||||
createQueryKey("listRcloneRemotes", options);
|
||||
|
||||
/**
|
||||
* List all configured rclone remotes on the host system
|
||||
*/
|
||||
export const listRcloneRemotesOptions = (options?: Options<ListRcloneRemotesData>) => {
|
||||
return queryOptions({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await listRcloneRemotes({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: listRcloneRemotesQueryKey(options),
|
||||
});
|
||||
};
|
||||
|
||||
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) =>
|
||||
createQueryKey("listBackupSchedules", options);
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ import type {
|
||||
ListRepositoriesResponses,
|
||||
CreateRepositoryData,
|
||||
CreateRepositoryResponses,
|
||||
ListRcloneRemotesData,
|
||||
ListRcloneRemotesResponses,
|
||||
DeleteRepositoryData,
|
||||
DeleteRepositoryResponses,
|
||||
GetRepositoryData,
|
||||
@@ -60,8 +62,6 @@ import type {
|
||||
RestoreSnapshotResponses,
|
||||
DoctorRepositoryData,
|
||||
DoctorRepositoryResponses,
|
||||
ListRcloneRemotesData,
|
||||
ListRcloneRemotesResponses,
|
||||
ListBackupSchedulesData,
|
||||
ListBackupSchedulesResponses,
|
||||
CreateBackupScheduleData,
|
||||
@@ -357,6 +357,18 @@ export const createRepository = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List all configured rclone remotes on the host system
|
||||
*/
|
||||
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ListRcloneRemotesData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/repositories/rclone-remotes",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a repository
|
||||
*/
|
||||
@@ -445,18 +457,6 @@ export const doctorRepository = <ThrowOnError extends boolean = false>(
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List all configured rclone remotes on the host system
|
||||
*/
|
||||
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(
|
||||
options?: Options<ListRcloneRemotesData, ThrowOnError>,
|
||||
) => {
|
||||
return (options?.client ?? _heyApiClient).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
|
||||
url: "/api/v1/repositories/rclone-remotes",
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* List all backup schedules
|
||||
*/
|
||||
|
||||
@@ -828,6 +828,25 @@ export type CreateRepositoryResponses = {
|
||||
|
||||
export type CreateRepositoryResponse = CreateRepositoryResponses[keyof CreateRepositoryResponses];
|
||||
|
||||
export type ListRcloneRemotesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/api/v1/repositories/rclone-remotes";
|
||||
};
|
||||
|
||||
export type ListRcloneRemotesResponses = {
|
||||
/**
|
||||
* List of rclone remotes
|
||||
*/
|
||||
200: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses];
|
||||
|
||||
export type DeleteRepositoryData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -1052,25 +1071,6 @@ export type DoctorRepositoryResponses = {
|
||||
|
||||
export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof DoctorRepositoryResponses];
|
||||
|
||||
export type ListRcloneRemotesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: "/api/v1/repositories/rclone-remotes";
|
||||
};
|
||||
|
||||
export type ListRcloneRemotesResponses = {
|
||||
/**
|
||||
* List of rclone remotes
|
||||
*/
|
||||
200: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ListRcloneRemotesResponse = ListRcloneRemotesResponses[keyof ListRcloneRemotesResponses];
|
||||
|
||||
export type ListBackupSchedulesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -1648,6 +1648,7 @@ export type GetSystemInfoResponses = {
|
||||
200: {
|
||||
capabilities: {
|
||||
docker: boolean;
|
||||
rclone: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,10 +9,11 @@ import { Button } from "./ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
||||
import { Input } from "./ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import { listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { getSystemInfoOptions, listRcloneRemotesOptions } from "~/api-client/@tanstack/react-query.gen";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
@@ -71,6 +72,10 @@ export const CreateRepositoryForm = ({
|
||||
}
|
||||
}, [watchedBackend, watchedName, form]);
|
||||
|
||||
const { data: systemInfo } = useQuery({
|
||||
...getSystemInfoOptions(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
|
||||
@@ -113,7 +118,16 @@ export const CreateRepositoryForm = ({
|
||||
<SelectItem value="s3">S3</SelectItem>
|
||||
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
||||
<SelectItem value="azure">Azure Blob Storage</SelectItem>
|
||||
<SelectItem value="rclone">rclone (40+ cloud providers)</SelectItem>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SelectItem disabled={!systemInfo?.capabilities.rclone} value="rclone">
|
||||
rclone (40+ cloud providers)
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={cn({ hidden: systemInfo?.capabilities.rclone })}>
|
||||
<p>Setup rclone to use this backend</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Choose the storage backend for this repository.</FormDescription>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getVolume } from "~/api-client";
|
||||
import { VolumeInfoTabContent } from "../tabs/info";
|
||||
import { FilesTabContent } from "../tabs/files";
|
||||
import { DockerTabContent } from "../tabs/docker";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
return [
|
||||
@@ -150,7 +151,16 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="files">Files</TabsTrigger>
|
||||
{dockerAvailable && <TabsTrigger value="docker">Docker</TabsTrigger>}
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<TabsTrigger disabled={!dockerAvailable} value="docker">
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={cn({ hidden: dockerAvailable })}>
|
||||
<p>Enable Docker support to access this tab.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TabsList>
|
||||
<TabsContent value="info">
|
||||
<VolumeInfoTabContent volume={volume} statfs={statfs} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
|
||||
|
||||
export type SystemCapabilities = {
|
||||
docker: boolean;
|
||||
rclone: boolean;
|
||||
};
|
||||
|
||||
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
|
||||
@@ -28,6 +29,7 @@ export async function getCapabilities(): Promise<SystemCapabilities> {
|
||||
async function detectCapabilities(): Promise<SystemCapabilities> {
|
||||
return {
|
||||
docker: await detectDocker(),
|
||||
rclone: await detectRclone(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,3 +55,22 @@ async function detectDocker(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if rclone is available by:
|
||||
* 1. Checking if /root/.config/rclone directory exists and is accessible
|
||||
*/
|
||||
async function detectRclone(): Promise<boolean> {
|
||||
try {
|
||||
await fs.access("/root/.config/rclone");
|
||||
|
||||
logger.info("rclone capability: enabled");
|
||||
return true;
|
||||
} catch (_) {
|
||||
logger.warn(
|
||||
"rclone capability: disabled. " +
|
||||
"To enable: mount /root/.config/rclone in docker-compose.yml",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ export const eventsController = new Hono().get("/", (c) => {
|
||||
scheduleId: number;
|
||||
volumeName: string;
|
||||
repositoryName: string;
|
||||
secondsElapsed: number;
|
||||
percentDone: number;
|
||||
totalFiles: number;
|
||||
filesDone: number;
|
||||
totalBytes: number;
|
||||
bytesDone: number;
|
||||
currentFiles: string[];
|
||||
seconds_elapsed: number;
|
||||
percent_done: number;
|
||||
total_files: number;
|
||||
files_done: number;
|
||||
total_bytes: number;
|
||||
bytes_done: number;
|
||||
current_files: string[];
|
||||
}) => {
|
||||
stream.writeSSE({
|
||||
data: JSON.stringify(data),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describeRoute, resolver } from "hono-openapi";
|
||||
|
||||
export const capabilitiesSchema = type({
|
||||
docker: "boolean",
|
||||
rclone: "boolean",
|
||||
});
|
||||
|
||||
export const systemInfoResponse = type({
|
||||
|
||||
@@ -19,7 +19,7 @@ services:
|
||||
|
||||
- ./apps/client/app:/app/apps/client/app
|
||||
- ./apps/server/src:/app/apps/server/src
|
||||
- ~/.config/rclone:/root/.config/rclone
|
||||
# - ~/.config/rclone:/root/.config/rclone
|
||||
|
||||
ironmount-prod:
|
||||
build:
|
||||
|
||||
Reference in New Issue
Block a user