feat: add support for REST server

This commit is contained in:
Nicolas Meienberger
2025-11-16 18:24:09 +01:00
parent df6b70c96f
commit 4d48d7be58
5 changed files with 98 additions and 2 deletions

View File

@@ -41,6 +41,7 @@ const defaultValuesForType = {
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 },
rest: { backend: "rest" as const, compressionMode: "auto" as const },
}; };
export const CreateRepositoryForm = ({ export const CreateRepositoryForm = ({
@@ -126,6 +127,7 @@ export const CreateRepositoryForm = ({
<SelectItem value="r2">Cloudflare R2</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>
<SelectItem value="rest">REST Server</SelectItem>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<SelectItem disabled={!capabilities.rclone} value="rclone"> <SelectItem disabled={!capabilities.rclone} value="rclone">
@@ -546,6 +548,67 @@ export const CreateRepositoryForm = ({
</> </>
))} ))}
{watchedBackend === "rest" && (
<>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>REST Server URL</FormLabel>
<FormControl>
<Input placeholder="http://192.168.1.30:8000" {...field} />
</FormControl>
<FormDescription>URL of the REST server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Repository Path (Optional)</FormLabel>
<FormControl>
<Input placeholder="my-backup-repo" {...field} />
</FormControl>
<FormDescription>Path to the repository on the REST server (leave empty for root).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (Optional)</FormLabel>
<FormControl>
<Input placeholder="username" {...field} />
</FormControl>
<FormDescription>Username for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{mode === "update" && ( {mode === "update" && (
<Button type="submit" className="w-full" loading={loading}> <Button type="submit" className="w-full" loading={loading}>
Save Changes Save Changes

View File

@@ -1,4 +1,4 @@
import { Database, HardDrive, Cloud } from "lucide-react"; import { Database, HardDrive, Cloud, Server } from "lucide-react";
import type { RepositoryBackend } from "~/schemas/restic"; import type { RepositoryBackend } from "~/schemas/restic";
type Props = { type Props = {
@@ -14,6 +14,8 @@ export const RepositoryIcon = ({ backend, className = "h-4 w-4" }: Props) => {
return <Cloud className={className} />; return <Cloud className={className} />;
case "gcs": case "gcs":
return <Cloud className={className} />; return <Cloud className={className} />;
case "rest":
return <Server className={className} />;
default: default:
return <Database className={className} />; return <Database className={className} />;
} }

View File

@@ -7,6 +7,7 @@ export const REPOSITORY_BACKENDS = {
gcs: "gcs", gcs: "gcs",
azure: "azure", azure: "azure",
rclone: "rclone", rclone: "rclone",
rest: "rest",
} as const; } as const;
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS; export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
@@ -59,12 +60,21 @@ export const rcloneRepositoryConfigSchema = type({
path: "string", path: "string",
}).and(baseRepositoryConfigSchema); }).and(baseRepositoryConfigSchema);
export const restRepositoryConfigSchema = type({
backend: "'rest'",
url: "string",
username: "string?",
password: "string?",
path: "string?",
}).and(baseRepositoryConfigSchema);
export const repositoryConfigSchema = s3RepositoryConfigSchema export const repositoryConfigSchema = s3RepositoryConfigSchema
.or(r2RepositoryConfigSchema) .or(r2RepositoryConfigSchema)
.or(localRepositoryConfigSchema) .or(localRepositoryConfigSchema)
.or(gcsRepositoryConfigSchema) .or(gcsRepositoryConfigSchema)
.or(azureRepositoryConfigSchema) .or(azureRepositoryConfigSchema)
.or(rcloneRepositoryConfigSchema); .or(rcloneRepositoryConfigSchema)
.or(restRepositoryConfigSchema);
export type RepositoryConfig = typeof repositoryConfigSchema.infer; export type RepositoryConfig = typeof repositoryConfigSchema.infer;

View File

@@ -33,6 +33,14 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
case "azure": case "azure":
encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey); encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
break; break;
case "rest":
if (config.username) {
encryptedConfig.username = await cryptoUtils.encrypt(config.username);
}
if (config.password) {
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
}
break;
} }
return encryptedConfig as RepositoryConfig; return encryptedConfig as RepositoryConfig;

View File

@@ -84,6 +84,10 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
return `azure:${config.container}:/`; return `azure:${config.container}:/`;
case "rclone": case "rclone":
return `rclone:${config.remote}:${config.path}`; return `rclone:${config.remote}:${config.path}`;
case "rest": {
const path = config.path ? `/${config.path}` : "";
return `rest:${config.url}${path}`;
}
default: { default: {
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`); throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
} }
@@ -133,6 +137,15 @@ const buildEnv = async (config: RepositoryConfig) => {
} }
break; break;
} }
case "rest": {
if (config.username) {
env.RESTIC_REST_USERNAME = await cryptoUtils.decrypt(config.username);
}
if (config.password) {
env.RESTIC_REST_PASSWORD = await cryptoUtils.decrypt(config.password);
}
break;
}
} }
return env; return env;