feat: add b2 backend option to repository

This commit is contained in:
Nemeth Csaba Tibor 2026-04-01 15:01:43 +02:00
parent add0f2788f
commit c18abac474
6 changed files with 111 additions and 0 deletions

View file

@ -26,6 +26,7 @@ import {
gcsRepositoryConfigSchema, gcsRepositoryConfigSchema,
localRepositoryConfigSchema, localRepositoryConfigSchema,
r2RepositoryConfigSchema, r2RepositoryConfigSchema,
b2RepositoryConfigSchema,
rcloneRepositoryConfigSchema, rcloneRepositoryConfigSchema,
restRepositoryConfigSchema, restRepositoryConfigSchema,
s3RepositoryConfigSchema, s3RepositoryConfigSchema,
@ -46,6 +47,7 @@ import {
import { useServerFn } from "@tanstack/react-start"; import { useServerFn } from "@tanstack/react-start";
import { getServerConstants } from "~/server/lib/functions/server-constants"; import { getServerConstants } from "~/server/lib/functions/server-constants";
import { useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { B2RepositoryForm } from "./repository-forms/b2-repository-form";
const formBaseFields = { const formBaseFields = {
name: z.string().min(2).max(32), name: z.string().min(2).max(32),
@ -56,6 +58,7 @@ export const formSchema = z
.discriminatedUnion("backend", [ .discriminatedUnion("backend", [
localRepositoryConfigSchema.extend(formBaseFields), localRepositoryConfigSchema.extend(formBaseFields),
s3RepositoryConfigSchema.extend(formBaseFields), s3RepositoryConfigSchema.extend(formBaseFields),
b2RepositoryConfigSchema.extend(formBaseFields),
r2RepositoryConfigSchema.extend(formBaseFields), r2RepositoryConfigSchema.extend(formBaseFields),
gcsRepositoryConfigSchema.extend(formBaseFields), gcsRepositoryConfigSchema.extend(formBaseFields),
azureRepositoryConfigSchema.extend(formBaseFields), azureRepositoryConfigSchema.extend(formBaseFields),
@ -87,6 +90,7 @@ type Props = {
const defaultValuesForType = (repoBase: string) => ({ const defaultValuesForType = (repoBase: string) => ({
local: { backend: "local" as const, compressionMode: "auto" as const, path: repoBase }, local: { backend: "local" as const, compressionMode: "auto" as const, path: repoBase },
s3: { backend: "s3" as const, compressionMode: "auto" as const }, 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 }, r2: { backend: "r2" as const, compressionMode: "auto" as const },
gcs: { backend: "gcs" as const, compressionMode: "auto" as const }, gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
azure: { backend: "azure" as const, compressionMode: "auto" as const }, azure: { backend: "azure" as const, compressionMode: "auto" as const },
@ -185,6 +189,7 @@ export const CreateRepositoryForm = ({
<SelectItem value="local">Local</SelectItem> <SelectItem value="local">Local</SelectItem>
<SelectItem value="s3">S3</SelectItem> <SelectItem value="s3">S3</SelectItem>
<SelectItem value="r2">Cloudflare R2</SelectItem> <SelectItem value="r2">Cloudflare R2</SelectItem>
<SelectItem value="b2">Backblaze B2</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem> <SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem> <SelectItem value="azure">Azure Blob Storage</SelectItem>
<SelectItem value="rest">REST Server</SelectItem> <SelectItem value="rest">REST Server</SelectItem>
@ -313,6 +318,7 @@ export const CreateRepositoryForm = ({
{watchedBackend === "local" && <LocalRepositoryForm form={form} />} {watchedBackend === "local" && <LocalRepositoryForm form={form} />}
{watchedBackend === "s3" && <S3RepositoryForm form={form} />} {watchedBackend === "s3" && <S3RepositoryForm form={form} />}
{watchedBackend === "b2" && <B2RepositoryForm form={form} />}
{watchedBackend === "r2" && <R2RepositoryForm form={form} />} {watchedBackend === "r2" && <R2RepositoryForm form={form} />}
{watchedBackend === "gcs" && <GCSRepositoryForm form={form} />} {watchedBackend === "gcs" && <GCSRepositoryForm form={form} />}
{watchedBackend === "azure" && <AzureRepositoryForm form={form} />} {watchedBackend === "azure" && <AzureRepositoryForm form={form} />}

View file

@ -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<RepositoryFormValues>;
};
export const B2RepositoryForm = ({ form }: Props) => {
return (
<>
<FormField
control={form.control}
name="bucket"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="my-backup-bucket" {...field} />
</FormControl>
<FormDescription>Backblaze bucket name for storing backups.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket</FormLabel>
<FormControl>
<Input placeholder="/path/to" {...field} />
</FormControl>
<FormDescription>Path inside the bucket to store data</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountId"
render={({ field }) => (
<FormItem>
<FormLabel>Access Key ID</FormLabel>
<FormControl>
<Input placeholder="AKIAIOSFODNN7EXAMPLE" {...field} />
</FormControl>
<FormDescription>S3 access key ID for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="accountKey"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Access Key</FormLabel>
<FormControl>
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
</FormControl>
<FormDescription>S3 secret access key for authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
);
};

View file

@ -31,6 +31,14 @@ export const mapRepositoryConfigSecrets = async (
accessKeyId: await transformSecret(config.accessKeyId), accessKeyId: await transformSecret(config.accessKeyId),
secretAccessKey: await transformSecret(config.secretAccessKey), secretAccessKey: await transformSecret(config.secretAccessKey),
}; };
case "b2":
return {
...config,
customPassword,
cacert,
accountId: await transformSecret(config.accountId),
accountKey: await transformSecret(config.accountKey),
};
case "gcs": case "gcs":
return { return {
...config, ...config,

View file

@ -44,6 +44,10 @@ export const buildEnv = async (
env.AWS_REGION = "auto"; env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true"; env.AWS_S3_FORCE_PATH_STYLE = "true";
break; break;
case "b2":
env.B2_ACCOUNT_ID = await deps.resolveSecret(config.accountId);
env.B2_ACCOUNT_KEY = await deps.resolveSecret(config.accountKey);
break;
case "gcs": { case "gcs": {
const decryptedCredentials = await deps.resolveSecret(config.credentialsJson); const decryptedCredentials = await deps.resolveSecret(config.credentialsJson);
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`); const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);

View file

@ -15,6 +15,8 @@ export const buildRepoUrl = (config: RepositoryConfig): string => {
.replace(/\/$/, ""); .replace(/\/$/, "");
return `s3:${endpoint}/${config.bucket}`; return `s3:${endpoint}/${config.bucket}`;
} }
case "b2":
return `b2:${config.bucket}:${config.path}`;
case "gcs": case "gcs":
return `gs:${config.bucket}:/`; return `gs:${config.bucket}:/`;
case "azure": case "azure":

View file

@ -4,6 +4,7 @@ export const REPOSITORY_BACKENDS = {
local: "local", local: "local",
s3: "s3", s3: "s3",
r2: "r2", r2: "r2",
b2: "b2",
gcs: "gcs", gcs: "gcs",
azure: "azure", azure: "azure",
rclone: "rclone", rclone: "rclone",
@ -50,6 +51,16 @@ export const s3RepositoryConfigSchema = z
}) })
.extend(baseRepositoryConfigSchema.shape); .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 export const r2RepositoryConfigSchema = z
.object({ .object({
backend: z.literal("r2"), backend: z.literal("r2"),
@ -124,6 +135,7 @@ export const sftpRepositoryConfigSchema = z
export const repositoryConfigSchemaBase = z.discriminatedUnion("backend", [ export const repositoryConfigSchemaBase = z.discriminatedUnion("backend", [
s3RepositoryConfigSchema, s3RepositoryConfigSchema,
r2RepositoryConfigSchema, r2RepositoryConfigSchema,
b2RepositoryConfigSchema,
localRepositoryConfigSchema, localRepositoryConfigSchema,
gcsRepositoryConfigSchema, gcsRepositoryConfigSchema,
azureRepositoryConfigSchema, azureRepositoryConfigSchema,