diff --git a/AGENTS.md b/AGENTS.md index 92f7ff1..9d09ac2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ ## Project Overview -Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage). +Zerobyte is a backup automation tool built on top of Restic that provides a web interface for scheduling, managing, and monitoring encrypted backups. It supports multiple volume backends (NFS, SMB, WebDAV, SFTP, local directories) and repository backends (S3, Azure, GCS, local, and rclone-based storage). ## Technology Stack @@ -99,7 +99,7 @@ The server follows a modular service-oriented architecture: Each module follows a controller � service � database pattern: - `auth/` - User authentication and session management -- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, directories) +- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, SFTP, directories) - `repositories/` - Restic repository management (S3, Azure, GCS, local, rclone) - `backups/` - Backup schedule management and execution - `notifications/` - Notification system with multiple providers (Discord, email, Gotify, Ntfy, Slack, Pushover) @@ -109,7 +109,7 @@ Each module follows a controller � service � database pattern: - `lifecycle/` - Application startup/shutdown hooks **Backends** (`app/server/modules/backends/`): -Each volume backend (NFS, SMB, WebDAV, directory) implements mounting logic using system tools (mount.nfs, mount.cifs, davfs2). +Each volume backend (NFS, SMB, WebDAV, SFTP, directory) implements mounting logic using system tools (mount.nfs, mount.cifs, davfs2, sshfs). **Jobs** (`app/server/jobs/`): Cron-based background jobs managed by the Scheduler: @@ -158,7 +158,7 @@ Routes are organized in feature modules at `app/client/modules/*/routes/`. `app/schemas/` contains ArkType schemas used by both client and server: -- Volume configurations (NFS, SMB, WebDAV, directory) +- Volume configurations (NFS, SMB, WebDAV, SFTP, directory) - Repository configurations (S3, Azure, GCS, local, rclone) - Restic command output parsing types - Backend status types diff --git a/Dockerfile b/Dockerfile index c95dab9..3ec7d89 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG BUN_VERSION="1.3.5" FROM oven/bun:${BUN_VERSION}-alpine AS base RUN apk upgrade --no-cache && \ - apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 + apk add --no-cache davfs2=1.6.1-r2 openssh-client fuse3 sshfs # ------------------------------ diff --git a/README.md b/README.md index 8000055..b77f57e 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Zerobyte is a backup automation tool that helps you save your data across multip -   **Automated backups** with encryption, compression and retention policies powered by Restic -   **Flexible scheduling** For automated backup jobs with fine-grained retention policies -   **End-to-end encryption** ensuring your data is always protected --   **Multi-protocol support**: Backup from NFS, SMB, WebDAV, or local directories +-   **Multi-protocol support**: Backup from NFS, SMB, WebDAV, SFTP, or local directories ## Installation @@ -94,7 +94,7 @@ services: - ✅ Improved security by reducing container capabilities - ✅ Support for local directories - ✅ Keep support all repository types (local, S3, GCS, Azure, rclone) -- ❌ Cannot mount NFS, SMB, or WebDAV shares directly from Zerobyte +- ❌ Cannot mount NFS, SMB, WebDAV, or SFTP shares directly from Zerobyte If you need remote mount capabilities, keep the original configuration with `cap_add: SYS_ADMIN` and `devices: /dev/fuse:/dev/fuse`. @@ -104,7 +104,7 @@ See [examples/README.md](examples/README.md) for runnable, copy/paste-friendly e ## Adding your first volume -Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, and local directories. A volume represents the source data you want to back up and monitor. +Zerobyte supports multiple volume backends including NFS, SMB, WebDAV, SFTP, and local directories. A volume represents the source data you want to back up and monitor. To add your first volume, navigate to the "Volumes" section in the web interface and click on "Create volume". Fill in the required details such as volume name, type, and connection settings. diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index d85c5ee..58f79ea 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -169,6 +169,17 @@ export type ListVolumesResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -196,7 +207,7 @@ export type ListVolumesResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }>; }; @@ -221,6 +232,17 @@ export type CreateVolumeData = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -270,6 +292,17 @@ export type CreateVolumeResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -297,7 +330,7 @@ export type CreateVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -322,6 +355,17 @@ export type TestConnectionData = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -424,6 +468,17 @@ export type GetVolumeResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -451,7 +506,7 @@ export type GetVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -478,6 +533,17 @@ export type UpdateVolumeData = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -536,6 +602,17 @@ export type UpdateVolumeResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -563,7 +640,7 @@ export type UpdateVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; }; @@ -773,8 +850,10 @@ export type ListRepositoriesResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -850,8 +929,10 @@ export type CreateRepositoryData = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; name: string; compressionMode?: 'auto' | 'max' | 'off'; @@ -989,8 +1070,10 @@ export type GetRepositoryResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1093,8 +1176,10 @@ export type UpdateRepositoryResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1367,8 +1452,10 @@ export type ListBackupSchedulesResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1410,6 +1497,17 @@ export type ListBackupSchedulesResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1437,7 +1535,7 @@ export type ListBackupSchedulesResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -1616,8 +1714,10 @@ export type GetBackupScheduleResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1659,6 +1759,17 @@ export type GetBackupScheduleResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1686,7 +1797,7 @@ export type GetBackupScheduleResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -1846,8 +1957,10 @@ export type GetBackupScheduleForVolumeResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -1889,6 +2002,17 @@ export type GetBackupScheduleForVolumeResponses = { path: string; remote: string; readOnly?: boolean; + } | { + backend: 'sftp'; + host: string; + path: string; + username: string; + port?: number; + skipHostKeyCheck?: boolean; + knownHosts?: string; + password?: string; + privateKey?: string; + readOnly?: boolean; } | { backend: 'smb'; password: string; @@ -1916,7 +2040,7 @@ export type GetBackupScheduleForVolumeResponses = { name: string; shortId: string; status: 'error' | 'mounted' | 'unmounted'; - type: 'directory' | 'nfs' | 'rclone' | 'smb' | 'webdav'; + type: 'directory' | 'nfs' | 'rclone' | 'sftp' | 'smb' | 'webdav'; updatedAt: number; }; volumeId: number; @@ -2243,8 +2367,10 @@ export type GetScheduleMirrorsResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; @@ -2347,8 +2473,10 @@ export type UpdateScheduleMirrorsResponses = { privateKey: string; user: string; port?: number; + skipHostKeyCheck?: boolean; customPassword?: string; isExistingRepository?: boolean; + knownHosts?: string; }; createdAt: number; id: string; diff --git a/app/client/components/volume-icon.tsx b/app/client/components/volume-icon.tsx index 3094d86..c21b3a0 100644 --- a/app/client/components/volume-icon.tsx +++ b/app/client/components/volume-icon.tsx @@ -1,53 +1,52 @@ -import { Cloud, Folder, Server, Share2 } from "lucide-react"; +import { Cloud, Folder, Server } from "lucide-react"; import type { BackendType } from "~/schemas/volumes"; type VolumeIconProps = { backend: BackendType; }; -const getIconAndColor = (backend: BackendType) => { +const getIconAndLabel = (backend: BackendType) => { switch (backend) { case "directory": return { icon: Folder, - color: "text-blue-600 dark:text-blue-400", label: "Directory", }; case "nfs": return { icon: Server, - color: "text-orange-600 dark:text-orange-400", label: "NFS", }; case "smb": return { - icon: Share2, - color: "text-purple-600 dark:text-purple-400", + icon: Server, label: "SMB", }; case "webdav": return { - icon: Cloud, - color: "text-green-600 dark:text-green-400", + icon: Server, label: "WebDAV", }; case "rclone": return { icon: Cloud, - color: "text-cyan-600 dark:text-cyan-400", label: "Rclone", }; + case "sftp": + return { + icon: Server, + label: "SFTP", + }; default: return { icon: Folder, - color: "text-gray-600 dark:text-gray-400", label: "Unknown", }; } }; export const VolumeIcon = ({ backend }: VolumeIconProps) => { - const { icon: Icon, label } = getIconAndColor(backend); + const { icon: Icon, label } = getIconAndLabel(backend); return ( diff --git a/app/client/modules/repositories/components/create-repository-form.tsx b/app/client/modules/repositories/components/create-repository-form.tsx index dce3ff5..08fa6e0 100644 --- a/app/client/modules/repositories/components/create-repository-form.tsx +++ b/app/client/modules/repositories/components/create-repository-form.tsx @@ -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 = ({ diff --git a/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx index 321d3ba..c5286a6 100644 --- a/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/sftp-repository-form.tsx @@ -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) => { Path - + Repository path on the SFTP server. @@ -96,6 +97,47 @@ export const SftpRepositoryForm = ({ form }: Props) => { )} /> + ( + +
+ Skip Host Key Verification + + Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys. + +
+ + + +
+ )} + /> + {!form.watch("skipHostKeyCheck") && ( + ( + + Known Hosts + +