mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
support cloudflare r2
This commit is contained in:
@@ -36,6 +36,7 @@ type Props = {
|
|||||||
const defaultValuesForType = {
|
const defaultValuesForType = {
|
||||||
local: { backend: "local" as const, compressionMode: "auto" as const },
|
local: { backend: "local" as const, compressionMode: "auto" as const },
|
||||||
s3: { backend: "s3" as const, compressionMode: "auto" as const },
|
s3: { backend: "s3" 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 },
|
||||||
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
||||||
@@ -115,6 +116,7 @@ export const CreateRepositoryForm = ({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<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="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>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -222,6 +224,67 @@ export const CreateRepositoryForm = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{watchedBackend === "r2" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="endpoint"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Endpoint</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="<account-id>.r2.cloudflarestorage.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>R2 endpoint (without https://). Find in R2 dashboard under bucket settings.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="bucket"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Bucket</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-backup-bucket" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>R2 bucket name for storing backups.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="accessKeyId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Access Key ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Access Key ID from R2 API tokens" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>R2 API token Access Key ID (create in Cloudflare R2 dashboard).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="secretAccessKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Secret Access Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>R2 API token Secret Access Key (shown once when creating token).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{watchedBackend === "gcs" && (
|
{watchedBackend === "gcs" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type } from "arktype";
|
|||||||
export const REPOSITORY_BACKENDS = {
|
export const REPOSITORY_BACKENDS = {
|
||||||
local: "local",
|
local: "local",
|
||||||
s3: "s3",
|
s3: "s3",
|
||||||
|
r2: "r2",
|
||||||
gcs: "gcs",
|
gcs: "gcs",
|
||||||
azure: "azure",
|
azure: "azure",
|
||||||
rclone: "rclone",
|
rclone: "rclone",
|
||||||
@@ -18,6 +19,14 @@ export const s3RepositoryConfigSchema = type({
|
|||||||
secretAccessKey: "string",
|
secretAccessKey: "string",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const r2RepositoryConfigSchema = type({
|
||||||
|
backend: "'r2'",
|
||||||
|
endpoint: "string",
|
||||||
|
bucket: "string",
|
||||||
|
accessKeyId: "string",
|
||||||
|
secretAccessKey: "string",
|
||||||
|
});
|
||||||
|
|
||||||
export const localRepositoryConfigSchema = type({
|
export const localRepositoryConfigSchema = type({
|
||||||
backend: "'local'",
|
backend: "'local'",
|
||||||
name: "string",
|
name: "string",
|
||||||
@@ -45,6 +54,7 @@ export const rcloneRepositoryConfigSchema = type({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
||||||
|
.or(r2RepositoryConfigSchema)
|
||||||
.or(localRepositoryConfigSchema)
|
.or(localRepositoryConfigSchema)
|
||||||
.or(gcsRepositoryConfigSchema)
|
.or(gcsRepositoryConfigSchema)
|
||||||
.or(azureRepositoryConfigSchema)
|
.or(azureRepositoryConfigSchema)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { repositoriesTable } from "../../db/schema";
|
|||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
import { restic } from "../../utils/restic";
|
import { restic } from "../../utils/restic";
|
||||||
import { cryptoUtils } from "../../utils/crypto";
|
import { cryptoUtils } from "../../utils/crypto";
|
||||||
|
import { logger } from "../../utils/logger";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
|
|
||||||
const listRepositories = async () => {
|
const listRepositories = async () => {
|
||||||
@@ -19,6 +20,7 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
|||||||
|
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
case "s3":
|
case "s3":
|
||||||
|
case "r2":
|
||||||
encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
|
encryptedConfig.accessKeyId = await cryptoUtils.encrypt(config.accessKeyId);
|
||||||
encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
|
encryptedConfig.secretAccessKey = await cryptoUtils.encrypt(config.secretAccessKey);
|
||||||
break;
|
break;
|
||||||
@@ -80,6 +82,22 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = toMessage(error);
|
const errorMessage = toMessage(error);
|
||||||
|
|
||||||
|
if (errorMessage.includes("already initialized") || errorMessage.includes("config file already exists")) {
|
||||||
|
logger.info(`Repository already exists on backend, connecting to existing repository: ${slug}`);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(repositoriesTable)
|
||||||
|
.set({
|
||||||
|
status: "healthy",
|
||||||
|
lastChecked: Date.now(),
|
||||||
|
lastError: null,
|
||||||
|
})
|
||||||
|
.where(eq(repositoriesTable.id, id));
|
||||||
|
|
||||||
|
return { repository: created, status: 201 };
|
||||||
|
}
|
||||||
|
|
||||||
await db.delete(repositoriesTable).where(eq(repositoriesTable.id, id));
|
await db.delete(repositoriesTable).where(eq(repositoriesTable.id, id));
|
||||||
|
|
||||||
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||||||
return `${REPOSITORY_BASE}/${config.name}`;
|
return `${REPOSITORY_BASE}/${config.name}`;
|
||||||
case "s3":
|
case "s3":
|
||||||
return `s3:${config.endpoint}/${config.bucket}`;
|
return `s3:${config.endpoint}/${config.bucket}`;
|
||||||
|
case "r2": {
|
||||||
|
const endpoint = config.endpoint.replace(/^https?:\/\//, '');
|
||||||
|
return `s3:${endpoint}/${config.bucket}`;
|
||||||
|
}
|
||||||
case "gcs":
|
case "gcs":
|
||||||
return `gs:${config.bucket}:/`;
|
return `gs:${config.bucket}:/`;
|
||||||
case "azure":
|
case "azure":
|
||||||
@@ -98,6 +102,12 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||||||
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
|
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
|
||||||
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
|
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
|
||||||
break;
|
break;
|
||||||
|
case "r2":
|
||||||
|
env.AWS_ACCESS_KEY_ID = await cryptoUtils.decrypt(config.accessKeyId);
|
||||||
|
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.decrypt(config.secretAccessKey);
|
||||||
|
env.AWS_REGION = "auto";
|
||||||
|
env.AWS_S3_FORCE_PATH_STYLE = "true";
|
||||||
|
break;
|
||||||
case "gcs": {
|
case "gcs": {
|
||||||
const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson);
|
const decryptedCredentials = await cryptoUtils.decrypt(config.credentialsJson);
|
||||||
const credentialsPath = path.join("/tmp", `gcs-credentials-${crypto.randomBytes(8).toString("hex")}.json`);
|
const credentialsPath = path.join("/tmp", `gcs-credentials-${crypto.randomBytes(8).toString("hex")}.json`);
|
||||||
|
|||||||
Reference in New Issue
Block a user