From 36b0282d18fac8f76ba28e5a6bc6073d4da26c45 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Tue, 11 Nov 2025 21:31:10 +0100 Subject: [PATCH] feat(frontend): rclone repositories config --- README.md | 62 ++++++++++++++- .../api-client/@tanstack/react-query.gen.ts | 23 ++++++ apps/client/app/api-client/sdk.gen.ts | 14 ++++ apps/client/app/api-client/types.gen.ts | 59 ++++++++++++-- .../app/components/create-repository-form.tsx | 79 +++++++++++++++++++ .../components/snapshot-file-browser.tsx | 2 +- .../repositories/repositories.controller.ts | 30 +++---- apps/server/src/utils/rclone.ts | 31 +++----- apps/server/src/utils/restic.ts | 7 +- docker-compose.yml | 1 + 10 files changed, 265 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 49df0e5..a6db383 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,69 @@ Now, when adding a new volume in the Ironmount web interface, you can select "Di ## Creating a repository -A repository is where your backups will be securely stored encrypted. Ironmount currently supports S3-compatible storage backends and local directories for storing your backup repositories. +A repository is where your backups will be securely stored encrypted. Ironmount supports multiple storage backends for your backup repositories: + +- **Local directories** - Store backups on local disk at `/var/lib/ironmount/repositories/` +- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc. +- **Google Cloud Storage** - Google's cloud storage service +- **Azure Blob Storage** - Microsoft Azure storage +- **rclone remotes** - 40+ cloud storage providers via rclone (see below) Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features. -To create a repository, navigate to the "Repositories" section in the web interface and click on "Create repository". Fill in the required details such as repository name, type, and connection settings. If you choose a local directory as the repository type, your backups will be stored at `/var/lib/ironmount/repositories/`. +To create a repository, navigate to the "Repositories" section in the web interface and click on "Create repository". Fill in the required details such as repository name, type, and connection settings. + +### Using rclone for cloud storage + +Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage providers including Google Drive, Dropbox, OneDrive, Box, pCloud, Mega, and many more. This gives you the flexibility to store your backups on virtually any cloud storage service. + +**Setup instructions:** + +1. **Install rclone on your host system** (if not already installed): + ```bash + curl https://rclone.org/install.sh | sudo bash + ``` + +2. **Configure your cloud storage remote** using rclone's interactive config: + ```bash + rclone config + ``` + Follow the prompts to set up your cloud storage provider. For OAuth providers (Google Drive, Dropbox, etc.), rclone will guide you through the authentication flow. + +3. **Verify your remote is configured**: + ```bash + rclone listremotes + ``` + +4. **Mount the rclone config into the Ironmount container** by updating your `docker-compose.yml`: + ```diff + services: + ironmount: + image: ghcr.io/nicotsx/ironmount:v0.6 + container_name: ironmount + restart: unless-stopped + privileged: true + ports: + - "4096:4096" + devices: + - /dev/fuse:/dev/fuse + volumes: + - /var/lib/ironmount:/var/lib/ironmount + + - ~/.config/rclone:/root/.config/rclone + ``` + +5. **Restart the Ironmount container**: + ```bash + docker compose down + docker compose up -d + ``` + +6. **Create a repository** in Ironmount: + - Select "rclone" as the repository type + - Choose your configured remote from the dropdown + - Specify the path within your remote (e.g., `backups/ironmount`) + +For a complete list of supported providers, see the [rclone documentation](https://rclone.org/). ## Your first backup job 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 7f47a15..2903b75 100644 --- a/apps/client/app/api-client/@tanstack/react-query.gen.ts +++ b/apps/client/app/api-client/@tanstack/react-query.gen.ts @@ -29,6 +29,7 @@ import { listSnapshotFiles, restoreSnapshot, doctorRepository, + listRcloneRemotes, listBackupSchedules, createBackupSchedule, deleteBackupSchedule, @@ -84,6 +85,7 @@ import type { RestoreSnapshotResponse, DoctorRepositoryData, DoctorRepositoryResponse, + ListRcloneRemotesData, ListBackupSchedulesData, CreateBackupScheduleData, CreateBackupScheduleResponse, @@ -918,6 +920,27 @@ 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 f5d1dc2..9d304ca 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/apps/client/app/api-client/sdk.gen.ts @@ -60,6 +60,8 @@ import type { RestoreSnapshotResponses, DoctorRepositoryData, DoctorRepositoryResponses, + ListRcloneRemotesData, + ListRcloneRemotesResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, CreateBackupScheduleData, @@ -443,6 +445,18 @@ 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 7648bed..50cc622 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -754,6 +754,11 @@ export type ListRepositoriesResponses = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; createdAt: number; id: string; @@ -761,7 +766,7 @@ export type ListRepositoriesResponses = { lastError: string | null; name: string; status: "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "s3"; + type: "azure" | "gcs" | "local" | "rclone" | "s3"; updatedAt: number; }>; }; @@ -794,6 +799,11 @@ export type CreateRepositoryData = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; name: string; compressionMode?: "auto" | "better" | "fastest" | "max" | "off"; @@ -877,6 +887,11 @@ export type GetRepositoryResponses = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; createdAt: number; id: string; @@ -884,7 +899,7 @@ export type GetRepositoryResponses = { lastError: string | null; name: string; status: "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "s3"; + type: "azure" | "gcs" | "local" | "rclone" | "s3"; updatedAt: number; }; }; @@ -1037,6 +1052,25 @@ 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; @@ -1085,6 +1119,11 @@ export type ListBackupSchedulesResponses = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; createdAt: number; id: string; @@ -1092,7 +1131,7 @@ export type ListBackupSchedulesResponses = { lastError: string | null; name: string; status: "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "s3"; + type: "azure" | "gcs" | "local" | "rclone" | "s3"; updatedAt: number; }; repositoryId: string; @@ -1284,6 +1323,11 @@ export type GetBackupScheduleResponses = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; createdAt: number; id: string; @@ -1291,7 +1335,7 @@ export type GetBackupScheduleResponses = { lastError: string | null; name: string; status: "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "s3"; + type: "azure" | "gcs" | "local" | "rclone" | "s3"; updatedAt: number; }; repositoryId: string; @@ -1464,6 +1508,11 @@ export type GetBackupScheduleForVolumeResponses = { | { backend: "local"; name: string; + } + | { + backend: "rclone"; + path: string; + remote: string; }; createdAt: number; id: string; @@ -1471,7 +1520,7 @@ export type GetBackupScheduleForVolumeResponses = { lastError: string | null; name: string; status: "error" | "healthy" | "unknown" | null; - type: "azure" | "gcs" | "local" | "s3"; + type: "azure" | "gcs" | "local" | "rclone" | "s3"; updatedAt: number; }; repositoryId: string; diff --git a/apps/client/app/components/create-repository-form.tsx b/apps/client/app/components/create-repository-form.tsx index 977903c..7cb7d9a 100644 --- a/apps/client/app/components/create-repository-form.tsx +++ b/apps/client/app/components/create-repository-form.tsx @@ -9,6 +9,10 @@ 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 { useQuery } from "@tanstack/react-query"; +import { Alert, AlertDescription } from "./ui/alert"; +import { ExternalLink } from "lucide-react"; export const formSchema = type({ name: "2<=string<=32", @@ -32,6 +36,7 @@ const defaultValuesForType = { s3: { backend: "s3" as const, compressionMode: "auto" as const }, gcs: { backend: "gcs" as const, compressionMode: "auto" as const }, azure: { backend: "azure" as const, compressionMode: "auto" as const }, + rclone: { backend: "rclone" as const, compressionMode: "auto" as const }, }; export const CreateRepositoryForm = ({ @@ -56,6 +61,10 @@ export const CreateRepositoryForm = ({ const watchedBackend = watch("backend"); const watchedName = watch("name"); + const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ + ...listRcloneRemotesOptions(), + }); + useEffect(() => { if (watchedBackend && watchedBackend in defaultValuesForType) { form.reset({ name: watchedName, ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType] }); @@ -104,6 +113,7 @@ export const CreateRepositoryForm = ({ S3 Google Cloud Storage Azure Blob Storage + rclone (40+ cloud providers) Choose the storage backend for this repository. @@ -307,6 +317,75 @@ export const CreateRepositoryForm = ({ )} + {watchedBackend === "rclone" && + (!rcloneRemotes || rcloneRemotes.length === 0 ? ( + + +

No rclone remotes configured

+

+ To use rclone, you need to configure remotes on your host system +

+ + View rclone documentation + + +
+
+ ) : ( + <> + ( + + Remote + + Select the rclone remote configured on your host system. + + + )} + /> + ( + + Path + + + + Path within the remote where backups will be stored. + + + )} + /> + + ))} + {mode === "update" && (