diff --git a/apps/client/app/api-client/@tanstack/react-query.gen.ts b/apps/client/app/api-client/@tanstack/react-query.gen.ts index 2903b75..38a20d7 100644 --- a/apps/client/app/api-client/@tanstack/react-query.gen.ts +++ b/apps/client/app/api-client/@tanstack/react-query.gen.ts @@ -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) => + createQueryKey("listRcloneRemotes", options); + +/** + * List all configured rclone remotes on the host system + */ +export const listRcloneRemotesOptions = (options?: Options) => { + 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) => - createQueryKey("listRcloneRemotes", options); - -/** - * List all configured rclone remotes on the host system - */ -export const listRcloneRemotesOptions = (options?: Options) => { - 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) => createQueryKey("listBackupSchedules", options); diff --git a/apps/client/app/api-client/sdk.gen.ts b/apps/client/app/api-client/sdk.gen.ts index 9d304ca..824ca97 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/apps/client/app/api-client/sdk.gen.ts @@ -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 = ( }); }; +/** + * List all configured rclone remotes on the host system + */ +export const listRcloneRemotes = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get({ + url: "/api/v1/repositories/rclone-remotes", + ...options, + }); +}; + /** * Delete a repository */ @@ -445,18 +457,6 @@ export const doctorRepository = ( }); }; -/** - * List all configured rclone remotes on the host system - */ -export const listRcloneRemotes = ( - options?: Options, -) => { - return (options?.client ?? _heyApiClient).get({ - url: "/api/v1/repositories/rclone-remotes", - ...options, - }); -}; - /** * List all backup schedules */ diff --git a/apps/client/app/api-client/types.gen.ts b/apps/client/app/api-client/types.gen.ts index 50cc622..27a9215 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -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; }; }; }; diff --git a/apps/client/app/components/create-repository-form.tsx b/apps/client/app/components/create-repository-form.tsx index 7cb7d9a..573cd9a 100644 --- a/apps/client/app/components/create-repository-form.tsx +++ b/apps/client/app/components/create-repository-form.tsx @@ -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 (
@@ -113,7 +118,16 @@ export const CreateRepositoryForm = ({ S3 Google Cloud Storage Azure Blob Storage - rclone (40+ cloud providers) + + + + rclone (40+ cloud providers) + + + +

Setup rclone to use this backend

+
+
Choose the storage backend for this repository. diff --git a/apps/client/app/modules/volumes/routes/volume-details.tsx b/apps/client/app/modules/volumes/routes/volume-details.tsx index 7ac12a7..fad319a 100644 --- a/apps/client/app/modules/volumes/routes/volume-details.tsx +++ b/apps/client/app/modules/volumes/routes/volume-details.tsx @@ -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) { Configuration Files - {dockerAvailable && Docker} + + + + Docker + + + +

Enable Docker support to access this tab.

+
+
diff --git a/apps/server/src/core/capabilities.ts b/apps/server/src/core/capabilities.ts index 1b87a42..9e6c42a 100644 --- a/apps/server/src/core/capabilities.ts +++ b/apps/server/src/core/capabilities.ts @@ -4,6 +4,7 @@ import { logger } from "../utils/logger"; export type SystemCapabilities = { docker: boolean; + rclone: boolean; }; let capabilitiesPromise: Promise | null = null; @@ -28,6 +29,7 @@ export async function getCapabilities(): Promise { async function detectCapabilities(): Promise { return { docker: await detectDocker(), + rclone: await detectRclone(), }; } @@ -53,3 +55,22 @@ async function detectDocker(): Promise { return false; } } + +/** + * Checks if rclone is available by: + * 1. Checking if /root/.config/rclone directory exists and is accessible + */ +async function detectRclone(): Promise { + 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; + } +} diff --git a/apps/server/src/modules/events/events.controller.ts b/apps/server/src/modules/events/events.controller.ts index 06bdb9f..971f0b9 100644 --- a/apps/server/src/modules/events/events.controller.ts +++ b/apps/server/src/modules/events/events.controller.ts @@ -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), diff --git a/apps/server/src/modules/system/system.dto.ts b/apps/server/src/modules/system/system.dto.ts index dc9bc85..4168719 100644 --- a/apps/server/src/modules/system/system.dto.ts +++ b/apps/server/src/modules/system/system.dto.ts @@ -3,6 +3,7 @@ import { describeRoute, resolver } from "hono-openapi"; export const capabilitiesSchema = type({ docker: "boolean", + rclone: "boolean", }); export const systemInfoResponse = type({ diff --git a/docker-compose.yml b/docker-compose.yml index 09109a3..c6423f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: