mirror of
https://github.com/nicotsx/zerobyte.git
synced 2026-06-19 14:02:25 -04:00
feat: add support for SFTP volume backend (#249)
* feat: add support for SFTP volume backend * feat: allow more secure SFTP connection with known hosts * refactor: make default value for skip host key check to false * refactor: use os.userInfo when setting uid/gid in mount commands
This commit is contained in:
@@ -58,7 +58,7 @@ const defaultValuesForType = {
|
||||
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
||||
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
||||
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
||||
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 },
|
||||
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22, skipHostKeyCheck: false },
|
||||
};
|
||||
|
||||
export const CreateRepositoryForm = ({
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../../../components/ui/form";
|
||||
import { Input } from "../../../../components/ui/input";
|
||||
import { Textarea } from "../../../../components/ui/textarea";
|
||||
import { Switch } from "../../../../components/ui/switch";
|
||||
import type { RepositoryFormValues } from "../create-repository-form";
|
||||
|
||||
type Props = {
|
||||
@@ -72,7 +73,7 @@ export const SftpRepositoryForm = ({ form }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="backups/ironmount" {...field} />
|
||||
<Input placeholder="backups/zerobyte" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Repository path on the SFTP server. </FormDescription>
|
||||
<FormMessage />
|
||||
@@ -96,6 +97,47 @@ export const SftpRepositoryForm = ({ form }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skipHostKeyCheck"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||
<FormDescription>
|
||||
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!form.watch("skipHostKeyCheck") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="knownHosts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Known Hosts</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||
className="font-mono text-xs"
|
||||
rows={3}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The contents of the <code>known_hosts</code> file for this server.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import { volumeConfigSchemaBase } from "~/schemas/volumes";
|
||||
import { testConnectionMutation } from "../../../api-client/@tanstack/react-query.gen";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||
import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm } from "./volume-forms";
|
||||
import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm, SFTPForm } from "./volume-forms";
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
@@ -46,6 +46,7 @@ const defaultValuesForType = {
|
||||
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
|
||||
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
||||
rclone: { backend: "rclone" as const, path: "/" },
|
||||
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false },
|
||||
};
|
||||
|
||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
||||
@@ -96,7 +97,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
const handleTestConnection = async () => {
|
||||
const formValues = getValues();
|
||||
|
||||
if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
|
||||
if (
|
||||
formValues.backend === "nfs" ||
|
||||
formValues.backend === "smb" ||
|
||||
formValues.backend === "webdav" ||
|
||||
formValues.backend === "sftp"
|
||||
) {
|
||||
testBackendConnection.mutate({
|
||||
body: { config: formValues },
|
||||
});
|
||||
@@ -177,6 +183,18 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
<p>Remote mounts require SYS_ADMIN capability</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SelectItem disabled={!capabilities.sysAdmin} value="sftp">
|
||||
SFTP
|
||||
</SelectItem>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
|
||||
<p>Remote mounts require SYS_ADMIN capability</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
@@ -204,6 +222,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
{watchedBackend === "webdav" && <WebDAVForm form={form} />}
|
||||
{watchedBackend === "smb" && <SMBForm form={form} />}
|
||||
{watchedBackend === "rclone" && <RcloneForm form={form} />}
|
||||
{watchedBackend === "sftp" && <SFTPForm form={form} />}
|
||||
{watchedBackend && watchedBackend !== "directory" && watchedBackend !== "rclone" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -3,3 +3,4 @@ export { NFSForm } from "./nfs-form";
|
||||
export { SMBForm } from "./smb-form";
|
||||
export { WebDAVForm } from "./webdav-form";
|
||||
export { RcloneForm } from "./rclone-form";
|
||||
export { SFTPForm } from "./sftp-form";
|
||||
|
||||
161
app/client/modules/volumes/components/volume-forms/sftp-form.tsx
Normal file
161
app/client/modules/volumes/components/volume-forms/sftp-form.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { FormValues } from "../create-volume-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 { Textarea } from "../../../../components/ui/textarea";
|
||||
import { Switch } from "../../../../components/ui/switch";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<FormValues>;
|
||||
};
|
||||
|
||||
export const SFTPForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>SFTP server hostname or IP address.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="22"
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>SFTP server port (default: 22).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Username for SFTP authentication.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>Password for SFTP authentication (optional if using private key).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="privateKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Private Key (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
className="font-mono text-xs"
|
||||
rows={5}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>SSH private key for authentication (optional if using password).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/backups" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Path to the directory on the SFTP server.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skipHostKeyCheck"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||
<FormDescription>
|
||||
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!form.watch("skipHostKeyCheck") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="knownHosts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Known Hosts</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||
className="font-mono text-xs"
|
||||
rows={3}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The contents of the <code>known_hosts</code> file for this server.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user