diff --git a/.dockerignore b/.dockerignore index 9980d983..d2d3fe67 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,4 @@ -* +** !turbo.json !bun.lock 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/.gitignore b/.gitignore index 9f32df66..99880c84 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ CLAUDE.md mutagen.yml.lock notes.md +smb-password.txt diff --git a/AGENTS.md b/AGENTS.md index 136692d0..6ba3fcd9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,6 @@ Zerobyte is a backup automation tool built on top of Restic that provides a web - **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 @@ -84,9 +83,7 @@ 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) +- Initializes main API server on port 4096 (REST API + serves static frontend) **Modules** (`app/server/modules/`): Each module follows a controller � service � database pattern: @@ -116,7 +113,7 @@ Cron-based background jobs managed by the Scheduler: **Core** (`app/server/core/`): - `scheduler.ts` - Job scheduling system using node-cron -- `capabilities.ts` - Detects available system features (Docker support, etc.) +- `capabilities.ts` - Detects available system features - `constants.ts` - Application-wide constants **Utils** (`app/server/utils/`): @@ -188,17 +185,6 @@ Zerobyte is a wrapper around Restic for backup operations. Key integration point - 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**: @@ -212,7 +198,7 @@ The plugin implements the Docker Volume Plugin API v1. **Capabilities Detection**: On startup, the server detects available capabilities (see `core/capabilities.ts`): -- **Docker**: Requires `/var/run/docker.sock` access +- **rclone**: Requires `/root/.config/rclone` directory access - System will gracefully degrade if capabilities are unavailable ## Common Workflows @@ -252,16 +238,3 @@ On startup, the server detects available capabilities (see `core/capabilities.ts - **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 b9c01db1..206d294b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ 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 # ------------------------------ diff --git a/README.md b/README.md index b1f03613..55bf3cdf 100644 --- a/README.md +++ b/README.md @@ -413,7 +413,7 @@ Secrets/credentials in the config file can reference environment variables using ```yaml services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.15 + image: ghcr.io/nicotsx/zerobyte:v0.19 container_name: zerobyte restart: unless-stopped cap_add: @@ -441,6 +441,34 @@ docker compose up -d Once the container is running, you can access the web interface at `http://:4096`. +### Simplified setup (No remote mounts) + +If you only need to back up locally mounted folders and don't require remote share mounting capabilities, you can remove the `SYS_ADMIN` capability and FUSE device from your `docker-compose.yml`: + +```yaml +services: + zerobyte: + image: ghcr.io/nicotsx/zerobyte:v0.19 + container_name: zerobyte + restart: unless-stopped + ports: + - "4096:4096" + environment: + - TZ=Europe/Paris # Set your timezone here + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/lib/zerobyte:/var/lib/zerobyte + - /path/to/your/directory:/mydata +``` + +**Trade-offs:** +- ✅ Improved security by reducing container capabilities +- ✅ Support for local directories +- ✅ Keep support all repository types (local, S3, GCS, Azure, rclone) +- ❌ Cannot mount NFS, SMB, or WebDAV shares directly from Zerobyte + +If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`. + ## Adding your first volume Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor. @@ -452,7 +480,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.15 + image: ghcr.io/nicotsx/zerobyte:v0.19 container_name: zerobyte restart: unless-stopped cap_add: @@ -520,7 +548,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ```diff services: zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.15 + image: ghcr.io/nicotsx/zerobyte:v0.19 container_name: zerobyte restart: unless-stopped cap_add: @@ -570,97 +598,6 @@ Zerobyte allows you to easily restore your data from backups. To restore data, n ![Preview](https://github.com/nicotsx/zerobyte/blob/main/screenshots/restoring.png?raw=true) -## Propagating mounts to host - -Zerobyte is capable of propagating mounted volumes from within the container to the host system. This is particularly useful when you want to access the mounted data directly from the host to use it with other applications or services. - -In order to enable this feature, you need to change your bind mount `/var/lib/zerobyte` to use the `:rshared` flag. Here is an example of how to set this up in your `docker-compose.yml` file: - -```diff -services: - zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.15 - container_name: zerobyte - restart: unless-stopped - ports: - - "4096:4096" - devices: - - /dev/fuse:/dev/fuse - environment: - - TZ=Europe/Paris - volumes: - - /etc/localtime:/etc/localtime:ro -- - /var/lib/zerobyte:/var/lib/zerobyte -+ - /var/lib/zerobyte:/var/lib/zerobyte:rshared -``` - -Restart the Zerobyte container to apply the changes: - -```bash -docker compose down -docker compose up -d -``` - -## Docker plugin - -Zerobyte can also be used as a Docker volume plugin, allowing you to mount your volumes directly into other Docker containers. This enables seamless integration with your containerized applications. - -In order to enable this feature, you need to run Zerobyte with several items shared from the host. Here is an example of how to set this up in your `docker-compose.yml` file: - -```diff -services: - zerobyte: - image: ghcr.io/nicotsx/zerobyte:v0.15 - container_name: zerobyte - restart: unless-stopped - cap_add: - - SYS_ADMIN - ports: - - "4096:4096" - devices: - - /dev/fuse:/dev/fuse - environment: - - TZ=Europe/Paris - volumes: - - /etc/localtime:/etc/localtime:ro -- - /var/lib/zerobyte:/var/lib/zerobyte -+ - /var/lib/zerobyte:/var/lib/zerobyte:rshared -+ - /run/docker/plugins:/run/docker/plugins -+ - /var/run/docker.sock:/var/run/docker.sock -``` - -Restart the Zerobyte container to apply the changes: - -```bash -docker compose down -docker compose up -d -``` - -Your Zerobyte volumes will now be available as Docker volumes that you can mount into other containers using the `--volume` flag: - -```bash -docker run -v zb-abc12:/path/in/container nginx:latest -``` - -Or using Docker Compose: - -```yaml -services: - myservice: - image: nginx:latest - volumes: - - zb-abc12:/path/in/container -volumes: - zb-abc12: - external: true -``` - -The volume name format is `zb-` where `` is the unique identifier shown on the volume's Docker tab in Zerobyte. This short ID remains stable even if you rename the volume. You can verify that the volume is available by running: - -```bash -docker volume ls -``` - ## Third-Party Software This project includes the following third-party software components: diff --git a/app/app.css b/app/app.css index c12eee55..c84370ed 100644 --- a/app/app.css +++ b/app/app.css @@ -173,3 +173,9 @@ body { --chart-5: oklch(0.645 0.246 16.439); } } + +/* Hide built-in password reveal/clear controls (notably in Edge on Windows) */ +[data-secret-input] input[type="password"]::-ms-reveal, +[data-secret-input] input[type="password"]::-ms-clear { + display: none; +} diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index a4ac2a30..e5c39e40 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, 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, 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'; +import { browseFilesystem, changePassword, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, 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, reorderBackupSchedules, 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, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, 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, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, 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 @@ -247,24 +247,6 @@ export const updateVolumeMutation = (options?: Partial return mutationOptions; }; -export const getContainersUsingVolumeQueryKey = (options: Options) => createQueryKey('getContainersUsingVolume', options); - -/** - * Get containers using a volume by name - */ -export const getContainersUsingVolumeOptions = (options: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getContainersUsingVolume({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getContainersUsingVolumeQueryKey(options) -}); - /** * Mount a volume */ @@ -806,6 +788,23 @@ export const getMirrorCompatibilityOptions = (options: Options>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await reorderBackupSchedules({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const listNotificationDestinationsQueryKey = (options?: Options) => createQueryKey('listNotificationDestinations', options); /** diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 4dc9424f..a31c7b49 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen'; */ export type CreateClientConfig = (override?: Config) => Config & T>; -export const client = createClient(createConfig({ baseUrl: 'http://192.168.2.42:4096' })); +export const client = createClient(createConfig({ baseUrl: 'http://localhost:4096' })); diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index c27c2697..4f3e083e 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, 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'; +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, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, 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, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, 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 & { /** @@ -120,11 +120,6 @@ export const updateVolume = (options: Opti } }); -/** - * Get containers using a volume by name - */ -export const getContainersUsingVolume = (options: Options) => (options.client ?? client).get({ url: '/api/v1/volumes/{name}/containers', ...options }); - /** * Mount a volume */ @@ -329,6 +324,18 @@ export const updateScheduleMirrors = (opti */ export const getMirrorCompatibility = (options: Options) => (options.client ?? client).get({ url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', ...options }); +/** + * Reorder backup schedules by providing an array of schedule IDs in the desired order + */ +export const reorderBackupSchedules = (options?: Options) => (options?.client ?? client).post({ + url: '/api/v1/backups/reorder', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + } +}); + /** * List all notification destinations */ diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 2790e840..eb9dc447 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: 'http://192.168.2.42:4096' | (string & {}); + baseUrl: 'http://localhost:4096' | (string & {}); }; export type RegisterData = { @@ -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,43 +563,13 @@ export type UpdateVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; updatedAt: number; }; }; export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeResponses]; -export type GetContainersUsingVolumeData = { - body?: never; - path: { - name: string; - }; - query?: never; - url: '/api/v1/volumes/{name}/containers'; -}; - -export type GetContainersUsingVolumeErrors = { - /** - * Volume not found - */ - 404: unknown; -}; - -export type GetContainersUsingVolumeResponses = { - /** - * List of containers using the volume - */ - 200: Array<{ - id: string; - image: string; - name: string; - state: string; - }>; -}; - -export type GetContainersUsingVolumeResponse = GetContainersUsingVolumeResponses[keyof GetContainersUsingVolumeResponses]; - export type MountVolumeData = { body?: never; path: { @@ -1397,6 +1402,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; @@ -1424,7 +1434,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; @@ -1636,6 +1646,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; @@ -1663,7 +1678,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; @@ -1856,6 +1871,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; @@ -1883,7 +1903,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; @@ -1987,13 +2007,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; @@ -2034,6 +2054,7 @@ export type GetScheduleNotificationsResponses = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; scheduleId: number; }>; }; @@ -2047,6 +2068,7 @@ export type UpdateScheduleNotificationsData = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; }>; }; path: { @@ -2075,13 +2097,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; @@ -2122,6 +2144,7 @@ export type UpdateScheduleNotificationsResponses = { notifyOnFailure: boolean; notifyOnStart: boolean; notifyOnSuccess: boolean; + notifyOnWarning: boolean; scheduleId: number; }>; }; @@ -2353,6 +2376,26 @@ export type GetMirrorCompatibilityResponses = { export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; +export type ReorderBackupSchedulesData = { + body?: { + scheduleIds: Array; + }; + path?: never; + query?: never; + url: '/api/v1/backups/reorder'; +}; + +export type ReorderBackupSchedulesResponses = { + /** + * Backup schedules reordered successfully + */ + 200: { + success: boolean; + }; +}; + +export type ReorderBackupSchedulesResponse = ReorderBackupSchedulesResponses[keyof ReorderBackupSchedulesResponses]; + export type ListNotificationDestinationsData = { body?: never; path?: never; @@ -2377,13 +2420,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; @@ -2438,13 +2481,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; @@ -2498,13 +2541,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; @@ -2605,13 +2648,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; @@ -2666,13 +2709,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; @@ -2736,13 +2779,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; @@ -2831,8 +2874,8 @@ export type GetSystemInfoResponses = { */ 200: { capabilities: { - docker: boolean; rclone: boolean; + sysAdmin: boolean; }; }; }; 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 - -