add restic sftp support
This commit is contained in:
parent
c0bef7f65e
commit
ae1d822260
4 changed files with 102 additions and 2 deletions
|
|
@ -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 },
|
||||||
|
sftp: { backend: "sftp" as const, compressionMode: "auto" as const, port: 22 },
|
||||||
};
|
};
|
||||||
|
|
||||||
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="sftp">SFTP</SelectItem>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<SelectItem disabled={!capabilities.rclone} value="rclone">
|
<SelectItem disabled={!capabilities.rclone} value="rclone">
|
||||||
|
|
@ -546,6 +548,81 @@ export const CreateRepositoryForm = ({
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{watchedBackend === "sftp" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="server"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="sftp.example.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>SFTP/SSH 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} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>SSH server port (default: 22).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="sftpuser" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>SSH account username.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>SSH account password.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="/home/user/backups/repository" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Remote path on the SFTP server where backups will be stored.</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
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export const REPOSITORY_BACKENDS = {
|
||||||
gcs: "gcs",
|
gcs: "gcs",
|
||||||
azure: "azure",
|
azure: "azure",
|
||||||
rclone: "rclone",
|
rclone: "rclone",
|
||||||
|
sftp: "sftp",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
||||||
|
|
@ -59,12 +60,22 @@ export const rcloneRepositoryConfigSchema = type({
|
||||||
path: "string",
|
path: "string",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
|
export const sftpRepositoryConfigSchema = type({
|
||||||
|
backend: "'sftp'",
|
||||||
|
server: "string",
|
||||||
|
port: type("string.integer").or(type("number")).to("1 <= number <= 65536").default(22),
|
||||||
|
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(sftpRepositoryConfigSchema);
|
||||||
|
|
||||||
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const listRepositories = async () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
|
const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig> => {
|
||||||
const encryptedConfig: Record<string, string | boolean> = { ...config };
|
const encryptedConfig: Record<string, string | boolean | number> = { ...config };
|
||||||
|
|
||||||
if (config.customPassword) {
|
if (config.customPassword) {
|
||||||
encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
|
encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
|
||||||
|
|
@ -33,6 +33,9 @@ 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 "sftp":
|
||||||
|
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptedConfig as RepositoryConfig;
|
return encryptedConfig as RepositoryConfig;
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,8 @@ 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 "sftp":
|
||||||
|
return `sftp:${config.username}@${config.server}:${config.port}${config.path}`;
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +135,13 @@ const buildEnv = async (config: RepositoryConfig) => {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "sftp": {
|
||||||
|
const decryptedPassword = await cryptoUtils.decrypt(config.password);
|
||||||
|
const passwordFilePath = path.join("/tmp", `ironmount-sftp-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
|
||||||
|
await fs.writeFile(passwordFilePath, decryptedPassword, { mode: 0o600 });
|
||||||
|
env.RESTIC_SFTP_PASSWORD_FILE = passwordFilePath;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue