add sftp support
This commit is contained in:
parent
4d48d7be58
commit
122f351555
5 changed files with 103 additions and 3 deletions
|
|
@ -103,9 +103,10 @@ Now, when adding a new volume in the Ironmount web interface, you can select "Di
|
|||
A repository is where your backups will be securely stored encrypted. Ironmount supports multiple storage backends for your backup repositories:
|
||||
|
||||
- **Local directories** - Store backups on local disk at `/var/lib/ironmount/repositories/<repository-name>`
|
||||
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, etc.
|
||||
- **S3-compatible storage** - Amazon S3, MinIO, Wasabi, DigitalOcean Spaces, Cloudflare R2, etc.
|
||||
- **Google Cloud Storage** - Google's cloud storage service
|
||||
- **Azure Blob Storage** - Microsoft Azure storage
|
||||
- **SFTP** - Secure File Transfer Protocol
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,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 },
|
||||
};
|
||||
|
||||
export const CreateRepositoryForm = ({
|
||||
|
|
@ -128,6 +129,7 @@ export const CreateRepositoryForm = ({
|
|||
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
||||
<SelectItem value="azure">Azure Blob Storage</SelectItem>
|
||||
<SelectItem value="rest">REST Server</SelectItem>
|
||||
<SelectItem value="sftp">SFTP</SelectItem>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<SelectItem disabled={!capabilities.rclone} value="rclone">
|
||||
|
|
@ -609,6 +611,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" && (
|
||||
<Button type="submit" className="w-full" loading={loading}>
|
||||
Save Changes
|
||||
|
|
|
|||
|
|
@ -68,13 +68,23 @@ export const restRepositoryConfigSchema = type({
|
|||
path: "string?",
|
||||
}).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
|
||||
.or(r2RepositoryConfigSchema)
|
||||
.or(localRepositoryConfigSchema)
|
||||
.or(gcsRepositoryConfigSchema)
|
||||
.or(azureRepositoryConfigSchema)
|
||||
.or(rcloneRepositoryConfigSchema)
|
||||
.or(restRepositoryConfigSchema);
|
||||
.or(restRepositoryConfigSchema)
|
||||
.or(sftpRepositoryConfigSchema);
|
||||
|
||||
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const listRepositories = async () => {
|
|||
};
|
||||
|
||||
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) {
|
||||
encryptedConfig.customPassword = await cryptoUtils.encrypt(config.customPassword);
|
||||
|
|
@ -41,6 +41,9 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
|||
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
|
||||
}
|
||||
break;
|
||||
case "sftp":
|
||||
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
|
||||
break;
|
||||
}
|
||||
|
||||
return encryptedConfig as RepositoryConfig;
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||
const path = config.path ? `/${config.path}` : "";
|
||||
return `rest:${config.url}${path}`;
|
||||
}
|
||||
case "sftp":
|
||||
return `sftp:${config.username}@${config.server}:${config.port}${config.path}`;
|
||||
default: {
|
||||
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
||||
}
|
||||
|
|
@ -146,6 +148,13 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||
}
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue