From c18abac4744a073cc156ffb4ce53012814e5b6d0 Mon Sep 17 00:00:00 2001 From: Nemeth Csaba Tibor Date: Wed, 1 Apr 2026 15:01:43 +0200 Subject: [PATCH] feat: add b2 backend option to repository --- .../components/create-repository-form.tsx | 6 ++ .../repository-forms/b2-repository-form.tsx | 79 +++++++++++++++++++ .../repositories/repository-config-secrets.ts | 8 ++ packages/core/src/restic/helpers/build-env.ts | 4 + .../core/src/restic/helpers/build-repo-url.ts | 2 + packages/core/src/restic/schemas.ts | 12 +++ 6 files changed, 111 insertions(+) create mode 100644 app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index 54b0ffc3..d3e755ec 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -26,6 +26,7 @@ import { gcsRepositoryConfigSchema, localRepositoryConfigSchema, r2RepositoryConfigSchema, + b2RepositoryConfigSchema, rcloneRepositoryConfigSchema, restRepositoryConfigSchema, s3RepositoryConfigSchema, @@ -46,6 +47,7 @@ import { import { useServerFn } from "@tanstack/react-start"; import { getServerConstants } from "~/server/lib/functions/server-constants"; import { useSuspenseQuery } from "@tanstack/react-query"; +import { B2RepositoryForm } from "./repository-forms/b2-repository-form"; const formBaseFields = { name: z.string().min(2).max(32), @@ -56,6 +58,7 @@ export const formSchema = z .discriminatedUnion("backend", [ localRepositoryConfigSchema.extend(formBaseFields), s3RepositoryConfigSchema.extend(formBaseFields), + b2RepositoryConfigSchema.extend(formBaseFields), r2RepositoryConfigSchema.extend(formBaseFields), gcsRepositoryConfigSchema.extend(formBaseFields), azureRepositoryConfigSchema.extend(formBaseFields), @@ -87,6 +90,7 @@ type Props = { const defaultValuesForType = (repoBase: string) => ({ local: { backend: "local" as const, compressionMode: "auto" as const, path: repoBase }, s3: { backend: "s3" as const, compressionMode: "auto" as const }, + b2: { backend: "b2" 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 }, @@ -185,6 +189,7 @@ export const CreateRepositoryForm = ({ Local S3 Cloudflare R2 + Backblaze B2 Google Cloud Storage Azure Blob Storage REST Server @@ -313,6 +318,7 @@ export const CreateRepositoryForm = ({ {watchedBackend === "local" && } {watchedBackend === "s3" && } + {watchedBackend === "b2" && } {watchedBackend === "r2" && } {watchedBackend === "gcs" && } {watchedBackend === "azure" && } diff --git a/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx new file mode 100644 index 00000000..ba45e4a6 --- /dev/null +++ b/app/client/modules/repositories/components/repository-forms/b2-repository-form.tsx @@ -0,0 +1,79 @@ +import type { UseFormReturn } from "react-hook-form"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../../../../components/ui/form"; +import { Input } from "../../../../components/ui/input"; +import { SecretInput } from "../../../../components/ui/secret-input"; +import type { RepositoryFormValues } from "../create-repository-form"; + +type Props = { + form: UseFormReturn; +}; + +export const B2RepositoryForm = ({ form }: Props) => { + return ( + <> + ( + + Bucket + + + + Backblaze bucket name for storing backups. + + + )} + /> + ( + + Bucket + + + + Path inside the bucket to store data + + + )} + /> + ( + + Access Key ID + + + + S3 access key ID for authentication. + + + )} + /> + ( + + Secret Access Key + + + + S3 secret access key for authentication. + + + )} + /> + + ); +}; diff --git a/app/server/modules/repositories/repository-config-secrets.ts b/app/server/modules/repositories/repository-config-secrets.ts index 823ef5cc..2e31a36c 100644 --- a/app/server/modules/repositories/repository-config-secrets.ts +++ b/app/server/modules/repositories/repository-config-secrets.ts @@ -31,6 +31,14 @@ export const mapRepositoryConfigSecrets = async ( accessKeyId: await transformSecret(config.accessKeyId), secretAccessKey: await transformSecret(config.secretAccessKey), }; + case "b2": + return { + ...config, + customPassword, + cacert, + accountId: await transformSecret(config.accountId), + accountKey: await transformSecret(config.accountKey), + }; case "gcs": return { ...config, diff --git a/packages/core/src/restic/helpers/build-env.ts b/packages/core/src/restic/helpers/build-env.ts index ae6ed50e..bea75602 100644 --- a/packages/core/src/restic/helpers/build-env.ts +++ b/packages/core/src/restic/helpers/build-env.ts @@ -44,6 +44,10 @@ export const buildEnv = async ( env.AWS_REGION = "auto"; env.AWS_S3_FORCE_PATH_STYLE = "true"; break; + case "b2": + env.B2_ACCOUNT_ID = await deps.resolveSecret(config.accountId); + env.B2_ACCOUNT_KEY = await deps.resolveSecret(config.accountKey); + break; case "gcs": { const decryptedCredentials = await deps.resolveSecret(config.credentialsJson); const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`); diff --git a/packages/core/src/restic/helpers/build-repo-url.ts b/packages/core/src/restic/helpers/build-repo-url.ts index ba896040..b985c436 100644 --- a/packages/core/src/restic/helpers/build-repo-url.ts +++ b/packages/core/src/restic/helpers/build-repo-url.ts @@ -15,6 +15,8 @@ export const buildRepoUrl = (config: RepositoryConfig): string => { .replace(/\/$/, ""); return `s3:${endpoint}/${config.bucket}`; } + case "b2": + return `b2:${config.bucket}:${config.path}`; case "gcs": return `gs:${config.bucket}:/`; case "azure": diff --git a/packages/core/src/restic/schemas.ts b/packages/core/src/restic/schemas.ts index c38dfc1f..3c03456d 100644 --- a/packages/core/src/restic/schemas.ts +++ b/packages/core/src/restic/schemas.ts @@ -4,6 +4,7 @@ export const REPOSITORY_BACKENDS = { local: "local", s3: "s3", r2: "r2", + b2: "b2", gcs: "gcs", azure: "azure", rclone: "rclone", @@ -50,6 +51,16 @@ export const s3RepositoryConfigSchema = z }) .extend(baseRepositoryConfigSchema.shape); +export const b2RepositoryConfigSchema = z + .object({ + backend: z.literal("b2"), + bucket: z.string().min(1), + path: z.string().min(1), + accountId: z.string().min(1), + accountKey: z.string().min(1), + }) + .extend(baseRepositoryConfigSchema.shape); + export const r2RepositoryConfigSchema = z .object({ backend: z.literal("r2"), @@ -124,6 +135,7 @@ export const sftpRepositoryConfigSchema = z export const repositoryConfigSchemaBase = z.discriminatedUnion("backend", [ s3RepositoryConfigSchema, r2RepositoryConfigSchema, + b2RepositoryConfigSchema, localRepositoryConfigSchema, gcsRepositoryConfigSchema, azureRepositoryConfigSchema,