From 512bee535a6ad98d0dffd98231702ae15cc2b210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?= Date: Mon, 22 Dec 2025 17:00:59 +0100 Subject: [PATCH] Merge remote-tracking branch 'upstream/main' into config-export-feature --- .env.test | 1 + .../actions/install-dependencies/action.yml | 15 + .github/dependabot.yml | 18 + .github/workflows/ci.yml | 37 + .github/workflows/release.yml | 47 +- AGENTS.md | 10 + Dockerfile | 18 +- README.md | 14 + .../api-client/core/serverSentEvents.gen.ts | 2 + app/client/api-client/types.gen.ts | 7 + app/client/components/app-sidebar.tsx | 18 +- .../components/create-schedule-form.tsx | 81 +- .../backups/components/schedule-summary.tsx | 3 +- .../modules/backups/routes/backup-details.tsx | 9 +- .../modules/backups/routes/create-backup.tsx | 8 +- .../components/create-notification-form.tsx | 4 +- .../routes/notification-details.tsx | 10 +- .../components/create-repository-form.tsx | 4 +- .../volumes/components/create-volume-form.tsx | 4 +- app/drizzle/0023_special_thor.sql | 2 + app/drizzle/0024_schedules-one-fs.sql | 1 + app/drizzle/meta/0023_snapshot.json | 839 ++++++++++++++++++ app/drizzle/meta/0024_snapshot.json | 839 ++++++++++++++++++ app/drizzle/meta/_journal.json | 14 + app/schemas/notifications.ts | 4 +- app/schemas/restic.ts | 4 +- app/schemas/volumes.ts | 8 +- app/server/app.ts | 90 ++ .../core/__tests__/repository-mutex.test.ts | 38 + app/server/core/constants.ts | 4 +- app/server/core/repository-mutex.ts | 6 +- app/server/db/schema.ts | 4 + app/server/index.ts | 68 +- app/server/jobs/auto-remount.ts | 28 + app/server/jobs/backup-execution.ts | 5 +- app/server/jobs/cleanup-dangling.ts | 2 +- app/server/jobs/repository-healthchecks.ts | 6 - app/server/modules/auth/auth.controller.ts | 60 +- .../modules/backends/nfs/nfs-backend.ts | 37 +- .../modules/backends/rclone/rclone-backend.ts | 37 +- .../modules/backends/smb/smb-backend.ts | 37 +- .../modules/backends/utils/backend-utils.ts | 4 +- .../modules/backends/webdav/webdav-backend.ts | 52 +- .../__tests__/backups.controller.test.ts | 126 +++ .../__tests__/backups.patterns.test.ts | 136 +++ .../backups/__tests__/backups.service.test.ts | 176 ++++ .../modules/backups/backups.controller.ts | 4 +- app/server/modules/backups/backups.dto.ts | 3 + app/server/modules/backups/backups.service.ts | 70 +- .../__tests__/events.controller.test.ts | 53 ++ app/server/modules/lifecycle/startup.ts | 46 +- .../notifications.controller.test.ts | 110 +++ .../notifications/notifications.service.ts | 14 +- .../__tests__/repositories.controller.test.ts | 105 +++ .../repositories/repositories.service.ts | 16 +- .../__tests__/system.controller.test.ts | 89 ++ .../__tests__/volumes.controller.test.ts | 104 +++ app/server/modules/volumes/volume.service.ts | 10 +- app/server/utils/restic.ts | 99 ++- app/server/utils/spawn.ts | 4 +- app/test/helpers/auth.ts | 24 + app/test/helpers/backup.ts | 16 + app/test/helpers/repository.ts | 20 + app/test/helpers/restic.ts | 18 + app/test/helpers/volume.ts | 21 + app/test/setup.ts | 19 + app/utils/utils.ts | 16 +- bun.lock | 401 +++++---- docker-compose.yml | 14 +- examples/README.md | 17 + examples/basic-docker-compose/.env.example | 2 + examples/basic-docker-compose/README.md | 25 + .../basic-docker-compose/docker-compose.yml | 16 + examples/directory-bind-mount/.env.example | 8 + examples/directory-bind-mount/README.md | 34 + .../directory-bind-mount/docker-compose.yml | 13 + examples/rclone-config-mount/.env.example | 8 + examples/rclone-config-mount/README.md | 41 + .../rclone-config-mount/docker-compose.yml | 13 + examples/secrets-placeholders/.env.example | 6 + examples/secrets-placeholders/.gitignore | 5 + examples/secrets-placeholders/README.md | 61 ++ .../secrets-placeholders/docker-compose.yml | 26 + .../simplified-docker-compose/.env.example | 2 + examples/simplified-docker-compose/README.md | 25 + .../docker-compose.yml | 12 + examples/tailscale-sidecar/.env.example | 22 + examples/tailscale-sidecar/README.md | 93 ++ examples/tailscale-sidecar/docker-compose.yml | 51 ++ package.json | 17 +- vite.config.ts | 2 +- 91 files changed, 4213 insertions(+), 499 deletions(-) create mode 100644 .env.test create mode 100644 .github/actions/install-dependencies/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 app/drizzle/0023_special_thor.sql create mode 100644 app/drizzle/0024_schedules-one-fs.sql create mode 100644 app/drizzle/meta/0023_snapshot.json create mode 100644 app/drizzle/meta/0024_snapshot.json create mode 100644 app/server/app.ts create mode 100644 app/server/core/__tests__/repository-mutex.test.ts create mode 100644 app/server/jobs/auto-remount.ts create mode 100644 app/server/modules/backups/__tests__/backups.controller.test.ts create mode 100644 app/server/modules/backups/__tests__/backups.patterns.test.ts create mode 100644 app/server/modules/backups/__tests__/backups.service.test.ts create mode 100644 app/server/modules/events/__tests__/events.controller.test.ts create mode 100644 app/server/modules/notifications/__tests__/notifications.controller.test.ts create mode 100644 app/server/modules/repositories/__tests__/repositories.controller.test.ts create mode 100644 app/server/modules/system/__tests__/system.controller.test.ts create mode 100644 app/server/modules/volumes/__tests__/volumes.controller.test.ts create mode 100644 app/test/helpers/auth.ts create mode 100644 app/test/helpers/backup.ts create mode 100644 app/test/helpers/repository.ts create mode 100644 app/test/helpers/restic.ts create mode 100644 app/test/helpers/volume.ts create mode 100644 app/test/setup.ts create mode 100644 examples/README.md create mode 100644 examples/basic-docker-compose/.env.example create mode 100644 examples/basic-docker-compose/README.md create mode 100644 examples/basic-docker-compose/docker-compose.yml create mode 100644 examples/directory-bind-mount/.env.example create mode 100644 examples/directory-bind-mount/README.md create mode 100644 examples/directory-bind-mount/docker-compose.yml create mode 100644 examples/rclone-config-mount/.env.example create mode 100644 examples/rclone-config-mount/README.md create mode 100644 examples/rclone-config-mount/docker-compose.yml create mode 100644 examples/secrets-placeholders/.env.example create mode 100644 examples/secrets-placeholders/.gitignore create mode 100644 examples/secrets-placeholders/README.md create mode 100644 examples/secrets-placeholders/docker-compose.yml create mode 100644 examples/simplified-docker-compose/.env.example create mode 100644 examples/simplified-docker-compose/README.md create mode 100644 examples/simplified-docker-compose/docker-compose.yml create mode 100644 examples/tailscale-sidecar/.env.example create mode 100644 examples/tailscale-sidecar/README.md create mode 100644 examples/tailscale-sidecar/docker-compose.yml diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..a8c805e0 --- /dev/null +++ b/.env.test @@ -0,0 +1 @@ +DATABASE_URL=:memory: diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 00000000..36b4abec --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,15 @@ +name: Install dependencies + +description: Install dependencies + +runs: + using: "composite" + steps: + - uses: oven-sh/setup-bun@v2 + name: Install Bun + with: + bun-version: "1.3.5" + + - name: Install dependencies + shell: bash + run: bun install --frozen-lockfile diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..cd0c8146 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "bun" + directory: "/" + schedule: + interval: "daily" + rebase-strategy: 'auto' + groups: + minor-patch: + update-types: + - minor + - patch + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + rebase-strategy: 'auto' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9a081d44 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: Checks + +permissions: + contents: read + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + ci: + timeout-minutes: 15 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install dependencies + uses: "./.github/actions/install-dependencies" + + - name: Run type checks + shell: bash + run: bun run tsc + + - name: Run tests + shell: bash + run: bun run test --ci --coverage + + - name: Build project + shell: bash + run: bun run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dadfa1b3..326ffa0f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,11 @@ on: - "v*.*.*-beta.*" - "v*.*.*-alpha.*" +permissions: + contents: write + packages: write + security-events: write + jobs: determine-release-type: runs-on: ubuntu-latest @@ -32,16 +37,23 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.ref }} - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + with: + driver: cloud + endpoint: "meienberger/runtipi-builder" + install: true - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -50,6 +62,31 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Build docker image + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64 + push: false + load: true + tags: local/zerobyte:ci + build-args: | + APP_VERSION=${{ needs.determine-release-type.outputs.tagname }} + + - name: Scan new image for vulnerabilities + uses: anchore/scan-action@v7 + id: scan + with: + image: local/zerobyte:ci + fail-build: true + severity-cutoff: critical + + - name: upload Anchore scan report + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} + - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -63,7 +100,7 @@ jobs: flavor: | latest=${{ needs.determine-release-type.outputs.release_type == 'release' }} - - name: Build and push images + - name: Push images to GitHub Container Registry uses: docker/build-push-action@v6 with: context: . @@ -74,8 +111,6 @@ 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 index 6ba3fcd9..92f7ff18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,16 @@ This is a unified application with the following structure: bun run tsc ``` +### Testing + +```bash +# Run all tests +bun run test + +# Run a specific test file +bunx dotenv-cli -e .env.test -- bun test --preload ./app/test/setup.ts path/to/test.ts +``` + ### Building ```bash diff --git a/Dockerfile b/Dockerfile index 206d294b..c95dab9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -ARG BUN_VERSION="1.3.3" +ARG BUN_VERSION="1.3.5" FROM oven/bun:${BUN_VERSION}-alpine AS base -RUN apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 +RUN apk upgrade --no-cache && \ + apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 # ------------------------------ @@ -14,7 +15,8 @@ WORKDIR /deps ARG TARGETARCH ARG RESTIC_VERSION="0.18.1" -ARG SHOUTRRR_VERSION="0.12.1" +ARG RCLONE_VERSION="1.72.1" +ARG SHOUTRRR_VERSION="0.13.1" ENV TARGETARCH=${TARGETARCH} RUN apk add --no-cache curl bzip2 unzip tar @@ -22,18 +24,18 @@ RUN apk add --no-cache curl bzip2 unzip tar RUN echo "Building for ${TARGETARCH}" RUN if [ "${TARGETARCH}" = "arm64" ]; then \ curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_arm64.bz2; \ - curl -O https://downloads.rclone.org/rclone-current-linux-arm64.zip; \ - unzip rclone-current-linux-arm64.zip; \ + curl -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v$RCLONE_VERSION/rclone-v$RCLONE_VERSION-linux-arm64.zip"; \ + unzip rclone.zip; \ curl -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_arm64v8_${SHOUTRRR_VERSION}.tar.gz"; \ elif [ "${TARGETARCH}" = "amd64" ]; then \ curl -L -o restic.bz2 "https://github.com/restic/restic/releases/download/v$RESTIC_VERSION/restic_$RESTIC_VERSION"_linux_amd64.bz2; \ - curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip; \ - unzip rclone-current-linux-amd64.zip; \ + curl -L -o rclone.zip "https://github.com/rclone/rclone/releases/download/v$RCLONE_VERSION/rclone-v$RCLONE_VERSION-linux-amd64.zip"; \ + unzip rclone.zip; \ curl -L -o shoutrrr.tar.gz "https://github.com/nicholas-fedor/shoutrrr/releases/download/v$SHOUTRRR_VERSION/shoutrrr_linux_amd64_${SHOUTRRR_VERSION}.tar.gz"; \ fi RUN bzip2 -d restic.bz2 && chmod +x restic -RUN mv rclone-*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone +RUN mv rclone-v*-linux-*/rclone /deps/rclone && chmod +x /deps/rclone RUN tar -xzf shoutrrr.tar.gz && chmod +x shoutrrr # ------------------------------ diff --git a/README.md b/README.md index 21a1a176..63716acb 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ services: - /var/lib/zerobyte:/var/lib/zerobyte ``` +> [!WARNING] +> It is highly discouraged to run Zerobyte on a server that is accessible from the internet (VPS or home server with port forwarding) If you do, make sure to change the port mapping to "127.0.0.1:4096:4096" and use a secure tunnel (SSH tunnel, Cloudflare Tunnel, etc.) with authentication. + > [!WARNING] > Do not try to point `/var/lib/zerobyte` on a network share. You will face permission issues and strong performance degradation. @@ -95,6 +98,10 @@ services: If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`. +## Examples + +See [examples/README.md](examples/README.md) for runnable, copy/paste-friendly examples. + ## 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. @@ -155,22 +162,27 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov **Setup instructions:** 1. **Install rclone on your host system** (if not already installed): + ```bash curl https://rclone.org/install.sh | sudo bash ``` 2. **Configure your cloud storage remote** using rclone's interactive config: + ```bash rclone config ``` + Follow the prompts to set up your cloud storage provider. For OAuth providers (Google Drive, Dropbox, etc.), rclone will guide you through the authentication flow. 3. **Verify your remote is configured**: + ```bash rclone listremotes ``` 4. **Mount the rclone config into the Zerobyte container** by updating your `docker-compose.yml`: + ```diff services: zerobyte: @@ -192,6 +204,7 @@ Zerobyte can use [rclone](https://rclone.org/) to support 40+ cloud storage prov ``` 5. **Restart the Zerobyte container**: + ```bash docker compose down docker compose up -d @@ -209,6 +222,7 @@ For a complete list of supported providers, see the [rclone documentation](https Once you have added a volume and created a repository, you can create your first backup job. A backup job defines the schedule and parameters for backing up a specific volume to a designated repository. When creating a backup job, you can specify the following settings: + - **Schedule**: Define how often the backup should run (e.g., daily, weekly) - **Retention Policy**: Set rules for how long backups should be retained (e.g., keep daily backups for 7 days, weekly backups for 4 weeks) - **Paths**: Specify which files or directories to include in the backup diff --git a/app/client/api-client/core/serverSentEvents.gen.ts b/app/client/api-client/core/serverSentEvents.gen.ts index f8fd78e2..343d25af 100644 --- a/app/client/api-client/core/serverSentEvents.gen.ts +++ b/app/client/api-client/core/serverSentEvents.gen.ts @@ -169,6 +169,8 @@ export const createSseClient = ({ const { done, value } = await reader.read(); if (done) break; buffer += value; + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); const chunks = buffer.split('\n\n'); buffer = chunks.pop() ?? ''; diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 83f4e884..ffbbce57 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1305,6 +1305,7 @@ export type ListBackupSchedulesResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { @@ -1453,6 +1454,7 @@ export type CreateBackupScheduleData = { excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; + oneFileSystem?: boolean; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1486,6 +1488,7 @@ export type CreateBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repositoryId: string; retentionPolicy: { keepDaily?: number; @@ -1549,6 +1552,7 @@ export type GetBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { @@ -1696,6 +1700,7 @@ export type UpdateBackupScheduleData = { excludePatterns?: Array; includePatterns?: Array; name?: string; + oneFileSystem?: boolean; retentionPolicy?: { keepDaily?: number; keepHourly?: number; @@ -1731,6 +1736,7 @@ export type UpdateBackupScheduleResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repositoryId: string; retentionPolicy: { keepDaily?: number; @@ -1774,6 +1780,7 @@ export type GetBackupScheduleForVolumeResponses = { lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null; name: string; nextBackupAt: number | null; + oneFileSystem: boolean; repository: { compressionMode: 'auto' | 'max' | 'off' | null; config: { diff --git a/app/client/components/app-sidebar.tsx b/app/client/components/app-sidebar.tsx index 813115dc..7796b467 100644 --- a/app/client/components/app-sidebar.tsx +++ b/app/client/components/app-sidebar.tsx @@ -46,10 +46,15 @@ const items = [ export function AppSidebar() { const { state } = useSidebar(); + const displayVersion = APP_VERSION.startsWith("v") || APP_VERSION === "dev" ? APP_VERSION : `v${APP_VERSION}`; + const releaseUrl = + APP_VERSION === "dev" + ? "https://github.com/nicotsx/zerobyte" + : `https://github.com/nicotsx/zerobyte/releases/tag/${displayVersion}`; return ( - + Zerobyte Logo -
- {APP_VERSION} -
+ {displayVersion} +
); diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index 8f6de25c..adfaf820 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -1,4 +1,5 @@ import { arktypeResolver } from "@hookform/resolvers/arktype"; + import { useQuery } from "@tanstack/react-query"; import { type } from "arktype"; import { useCallback, useState } from "react"; @@ -6,6 +7,7 @@ import { useForm } from "react-hook-form"; import { listRepositoriesOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { RepositoryIcon } from "~/client/components/repository-icon"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; +import { Checkbox } from "~/client/components/ui/checkbox"; import { Form, FormControl, @@ -17,6 +19,7 @@ import { } from "~/client/components/ui/form"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; +import { Button } from "~/client/components/ui/button"; import { Textarea } from "~/client/components/ui/textarea"; import { VolumeFileBrowser } from "~/client/components/volume-file-browser"; import type { BackupSchedule, Volume } from "~/client/lib/types"; @@ -32,12 +35,14 @@ const internalFormSchema = type({ frequency: "string", dailyTime: "string?", weeklyDay: "string?", + monthlyDays: "string[]?", keepLast: "number?", keepHourly: "number?", keepDaily: "number?", keepWeekly: "number?", keepMonthly: "number?", keepYearly: "number?", + oneFileSystem: "boolean?", }); const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d))); @@ -76,15 +81,16 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu } const parts = schedule.cronExpression.split(" "); - const [minutePart, hourPart, , , dayOfWeekPart] = parts; + const [minutePart, hourPart, dayOfMonthPart, , dayOfWeekPart] = parts; const isHourly = hourPart === "*"; - const isDaily = !isHourly && dayOfWeekPart === "*"; - const frequency = isHourly ? "hourly" : isDaily ? "daily" : "weekly"; + const isMonthly = !isHourly && dayOfMonthPart !== "*" && dayOfWeekPart === "*"; + const isDaily = !isHourly && dayOfMonthPart === "*" && dayOfWeekPart === "*"; + const frequency = isHourly ? "hourly" : isMonthly ? "monthly" : isDaily ? "daily" : "weekly"; const dailyTime = isHourly ? undefined : `${hourPart.padStart(2, "0")}:${minutePart.padStart(2, "0")}`; - const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined; + const monthlyDays = isMonthly ? dayOfMonthPart.split(",") : undefined; const patterns = schedule.includePatterns || []; const isGlobPattern = (p: string) => /[*?[\]]/.test(p); @@ -95,12 +101,14 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu name: schedule.name, repositoryId: schedule.repositoryId, frequency, + monthlyDays, dailyTime, weeklyDay, includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined, includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined, excludePatternsText: schedule.excludePatterns?.join("\n") || undefined, excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined, + oneFileSystem: schedule.oneFileSystem ?? false, ...schedule.retentionPolicy, }; }; @@ -246,6 +254,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Hourly Daily Weekly + Specific days @@ -299,6 +308,42 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: )} /> )} + {frequency === "monthly" && ( + ( + + Days of the month + +
+ {Array.from({ length: 31 }, (_, i) => { + const day = (i + 1).toString(); + const isSelected = field.value?.includes(day); + return ( + + ); + })} +
+
+ Select one or more days when the backup should run. + +
+ )} + /> + )} @@ -318,7 +363,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: onSelectionChange={handleSelectionChange} withCheckboxes={true} foldersOnly={false} - className="flex-1 border rounded-md bg-card p-2 min-h-[300px] max-h-[400px] overflow-auto" + className="flex-1 border rounded-md bg-card p-2 min-h-75 max-h-100 overflow-auto" /> {selectedPaths.size > 0 && (
@@ -342,7 +387,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: