diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..11d4b9cf --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +- This project uses the AGENTS.md file to give detailed information about the repository structure and development commands. Make sure to read this file before starting development. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16e8d9a0..dadfa1b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,8 +62,6 @@ jobs: type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }} flavor: | latest=${{ needs.determine-release-type.outputs.release_type == 'release' }} - cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache - cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max - name: Build and push images uses: docker/build-push-action@v6 @@ -76,6 +74,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | APP_VERSION=${{ needs.determine-release-type.outputs.tagname }} + cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache + cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max publish-release: runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..136692d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,267 @@ +# AGENTS.md + +## Important instructions + +- Never create migration files manually. Always use the provided command to generate migrations +- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration + +## Project Overview + +Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage). + +## Technology Stack + +- **Runtime**: Bun 1.3.1 +- **Server**: Hono (web framework) with Bun runtime +- **Client**: React Router v7 (SSR) with React 19 +- **Database**: SQLite with Drizzle ORM +- **Validation**: ArkType for runtime schema validation +- **Styling**: Tailwind CSS v4 + Radix UI components +- **Architecture**: Unified application structure (not a monorepo) +- **Code Quality**: Biome (formatter & linter) +- **Containerization**: Docker with multi-stage builds + +## Repository Structure + +This is a unified application with the following structure: + +- `app/server` - Bun-based API server with Hono +- `app/client` - React Router SSR frontend components and modules +- `app/schemas` - Shared ArkType schemas for validation +- `app/drizzle` - Database migrations + +### Type Checking + +```bash +# Run type checking and generate React Router types +bun run tsc +``` + +### Building + +```bash +# Build for production +bun run build +``` + +### Database Migrations + +```bash +# Generate new migration from schema changes +bun gen:migrations + +# Generate a custom empty migration +bunx drizzle-kit generate --custom --name=fix-timestamps-to-ms + +``` + +### API Client Generation + +```bash +# Generate TypeScript API client from OpenAPI spec +# Note: Server is always running don't need to start it separately +bun run gen:api-client +``` + +### Code Quality + +```bash +# Format and lint (Biome) +bunx biome check --write . + +# Format only +bunx biome format --write . + +# Lint only +bunx biome lint . +``` + +## Architecture + +### Server Architecture + +The server follows a modular service-oriented architecture: + +**Entry Point**: `app/server/index.ts` + +- Initializes servers using `react-router-hono-server`: + 1. Main API server on port 4096 (REST API + serves static frontend) + 2. Docker volume plugin server on Unix socket `/run/docker/plugins/zerobyte.sock` (optional, if Docker is available) + +**Modules** (`app/server/modules/`): +Each module follows a controller � service � database pattern: + +- `auth/` - User authentication and session management +- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, directories) +- `repositories/` - Restic repository management (S3, Azure, GCS, local, rclone) +- `backups/` - Backup schedule management and execution +- `notifications/` - Notification system with multiple providers (Discord, email, Gotify, Ntfy, Slack, Pushover) +- `driver/` - Docker volume plugin implementation +- `events/` - Server-Sent Events for real-time updates +- `system/` - System information and capabilities +- `lifecycle/` - Application startup/shutdown hooks + +**Backends** (`app/server/modules/backends/`): +Each volume backend (NFS, SMB, WebDAV, directory) implements mounting logic using system tools (mount.nfs, mount.cifs, davfs2). + +**Jobs** (`app/server/jobs/`): +Cron-based background jobs managed by the Scheduler: + +- `backup-execution.ts` - Runs scheduled backups (every minute) +- `cleanup-dangling.ts` - Removes stale mounts (hourly) +- `healthchecks.ts` - Checks volume health (every 5 minutes) +- `repository-healthchecks.ts` - Validates repositories (every 10 minutes) +- `cleanup-sessions.ts` - Expires old sessions (daily) + +**Core** (`app/server/core/`): + +- `scheduler.ts` - Job scheduling system using node-cron +- `capabilities.ts` - Detects available system features (Docker support, etc.) +- `constants.ts` - Application-wide constants + +**Utils** (`app/server/utils/`): + +- `restic.ts` - Restic CLI wrapper with type-safe output parsing +- `spawn.ts` - Safe subprocess execution helpers +- `logger.ts` - Winston-based logging +- `crypto.ts` - Encryption utilities +- `errors.ts` - Error handling middleware + +**Database** (`app/server/db/`): + +- Uses Drizzle ORM with SQLite +- Schema in `schema.ts` defines: volumes, repositories, backup schedules, notifications, users, sessions +- Migrations: `app/drizzle/` + +### Client Architecture + +**Framework**: React Router v7 with SSR +**Entry Point**: `app/root.tsx` + +The client uses: + +- TanStack Query for server state management +- Auto-generated API client from OpenAPI spec (in `app/client/api-client/`) +- Radix UI primitives with custom Tailwind styling +- Server-Sent Events hook (`use-server-events.ts`) for real-time updates + +Routes are organized in feature modules at `app/client/modules/*/routes/`. + +### Shared Schemas + +`app/schemas/` contains ArkType schemas used by both client and server: + +- Volume configurations (NFS, SMB, WebDAV, directory) +- Repository configurations (S3, Azure, GCS, local, rclone) +- Restic command output parsing types +- Backend status types + +These schemas provide runtime validation and TypeScript types. + +## Restic Integration + +Zerobyte is a wrapper around Restic for backup operations. Key integration points: + +**Repository Management**: + +- Creates/initializes Restic repositories via `restic init` +- Supports multiple backends: local, S3, Azure Blob Storage, Google Cloud Storage, or any rclone-supported backend +- Stores single encryption password in `/var/lib/zerobyte/restic/password` (auto-generated on first run) + +**Backup Operations**: + +- Executes `restic backup` with user-defined schedules (cron expressions) +- Supports include/exclude patterns for selective backups +- Parses JSON output for progress tracking and statistics +- Implements retention policies via `restic forget --prune` + +**Repository Utilities** (`utils/restic.ts`): + +- `buildRepoUrl()` - Constructs repository URLs for different backends +- `buildEnv()` - Sets environment variables (credentials, cache dir) +- `ensurePassfile()` - Manages encryption password file +- Type-safe parsing of Restic JSON output using ArkType schemas + +**Rclone Integration** (`app/server/modules/repositories/`): + +- Allows using any rclone backend as a Restic repository +- Dynamically generates rclone config and passes via environment variables +- Supports backends like Dropbox, Google Drive, OneDrive, Backblaze B2, etc. + +## Docker Volume Plugin + +When Docker socket is available (`/var/run/docker.sock`), Zerobyte registers as a Docker volume plugin: + +**Plugin Location**: `/run/docker/plugins/zerobyte.sock` +**Implementation**: `app/server/modules/driver/driver.controller.ts` + +This allows other containers to mount Zerobyte volumes using Docker. + +The plugin implements the Docker Volume Plugin API v1. + +## Environment & Configuration + +**Runtime Environment Variables**: + +- Database path: `./data/zerobyte.db` (configurable via `drizzle.config.ts`) +- Restic cache: `/var/lib/zerobyte/restic/cache` +- Restic password: `/var/lib/zerobyte/restic/password` +- Volume mounts: `/var/lib/zerobyte/mounts/` +- Local repositories: `/var/lib/zerobyte/repositories/` + +**Capabilities Detection**: +On startup, the server detects available capabilities (see `core/capabilities.ts`): + +- **Docker**: Requires `/var/run/docker.sock` access +- System will gracefully degrade if capabilities are unavailable + +## Common Workflows + +### Adding a New Volume Backend + +1. Create backend implementation in `app/server/modules/backends//` +2. Implement `mount()` and `unmount()` methods +3. Add schema to `app/schemas/volumes.ts` +4. Update `volumeConfigSchema` discriminated union +5. Update backend factory in `app/server/modules/backends/backend.ts` + +### Adding a New Repository Backend + +1. Add backend type to `app/schemas/restic.ts` +2. Update `buildRepoUrl()` in `app/server/utils/restic.ts` +3. Update `buildEnv()` to handle credentials/configuration +4. Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts` +5. Update repository service to handle new backend + +### Adding a New Scheduled Job + +1. Create job class in `app/server/jobs/.ts` extending `Job` +2. Implement `run()` method +3. Register in `app/server/modules/lifecycle/startup.ts` with cron expression: + ```typescript + Scheduler.build(YourJob).schedule("* * * * *"); + ``` + +## Important Notes + +- **Code Style**: Uses Biome with tabs (not spaces), 120 char line width, double quotes +- **Imports**: Organize imports is disabled in Biome - do not auto-organize +- **TypeScript**: Uses `"type": "module"` - all imports must include extensions when targeting Node/Bun +- **Validation**: Prefer ArkType over Zod - it's used throughout the codebase +- **Database**: Timestamps are stored as Unix epoch integers, not ISO strings +- **Security**: Restic password file has 0600 permissions - never expose it +- **Mounting**: Requires privileged container or CAP_SYS_ADMIN for FUSE mounts +- **API Documentation**: OpenAPI spec auto-generated at `/api/v1/openapi.json`, docs at `/api/v1/docs` + +## Docker Development Setup + +The `docker-compose.yml` defines two services: + +- `zerobyte-dev` - Development with hot reload (uses `development` stage) +- `zerobyte-prod` - Production build (uses `production` stage) + +Both mount: + +- `/var/lib/zerobyte` for persistent data +- `/dev/fuse` device for FUSE mounting +- Optionally `/var/run/docker.sock` for Docker plugin functionality diff --git a/Dockerfile b/Dockerfile index 52676669..206d294b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -ARG BUN_VERSION="1.3.1" +ARG BUN_VERSION="1.3.3" FROM oven/bun:${BUN_VERSION}-alpine AS base -RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client +RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 # ------------------------------ @@ -14,7 +14,7 @@ WORKDIR /deps ARG TARGETARCH ARG RESTIC_VERSION="0.18.1" -ARG SHOUTRRR_VERSION="0.12.0" +ARG SHOUTRRR_VERSION="0.12.1" ENV TARGETARCH=${TARGETARCH} RUN apk add --no-cache curl bzip2 unzip tar diff --git a/README.md b/README.md index f9838551..226a38f9 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ In order to run Zerobyte, you need to have Docker and Docker Compose installed o ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.13 + image: ghcr.io/nicotsx/zerobyte:v0.18 container_name: zerobyte restart: unless-stopped cap_add: @@ -78,7 +78,7 @@ If you want to track a local directory on the same server where Zerobyte is runn ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.13 + image: ghcr.io/nicotsx/zerobyte:v0.18 container_name: zerobyte restart: unless-stopped cap_add: @@ -146,7 +146,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.13 + image: ghcr.io/nicotsx/zerobyte:v0.18 container_name: zerobyte restart: unless-stopped cap_add: @@ -223,7 +223,7 @@ In order to enable this feature, you need to change your bind mount `/var/lib/ze ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.13 + image: ghcr.io/nicotsx/zerobyte:v0.18 container_name: zerobyte restart: unless-stopped ports: @@ -254,7 +254,7 @@ In order to enable this feature, you need to run Zerobyte with several items sha ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.13 + image: ghcr.io/nicotsx/zerobyte:v0.18 container_name: zerobyte restart: unless-stopped cap_add: diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index fab5ce72..83ae179c 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -3,8 +3,8 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { client } from '../client.gen'; -import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getNotificationDestination, getRepository, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleNotifications, updateVolume } from '../sdk.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; +import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, exportFullConfig, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getMirrorCompatibility, getNotificationDestination, getRepository, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, testNotificationDestination, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateVolume } from '../sdk.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, ExportFullConfigData, ExportFullConfigError, ExportFullConfigResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen'; /** * Register a new user @@ -755,6 +755,59 @@ export const updateScheduleNotificationsMutation = (options?: Partial) => createQueryKey("getScheduleMirrors", options); + +/** + * Get mirror repository assignments for a backup schedule + */ +export const getScheduleMirrorsOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getScheduleMirrors({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getScheduleMirrorsQueryKey(options) +}); + +/** + * Update mirror repository assignments for a backup schedule + */ +export const updateScheduleMirrorsMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateScheduleMirrors({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +export const getMirrorCompatibilityQueryKey = (options: Options) => createQueryKey("getMirrorCompatibility", options); + +/** + * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository + */ +export const getMirrorCompatibilityOptions = (options: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMirrorCompatibility({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getMirrorCompatibilityQueryKey(options) +}); + export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey("listNotificationDestinations", options); /** diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 7778eb87..6d83b761 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; +import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, ExportFullConfigData, ExportFullConfigErrors, ExportFullConfigResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen'; export type Options = Options2 & { /** @@ -476,6 +476,40 @@ export const updateScheduleNotifications = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/v1/backups/{scheduleId}/mirrors', + ...options + }); +}; + +/** + * Update mirror repository assignments for a backup schedule + */ +export const updateScheduleMirrors = (options: Options) => { + return (options.client ?? client).put({ + url: '/api/v1/backups/{scheduleId}/mirrors', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); +}; + +/** + * Get mirror compatibility info for all repositories relative to a backup schedule's primary repository + */ +export const getMirrorCompatibility = (options: Options) => { + return (options.client ?? client).get({ + url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', + ...options + }); +}; + /** * List all notification destinations */ diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index d196feef..3b0fb9bb 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -164,6 +164,11 @@ export type ListVolumesResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -191,7 +196,7 @@ export type ListVolumesResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }>; }; @@ -211,6 +216,11 @@ export type CreateVolumeData = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -255,6 +265,11 @@ export type CreateVolumeResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -282,7 +297,7 @@ export type CreateVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -302,6 +317,11 @@ export type TestConnectionData = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -399,6 +419,11 @@ export type GetVolumeResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -426,7 +451,7 @@ export type GetVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -448,6 +473,11 @@ export type UpdateVolumeData = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -501,6 +531,11 @@ export type UpdateVolumeResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -528,7 +563,7 @@ export type UpdateVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -1124,6 +1159,7 @@ export type ListSnapshotsResponses = { paths: Array; short_id: string; size: number; + tags: Array; time: number; }>; }; @@ -1170,6 +1206,7 @@ export type GetSnapshotDetailsResponses = { paths: Array; short_id: string; size: number; + tags: Array; time: number; }; }; @@ -1289,12 +1326,14 @@ export type ListBackupSchedulesResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; @@ -1393,6 +1432,11 @@ export type ListBackupSchedulesResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1420,7 +1464,7 @@ export type ListBackupSchedulesResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -1433,8 +1477,10 @@ export type CreateBackupScheduleData = { body?: { cronExpression: string; enabled: boolean; + name: string; repositoryId: string; volumeId: number; + excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; retentionPolicy?: { @@ -1461,12 +1507,14 @@ export type CreateBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repositoryId: string; retentionPolicy: { @@ -1522,12 +1570,14 @@ export type GetBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; @@ -1626,6 +1676,11 @@ export type GetBackupScheduleResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1653,7 +1708,7 @@ export type GetBackupScheduleResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -1667,8 +1722,10 @@ export type UpdateBackupScheduleData = { cronExpression: string; repositoryId: string; enabled?: boolean; + excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; + name?: string; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1695,12 +1752,14 @@ export type UpdateBackupScheduleResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repositoryId: string; retentionPolicy: { @@ -1736,12 +1795,14 @@ export type GetBackupScheduleForVolumeResponses = { createdAt: number; cronExpression: string; enabled: boolean; + excludeIfPresent: Array | null; excludePatterns: Array | null; id: number; includePatterns: Array | null; lastBackupAt: number | null; lastBackupError: string | null; lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; + name: string; nextBackupAt: number | null; repository: { compressionMode: 'auto' | 'max' | 'off' | null; @@ -1840,6 +1901,11 @@ export type GetBackupScheduleForVolumeResponses = { version: '3' | '4' | '4.1'; port?: number; readOnly?: boolean; + } | { + backend: 'rclone'; + path: string; + remote: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1867,7 +1933,7 @@ export type GetBackupScheduleForVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -1971,13 +2037,13 @@ export type GetScheduleNotificationsResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2018,6 +2084,7 @@ export type GetScheduleNotificationsResponses = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; scheduleId: number; }>; }; @@ -2031,6 +2098,7 @@ export type UpdateScheduleNotificationsData = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; }>; }; path: { @@ -2059,13 +2127,13 @@ export type UpdateScheduleNotificationsResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2106,12 +2174,238 @@ export type UpdateScheduleNotificationsResponses = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; scheduleId: number; }>; }; export type UpdateScheduleNotificationsResponse = UpdateScheduleNotificationsResponses[keyof UpdateScheduleNotificationsResponses]; +export type GetScheduleMirrorsData = { + body?: never; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors'; +}; + +export type GetScheduleMirrorsResponses = { + /** + * List of mirror repository assignments for the schedule + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + customPassword?: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'local'; + name: string; + customPassword?: string; + isExistingRepository?: boolean; + path?: string; + } | { + backend: 'rclone'; + path: string; + remote: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; + }; + createdAt: number; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: number; + }>; +}; + +export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; + +export type UpdateScheduleMirrorsData = { + body?: { + mirrors: Array<{ + enabled: boolean; + repositoryId: string; + }>; + }; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors'; +}; + +export type UpdateScheduleMirrorsResponses = { + /** + * Mirror assignments updated successfully + */ + 200: Array<{ + createdAt: number; + enabled: boolean; + lastCopyAt: number | null; + lastCopyError: string | null; + lastCopyStatus: 'error' | 'success' | null; + repository: { + compressionMode: 'auto' | 'max' | 'off' | null; + config: { + accessKeyId: string; + backend: 'r2'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accessKeyId: string; + backend: 's3'; + bucket: string; + endpoint: string; + secretAccessKey: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + accountKey: string; + accountName: string; + backend: 'azure'; + container: string; + customPassword?: string; + endpointSuffix?: string; + isExistingRepository?: boolean; + } | { + backend: 'gcs'; + bucket: string; + credentialsJson: string; + projectId: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'local'; + name: string; + customPassword?: string; + isExistingRepository?: boolean; + path?: string; + } | { + backend: 'rclone'; + path: string; + remote: string; + customPassword?: string; + isExistingRepository?: boolean; + } | { + backend: 'rest'; + url: string; + customPassword?: string; + isExistingRepository?: boolean; + password?: string; + path?: string; + username?: string; + } | { + backend: 'sftp'; + host: string; + path: string; + privateKey: string; + user: string; + port?: number; + customPassword?: string; + isExistingRepository?: boolean; + }; + createdAt: number; + id: string; + lastChecked: number | null; + lastError: string | null; + name: string; + shortId: string; + status: 'error' | 'healthy' | 'unknown' | null; + type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3' | 'sftp'; + updatedAt: number; + }; + repositoryId: string; + scheduleId: number; + }>; +}; + +export type UpdateScheduleMirrorsResponse = UpdateScheduleMirrorsResponses[keyof UpdateScheduleMirrorsResponses]; + +export type GetMirrorCompatibilityData = { + body?: never; + path: { + scheduleId: string; + }; + query?: never; + url: '/api/v1/backups/{scheduleId}/mirrors/compatibility'; +}; + +export type GetMirrorCompatibilityResponses = { + /** + * List of repositories with their mirror compatibility status + */ + 200: Array<{ + compatible: boolean; + reason: string | null; + repositoryId: string; + }>; +}; + +export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; + export type ListNotificationDestinationsData = { body?: never; path?: never; @@ -2136,13 +2430,13 @@ export type ListNotificationDestinationsResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2197,13 +2491,13 @@ export type CreateNotificationDestinationData = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2257,13 +2551,13 @@ export type CreateNotificationDestinationResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2364,13 +2658,13 @@ export type GetNotificationDestinationResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2425,13 +2719,13 @@ export type UpdateNotificationDestinationData = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; @@ -2495,13 +2789,13 @@ export type UpdateNotificationDestinationResponses = { type: 'telegram'; } | { from: string; - password: string; smtpHost: string; smtpPort: number; to: Array; type: 'email'; useTLS: boolean; - username: string; + password?: string; + username?: string; } | { priority: 'default' | 'high' | 'low' | 'max' | 'min'; topic: string; diff --git a/app/client/components/create-repository-form.tsx b/app/client/components/create-repository-form.tsx deleted file mode 100644 index 255dd7a1..00000000 --- a/app/client/components/create-repository-form.tsx +++ /dev/null @@ -1,784 +0,0 @@ -import { arktypeResolver } from "@hookform/resolvers/arktype"; -import { type } from "arktype"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { cn, slugify } from "~/client/lib/utils"; -import { deepClean } from "~/utils/object"; -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 { useQuery } from "@tanstack/react-query"; -import { Alert, AlertDescription } from "./ui/alert"; -import { ExternalLink, AlertTriangle } from "lucide-react"; -import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; -import { useSystemInfo } from "~/client/hooks/use-system-info"; -import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic"; -import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen"; -import { Checkbox } from "./ui/checkbox"; -import { DirectoryBrowser } from "./directory-browser"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "./ui/alert-dialog"; -import { Textarea } from "./ui/textarea"; - -export const formSchema = type({ - name: "2<=string<=32", - compressionMode: type.valueOf(COMPRESSION_MODES).optional(), -}).and(repositoryConfigSchema); -const cleanSchema = type.pipe((d) => formSchema(deepClean(d))); - -export type RepositoryFormValues = typeof formSchema.inferIn; - -type Props = { - onSubmit: (values: RepositoryFormValues) => void; - mode?: "create" | "update"; - initialValues?: Partial; - formId?: string; - loading?: boolean; - className?: string; -}; - -const defaultValuesForType = { - local: { backend: "local" as const, compressionMode: "auto" as const }, - s3: { backend: "s3" as const, compressionMode: "auto" as const }, - r2: { backend: "r2" 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 }, - rest: { backend: "rest" as const, compressionMode: "auto" as const }, - sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 }, -}; - -export const CreateRepositoryForm = ({ - onSubmit, - mode = "create", - initialValues, - formId, - loading, - className, -}: Props) => { - const form = useForm({ - resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema), - defaultValues: initialValues, - resetOptions: { - keepDefaultValues: true, - keepDirtyValues: false, - }, - }); - - const { watch, setValue } = form; - - const watchedBackend = watch("backend"); - const watchedIsExistingRepository = watch("isExistingRepository"); - - const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default"); - const [showPathBrowser, setShowPathBrowser] = useState(false); - const [showPathWarning, setShowPathWarning] = useState(false); - - const { capabilities } = useSystemInfo(); - - const { data: rcloneRemotes, isLoading: isLoadingRemotes } = useQuery({ - ...listRcloneRemotesOptions(), - enabled: capabilities.rclone, - }); - - useEffect(() => { - form.reset({ - name: form.getValues().name, - isExistingRepository: form.getValues().isExistingRepository, - customPassword: form.getValues().customPassword, - ...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType], - }); - }, [watchedBackend, form]); - - return ( -
- - ( - - Name - - field.onChange(slugify(e.target.value))} - max={32} - min={2} - /> - - Unique identifier for the repository. - - - )} - /> - ( - - Backend - - Choose the storage backend for this repository. - - - )} - /> - - ( - - Compression Mode - - Compression mode for backups stored in this repository. - - - )} - /> - - ( - - - { - field.onChange(checked); - if (!checked) { - setPasswordMode("default"); - setValue("customPassword", undefined); - } - }} - /> - -
- Import existing repository - Check this if the repository already exists at the specified location -
-
- )} - /> - {watchedIsExistingRepository && ( - <> - - Repository Password - - - Choose whether to use Zerobyte's master password or enter a custom password for the existing repository. - - - - {passwordMode === "custom" && ( - ( - - Repository Password - - - - - The password used to encrypt this repository. It will be stored securely. - - - - )} - /> - )} - - )} - - {watchedBackend === "local" && ( - <> - - Repository Directory -
-
- {form.watch("path") || "/var/lib/zerobyte/repositories"} -
- -
- The directory where the repository will be stored. -
- - - - - - - Important: Host Mount Required - - -

When selecting a custom path, ensure it is mounted from the host machine into the container.

-

- If the path is not a host mount, you will lose your repository data when the container restarts. -

-

- The default path /var/lib/zerobyte/repositories is - already mounted from the host and is safe to use. -

-
-
- - Cancel - { - setShowPathBrowser(true); - setShowPathWarning(false); - }} - > - I Understand, Continue - - -
-
- - - - - Select Repository Directory - - Choose a directory from the filesystem to store the repository. - - -
- form.setValue("path", path)} - selectedPath={form.watch("path") || "/var/lib/zerobyte/repositories"} - /> -
- - Cancel - setShowPathBrowser(false)}>Done - -
-
- - )} - - {watchedBackend === "s3" && ( - <> - ( - - Endpoint - - - - S3-compatible endpoint URL. - - - )} - /> - ( - - Bucket - - - - S3 bucket name for storing backups. - - - )} - /> - ( - - Access Key ID - - - - S3 access key ID for authentication. - - - )} - /> - ( - - Secret Access Key - - - - S3 secret access key for authentication. - - - )} - /> - - )} - - {watchedBackend === "r2" && ( - <> - ( - - Endpoint - - - - - R2 endpoint (without https://). Find in R2 dashboard under bucket settings. - - - - )} - /> - ( - - Bucket - - - - R2 bucket name for storing backups. - - - )} - /> - ( - - Access Key ID - - - - R2 API token Access Key ID (create in Cloudflare R2 dashboard). - - - )} - /> - ( - - Secret Access Key - - - - R2 API token Secret Access Key (shown once when creating token). - - - )} - /> - - )} - - {watchedBackend === "gcs" && ( - <> - ( - - Bucket - - - - GCS bucket name for storing backups. - - - )} - /> - ( - - Project ID - - - - Google Cloud project ID. - - - )} - /> - ( - - Service Account JSON - - - - Service account JSON credentials for authentication. - - - )} - /> - - )} - - {watchedBackend === "azure" && ( - <> - ( - - Container - - - - Azure Blob Storage container name for storing backups. - - - )} - /> - ( - - Account Name - - - - Azure Storage account name. - - - )} - /> - ( - - Account Key - - - - Azure Storage account key for authentication. - - - )} - /> - ( - - Endpoint Suffix (Optional) - - - - Custom Azure endpoint suffix (defaults to core.windows.net). - - - )} - /> - - )} - - {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. - - - )} - /> - - ))} - - {watchedBackend === "rest" && ( - <> - ( - - REST Server URL - - - - URL of the REST server. - - - )} - /> - ( - - Repository Path (Optional) - - - - Path to the repository on the REST server (leave empty for root). - - - )} - /> - ( - - Username (Optional) - - - - Username for REST server authentication. - - - )} - /> - ( - - Password (Optional) - - - - Password for REST server authentication. - - - )} - /> - - )} - - {watchedBackend === "sftp" && ( - <> - ( - - Host - - - - SFTP server hostname or IP address. - - - )} - /> - ( - - Port - - field.onChange(parseInt(e.target.value, 10))} - /> - - SSH port (default: 22). - - - )} - /> - ( - - User - - - - SSH username for authentication. - - - )} - /> - ( - - Path - - - - Repository path on the SFTP server. - - - )} - /> - ( - - SSH Private Key - -