Merge 486bf555b9 into a58fe82d48
This commit is contained in:
commit
6c3b6ce694
8 changed files with 113 additions and 1 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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} />}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue