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,
|
browseFilesystem,
|
||||||
listRepositories,
|
listRepositories,
|
||||||
createRepository,
|
createRepository,
|
||||||
|
listRcloneRemotes,
|
||||||
deleteRepository,
|
deleteRepository,
|
||||||
getRepository,
|
getRepository,
|
||||||
listSnapshots,
|
listSnapshots,
|
||||||
@@ -29,7 +30,6 @@ import {
|
|||||||
listSnapshotFiles,
|
listSnapshotFiles,
|
||||||
restoreSnapshot,
|
restoreSnapshot,
|
||||||
doctorRepository,
|
doctorRepository,
|
||||||
listRcloneRemotes,
|
|
||||||
listBackupSchedules,
|
listBackupSchedules,
|
||||||
createBackupSchedule,
|
createBackupSchedule,
|
||||||
deleteBackupSchedule,
|
deleteBackupSchedule,
|
||||||
@@ -75,6 +75,7 @@ import type {
|
|||||||
ListRepositoriesData,
|
ListRepositoriesData,
|
||||||
CreateRepositoryData,
|
CreateRepositoryData,
|
||||||
CreateRepositoryResponse,
|
CreateRepositoryResponse,
|
||||||
|
ListRcloneRemotesData,
|
||||||
DeleteRepositoryData,
|
DeleteRepositoryData,
|
||||||
DeleteRepositoryResponse,
|
DeleteRepositoryResponse,
|
||||||
GetRepositoryData,
|
GetRepositoryData,
|
||||||
@@ -85,7 +86,6 @@ import type {
|
|||||||
RestoreSnapshotResponse,
|
RestoreSnapshotResponse,
|
||||||
DoctorRepositoryData,
|
DoctorRepositoryData,
|
||||||
DoctorRepositoryResponse,
|
DoctorRepositoryResponse,
|
||||||
ListRcloneRemotesData,
|
|
||||||
ListBackupSchedulesData,
|
ListBackupSchedulesData,
|
||||||
CreateBackupScheduleData,
|
CreateBackupScheduleData,
|
||||||
CreateBackupScheduleResponse,
|
CreateBackupScheduleResponse,
|
||||||
@@ -739,6 +739,27 @@ export const createRepositoryMutation = (
|
|||||||
return mutationOptions;
|
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
|
* Delete a repository
|
||||||
*/
|
*/
|
||||||
@@ -920,27 +941,6 @@ export const doctorRepositoryMutation = (
|
|||||||
return mutationOptions;
|
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>) =>
|
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) =>
|
||||||
createQueryKey("listBackupSchedules", options);
|
createQueryKey("listBackupSchedules", options);
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ import type {
|
|||||||
ListRepositoriesResponses,
|
ListRepositoriesResponses,
|
||||||
CreateRepositoryData,
|
CreateRepositoryData,
|
||||||
CreateRepositoryResponses,
|
CreateRepositoryResponses,
|
||||||
|
ListRcloneRemotesData,
|
||||||
|
ListRcloneRemotesResponses,
|
||||||
DeleteRepositoryData,
|
DeleteRepositoryData,
|
||||||
DeleteRepositoryResponses,
|
DeleteRepositoryResponses,
|
||||||
GetRepositoryData,
|
GetRepositoryData,
|
||||||
@@ -60,8 +62,6 @@ import type {
|
|||||||
RestoreSnapshotResponses,
|
RestoreSnapshotResponses,
|
||||||
DoctorRepositoryData,
|
DoctorRepositoryData,
|
||||||
DoctorRepositoryResponses,
|
DoctorRepositoryResponses,
|
||||||
ListRcloneRemotesData,
|
|
||||||
ListRcloneRemotesResponses,
|
|
||||||
ListBackupSchedulesData,
|
ListBackupSchedulesData,
|
||||||
ListBackupSchedulesResponses,
|
ListBackupSchedulesResponses,
|
||||||
CreateBackupScheduleData,
|
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
|
* 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
|
* List all backup schedules
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -828,6 +828,25 @@ export type CreateRepositoryResponses = {
|
|||||||
|
|
||||||
export type CreateRepositoryResponse = CreateRepositoryResponses[keyof 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 = {
|
export type DeleteRepositoryData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path: {
|
path: {
|
||||||
@@ -1052,25 +1071,6 @@ export type DoctorRepositoryResponses = {
|
|||||||
|
|
||||||
export type DoctorRepositoryResponse = DoctorRepositoryResponses[keyof 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 = {
|
export type ListBackupSchedulesData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
@@ -1648,6 +1648,7 @@ export type GetSystemInfoResponses = {
|
|||||||
200: {
|
200: {
|
||||||
capabilities: {
|
capabilities: {
|
||||||
docker: boolean;
|
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 { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
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 { useQuery } from "@tanstack/react-query";
|
||||||
import { Alert, AlertDescription } from "./ui/alert";
|
import { Alert, AlertDescription } from "./ui/alert";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
|
|
||||||
export const formSchema = type({
|
export const formSchema = type({
|
||||||
name: "2<=string<=32",
|
name: "2<=string<=32",
|
||||||
@@ -71,6 +72,10 @@ export const CreateRepositoryForm = ({
|
|||||||
}
|
}
|
||||||
}, [watchedBackend, watchedName, form]);
|
}, [watchedBackend, watchedName, form]);
|
||||||
|
|
||||||
|
const { data: systemInfo } = useQuery({
|
||||||
|
...getSystemInfoOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
|
<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="s3">S3</SelectItem>
|
||||||
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
||||||
<SelectItem value="azure">Azure Blob 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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>Choose the storage backend for this repository.</FormDescription>
|
<FormDescription>Choose the storage backend for this repository.</FormDescription>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { getVolume } from "~/api-client";
|
|||||||
import { VolumeInfoTabContent } from "../tabs/info";
|
import { VolumeInfoTabContent } from "../tabs/info";
|
||||||
import { FilesTabContent } from "../tabs/files";
|
import { FilesTabContent } from "../tabs/files";
|
||||||
import { DockerTabContent } from "../tabs/docker";
|
import { DockerTabContent } from "../tabs/docker";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
|
||||||
|
|
||||||
export function meta({ params }: Route.MetaArgs) {
|
export function meta({ params }: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
@@ -150,7 +151,16 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
|||||||
<TabsList className="mb-2">
|
<TabsList className="mb-2">
|
||||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||||
<TabsTrigger value="files">Files</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>
|
</TabsList>
|
||||||
<TabsContent value="info">
|
<TabsContent value="info">
|
||||||
<VolumeInfoTabContent volume={volume} statfs={statfs} />
|
<VolumeInfoTabContent volume={volume} statfs={statfs} />
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { logger } from "../utils/logger";
|
|||||||
|
|
||||||
export type SystemCapabilities = {
|
export type SystemCapabilities = {
|
||||||
docker: boolean;
|
docker: boolean;
|
||||||
|
rclone: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
|
let capabilitiesPromise: Promise<SystemCapabilities> | null = null;
|
||||||
@@ -28,6 +29,7 @@ export async function getCapabilities(): Promise<SystemCapabilities> {
|
|||||||
async function detectCapabilities(): Promise<SystemCapabilities> {
|
async function detectCapabilities(): Promise<SystemCapabilities> {
|
||||||
return {
|
return {
|
||||||
docker: await detectDocker(),
|
docker: await detectDocker(),
|
||||||
|
rclone: await detectRclone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,3 +55,22 @@ async function detectDocker(): Promise<boolean> {
|
|||||||
return false;
|
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;
|
scheduleId: number;
|
||||||
volumeName: string;
|
volumeName: string;
|
||||||
repositoryName: string;
|
repositoryName: string;
|
||||||
secondsElapsed: number;
|
seconds_elapsed: number;
|
||||||
percentDone: number;
|
percent_done: number;
|
||||||
totalFiles: number;
|
total_files: number;
|
||||||
filesDone: number;
|
files_done: number;
|
||||||
totalBytes: number;
|
total_bytes: number;
|
||||||
bytesDone: number;
|
bytes_done: number;
|
||||||
currentFiles: string[];
|
current_files: string[];
|
||||||
}) => {
|
}) => {
|
||||||
stream.writeSSE({
|
stream.writeSSE({
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { describeRoute, resolver } from "hono-openapi";
|
|||||||
|
|
||||||
export const capabilitiesSchema = type({
|
export const capabilitiesSchema = type({
|
||||||
docker: "boolean",
|
docker: "boolean",
|
||||||
|
rclone: "boolean",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const systemInfoResponse = type({
|
export const systemInfoResponse = type({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
|
|
||||||
- ./apps/client/app:/app/apps/client/app
|
- ./apps/client/app:/app/apps/client/app
|
||||||
- ./apps/server/src:/app/apps/server/src
|
- ./apps/server/src:/app/apps/server/src
|
||||||
- ~/.config/rclone:/root/.config/rclone
|
# - ~/.config/rclone:/root/.config/rclone
|
||||||
|
|
||||||
ironmount-prod:
|
ironmount-prod:
|
||||||
build:
|
build:
|
||||||
|
|||||||
Reference in New Issue
Block a user