This commit is contained in:
WitherCOM 2026-05-11 17:15:28 +01:00 committed by GitHub
commit 6c3b6ce694
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 113 additions and 1 deletions

View file

@ -8,7 +8,7 @@
## Project Overview ## 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, SFTP, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage). 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, SFTP, local directories) and repository backends (S3, B2, Azure, GCS, local, and rclone-based storage).
### Type Checking ### Type Checking

View file

@ -233,6 +233,7 @@ A repository is where your backups will be securely stored encrypted. Zerobyte s
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc. - **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
- **Google Cloud Storage** - Google's cloud storage service - **Google Cloud Storage** - Google's cloud storage service
- **Azure Blob Storage** - Microsoft Azure storage - **Azure Blob Storage** - Microsoft Azure storage
- [**Backblaze B2**](https://www.backblaze.com/cloud-storage) - S3 compatible cloud storage
- **rclone remotes** - 40+ cloud storage providers via rclone (see below) - **rclone remotes** - 40+ cloud storage providers via rclone (see below)
Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features. Repositories are optimized for storage efficiency and data integrity, leveraging Restic's deduplication and encryption features.

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

@ -18,6 +18,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,