From e4f66de4471c21524db231b1782e825973bda310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Tr=C3=A1vn=C3=ADk?= Date: Mon, 29 Dec 2025 11:16:36 +0100 Subject: [PATCH] linting and SFTP volume support --- .../repositories/repositories.service.ts | 10 +- examples/config-file-import/README.md | 37 ++ .../zerobyte.config.example.json | 457 +++++++++--------- 3 files changed, 277 insertions(+), 227 deletions(-) diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index dd5088a2..8f8c414f 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; import { eq, or } from "drizzle-orm"; -import { InternalServerError, NotFoundError } from "http-errors-enhanced"; +import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced"; import { db } from "../../db/db"; import { repositoriesTable } from "../../db/schema"; import { toMessage } from "../../utils/errors"; @@ -74,7 +74,9 @@ const createRepository = async (name: string, config: RepositoryConfig, compress where: eq(repositoriesTable.shortId, shortId), }); if (existingByShortId) { - throw new ConflictError(`A repository with shortId '${shortId}' already exists. The shortId is used as the subdirectory name for local repositories.`); + throw new ConflictError( + `A repository with shortId '${shortId}' already exists. The shortId is used as the subdirectory name for local repositories.`, + ); } } else { shortId = generateShortId(); @@ -95,13 +97,13 @@ const createRepository = async (name: string, config: RepositoryConfig, compress if (repoExists && !config.isExistingRepository) { throw new ConflictError( `A restic repository already exists at this location. ` + - `If you want to use the existing repository, set "isExistingRepository": true in the config.` + `If you want to use the existing repository, set "isExistingRepository": true in the config.`, ); } if (!repoExists && config.isExistingRepository) { throw new InternalServerError( - `Cannot access existing repository. Verify the path/credentials are correct and the repository exists.` + `Cannot access existing repository. Verify the path/credentials are correct and the repository exists.`, ); } diff --git a/examples/config-file-import/README.md b/examples/config-file-import/README.md index a36c1671..3e37f353 100644 --- a/examples/config-file-import/README.md +++ b/examples/config-file-import/README.md @@ -264,6 +264,43 @@ This example is intended to be the primary, copy/paste-friendly reference for co } ``` +#### SFTP + +```json +{ + "name": "sftp-volume", + "config": { + "backend": "sftp", + "host": "sftp.example.com", + "port": 22, + "username": "user", + "password": "${SFTP_PASSWORD}", + "path": "/data", + "readOnly": false, + "skipHostKeyCheck": true + } +} +``` + +For key-based authentication: + +```json +{ + "name": "sftp-volume-key", + "config": { + "backend": "sftp", + "host": "sftp.example.com", + "port": 22, + "username": "user", + "privateKey": "${SFTP_PRIVATE_KEY}", + "path": "/data", + "readOnly": false, + "skipHostKeyCheck": false, + "knownHosts": "sftp.example.com ssh-ed25519 AAAA..." + } +} +``` + ### Repository types #### Local diff --git a/examples/config-file-import/zerobyte.config.example.json b/examples/config-file-import/zerobyte.config.example.json index 33efda9a..584fa7d1 100644 --- a/examples/config-file-import/zerobyte.config.example.json +++ b/examples/config-file-import/zerobyte.config.example.json @@ -1,225 +1,236 @@ { - "volumes": [ - { - "name": "local-volume", - "config": { - "backend": "directory", - "path": "/mydata", - "readOnly": true - } - }, - { - "name": "nfs-volume", - "config": { - "backend": "nfs", - "server": "nfs.example.com", - "exportPath": "/data", - "port": 2049, - "version": "4", - "readOnly": false - } - }, - { - "name": "smb-volume", - "config": { - "backend": "smb", - "server": "smb.example.com", - "share": "shared", - "username": "user", - "password": "${SMB_PASSWORD}", - "vers": "3.0", - "domain": "WORKGROUP", - "port": 445, - "readOnly": false - } - }, - { - "name": "webdav-volume", - "config": { - "backend": "webdav", - "server": "webdav.example.com", - "path": "/remote.php/webdav", - "username": "user", - "password": "${WEBDAV_PASSWORD}", - "port": 80, - "readOnly": false, - "ssl": true - } - } - ], - "repositories": [ - { - "name": "local-repo", - "config": { - "backend": "local", - "path": "/var/lib/zerobyte/repositories" - }, - "compressionMode": "auto" - }, - { - "name": "s3-repo", - "config": { - "backend": "s3", - "bucket": "mybucket", - "accessKeyId": "${ACCESS_KEY_ID}", - "secretAccessKey": "${SECRET_ACCESS_KEY}" - }, - "compressionMode": "auto" - }, - { - "name": "gcs-repo", - "config": { - "backend": "gcs", - "bucket": "mybucket", - "projectId": "my-gcp-project", - "credentialsJson": "${GCS_CREDENTIALS}" - } - }, - { - "name": "azure-repo", - "config": { - "backend": "azure", - "container": "mycontainer", - "accountName": "myaccount", - "accountKey": "${AZURE_KEY}" - } - }, - { - "name": "rclone-repo", - "config": { - "backend": "rclone", - "remote": "myremote", - "path": "backups/zerobyte" - } - }, - { - "name": "webdav-repo", - "config": { - "backend": "webdav", - "server": "webdav.example.com", - "path": "/remote.php/webdav", - "username": "user", - "password": "${WEBDAV_PASSWORD}", - "port": 80, - "ssl": true - } - }, - { - "name": "sftp-repo", - "config": { - "backend": "sftp", - "host": "sftp.example.com", - "port": 22, - "user": "sftpuser", - "privateKey": "${SFTP_PRIVATE_KEY}", - "path": "/backups" - } - } - ], - "backupSchedules": [ - { - "name": "local-volume-local-repo", - "volume": "local-volume", - "repository": "local-repo", - "cronExpression": "0 2 * * *", - "retentionPolicy": { "keepLast": 7, "keepDaily": 7 }, - "includePatterns": ["important-folder"], - "excludePatterns": ["*.tmp", "*.log"], - "excludeIfPresent": [".nobackup"], - "oneFileSystem": false, - "enabled": true, - "notifications": ["slack-alerts"], - "mirrors": [ - { "repository": "s3-repo" } - ] - } - ], - "notificationDestinations": [ - { - "name": "slack-alerts", - "config": { - "type": "slack", - "webhookUrl": "${SLACK_WEBHOOK_URL}", - "channel": "#backups", - "username": "zerobyte", - "iconEmoji": ":floppy_disk:" - } - }, - { - "name": "email-admin", - "config": { - "type": "email", - "smtpHost": "smtp.example.com", - "smtpPort": 587, - "username": "admin@example.com", - "password": "${EMAIL_PASSWORD}", - "from": "zerobyte@example.com", - "to": ["admin@example.com"], - "useTLS": true - } - }, - { - "name": "discord-backups", - "config": { - "type": "discord", - "webhookUrl": "${DISCORD_WEBHOOK_URL}", - "username": "zerobyte", - "avatarUrl": "https://example.com/avatar.png", - "threadId": "1234567890" - } - }, - { - "name": "gotify-notify", - "config": { - "type": "gotify", - "serverUrl": "https://gotify.example.com", - "token": "${GOTIFY_TOKEN}", - "path": "/message", - "priority": 5 - } - }, - { - "name": "ntfy-notify", - "config": { - "type": "ntfy", - "serverUrl": "https://ntfy.example.com", - "topic": "zerobyte-backups", - "priority": "high", - "username": "ntfyuser", - "password": "${NTFY_PASSWORD}" - } - }, - { - "name": "pushover-notify", - "config": { - "type": "pushover", - "userKey": "${PUSHOVER_USER_KEY}", - "apiToken": "${PUSHOVER_API_TOKEN}", - "devices": "phone,tablet", - "priority": 1 - } - }, - { - "name": "telegram-notify", - "config": { - "type": "telegram", - "botToken": "${TELEGRAM_BOT_TOKEN}", - "chatId": "123456789" - } - }, - { - "name": "custom-shoutrrr", - "config": { - "type": "custom", - "shoutrrrUrl": "${SHOUTRRR_URL}" - } - } - ], - "recoveryKey": "${RECOVERY_KEY}", - "users": [ - { - "username": "admin", - "password": "${ADMIN_PASSWORD}" - } - ] + "volumes": [ + { + "name": "local-volume", + "config": { + "backend": "directory", + "path": "/mydata", + "readOnly": true + } + }, + { + "name": "nfs-volume", + "config": { + "backend": "nfs", + "server": "nfs.example.com", + "exportPath": "/data", + "port": 2049, + "version": "4", + "readOnly": false + } + }, + { + "name": "smb-volume", + "config": { + "backend": "smb", + "server": "smb.example.com", + "share": "shared", + "username": "user", + "password": "${SMB_PASSWORD}", + "vers": "3.0", + "domain": "WORKGROUP", + "port": 445, + "readOnly": false + } + }, + { + "name": "webdav-volume", + "config": { + "backend": "webdav", + "server": "webdav.example.com", + "path": "/remote.php/webdav", + "username": "user", + "password": "${WEBDAV_PASSWORD}", + "port": 80, + "readOnly": false, + "ssl": true + } + }, + { + "name": "sftp-volume", + "config": { + "backend": "sftp", + "host": "sftp.example.com", + "port": 22, + "username": "user", + "password": "${SFTP_PASSWORD}", + "path": "/data", + "readOnly": false, + "skipHostKeyCheck": true + } + } + ], + "repositories": [ + { + "name": "local-repo", + "config": { + "backend": "local", + "path": "/var/lib/zerobyte/repositories" + }, + "compressionMode": "auto" + }, + { + "name": "s3-repo", + "config": { + "backend": "s3", + "bucket": "mybucket", + "accessKeyId": "${ACCESS_KEY_ID}", + "secretAccessKey": "${SECRET_ACCESS_KEY}" + }, + "compressionMode": "auto" + }, + { + "name": "gcs-repo", + "config": { + "backend": "gcs", + "bucket": "mybucket", + "projectId": "my-gcp-project", + "credentialsJson": "${GCS_CREDENTIALS}" + } + }, + { + "name": "azure-repo", + "config": { + "backend": "azure", + "container": "mycontainer", + "accountName": "myaccount", + "accountKey": "${AZURE_KEY}" + } + }, + { + "name": "rclone-repo", + "config": { + "backend": "rclone", + "remote": "myremote", + "path": "backups/zerobyte" + } + }, + { + "name": "webdav-repo", + "config": { + "backend": "webdav", + "server": "webdav.example.com", + "path": "/remote.php/webdav", + "username": "user", + "password": "${WEBDAV_PASSWORD}", + "port": 80, + "ssl": true + } + }, + { + "name": "sftp-repo", + "config": { + "backend": "sftp", + "host": "sftp.example.com", + "port": 22, + "user": "sftpuser", + "privateKey": "${SFTP_PRIVATE_KEY}", + "path": "/backups" + } + } + ], + "backupSchedules": [ + { + "name": "local-volume-local-repo", + "volume": "local-volume", + "repository": "local-repo", + "cronExpression": "0 2 * * *", + "retentionPolicy": { "keepLast": 7, "keepDaily": 7 }, + "includePatterns": ["important-folder"], + "excludePatterns": ["*.tmp", "*.log"], + "excludeIfPresent": [".nobackup"], + "oneFileSystem": false, + "enabled": true, + "notifications": ["slack-alerts"], + "mirrors": [{ "repository": "s3-repo" }] + } + ], + "notificationDestinations": [ + { + "name": "slack-alerts", + "config": { + "type": "slack", + "webhookUrl": "${SLACK_WEBHOOK_URL}", + "channel": "#backups", + "username": "zerobyte", + "iconEmoji": ":floppy_disk:" + } + }, + { + "name": "email-admin", + "config": { + "type": "email", + "smtpHost": "smtp.example.com", + "smtpPort": 587, + "username": "admin@example.com", + "password": "${EMAIL_PASSWORD}", + "from": "zerobyte@example.com", + "to": ["admin@example.com"], + "useTLS": true + } + }, + { + "name": "discord-backups", + "config": { + "type": "discord", + "webhookUrl": "${DISCORD_WEBHOOK_URL}", + "username": "zerobyte", + "avatarUrl": "https://example.com/avatar.png", + "threadId": "1234567890" + } + }, + { + "name": "gotify-notify", + "config": { + "type": "gotify", + "serverUrl": "https://gotify.example.com", + "token": "${GOTIFY_TOKEN}", + "path": "/message", + "priority": 5 + } + }, + { + "name": "ntfy-notify", + "config": { + "type": "ntfy", + "serverUrl": "https://ntfy.example.com", + "topic": "zerobyte-backups", + "priority": "high", + "username": "ntfyuser", + "password": "${NTFY_PASSWORD}" + } + }, + { + "name": "pushover-notify", + "config": { + "type": "pushover", + "userKey": "${PUSHOVER_USER_KEY}", + "apiToken": "${PUSHOVER_API_TOKEN}", + "devices": "phone,tablet", + "priority": 1 + } + }, + { + "name": "telegram-notify", + "config": { + "type": "telegram", + "botToken": "${TELEGRAM_BOT_TOKEN}", + "chatId": "123456789" + } + }, + { + "name": "custom-shoutrrr", + "config": { + "type": "custom", + "shoutrrrUrl": "${SHOUTRRR_URL}" + } + } + ], + "recoveryKey": "${RECOVERY_KEY}", + "users": [ + { + "username": "admin", + "password": "${ADMIN_PASSWORD}" + } + ] }