linting and SFTP volume support

This commit is contained in:
Jakub Trávník 2025-12-29 11:16:36 +01:00
parent 5cdec5cdaa
commit e4f66de447
3 changed files with 277 additions and 227 deletions

View file

@ -1,6 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { eq, or } from "drizzle-orm"; 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 { db } from "../../db/db";
import { repositoriesTable } from "../../db/schema"; import { repositoriesTable } from "../../db/schema";
import { toMessage } from "../../utils/errors"; import { toMessage } from "../../utils/errors";
@ -74,7 +74,9 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
where: eq(repositoriesTable.shortId, shortId), where: eq(repositoriesTable.shortId, shortId),
}); });
if (existingByShortId) { 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 { } else {
shortId = generateShortId(); shortId = generateShortId();
@ -95,13 +97,13 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
if (repoExists && !config.isExistingRepository) { if (repoExists && !config.isExistingRepository) {
throw new ConflictError( throw new ConflictError(
`A restic repository already exists at this location. ` + `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) { if (!repoExists && config.isExistingRepository) {
throw new InternalServerError( 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.`,
); );
} }

View file

@ -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 ### Repository types
#### Local #### Local

View file

@ -1,225 +1,236 @@
{ {
"volumes": [ "volumes": [
{ {
"name": "local-volume", "name": "local-volume",
"config": { "config": {
"backend": "directory", "backend": "directory",
"path": "/mydata", "path": "/mydata",
"readOnly": true "readOnly": true
} }
}, },
{ {
"name": "nfs-volume", "name": "nfs-volume",
"config": { "config": {
"backend": "nfs", "backend": "nfs",
"server": "nfs.example.com", "server": "nfs.example.com",
"exportPath": "/data", "exportPath": "/data",
"port": 2049, "port": 2049,
"version": "4", "version": "4",
"readOnly": false "readOnly": false
} }
}, },
{ {
"name": "smb-volume", "name": "smb-volume",
"config": { "config": {
"backend": "smb", "backend": "smb",
"server": "smb.example.com", "server": "smb.example.com",
"share": "shared", "share": "shared",
"username": "user", "username": "user",
"password": "${SMB_PASSWORD}", "password": "${SMB_PASSWORD}",
"vers": "3.0", "vers": "3.0",
"domain": "WORKGROUP", "domain": "WORKGROUP",
"port": 445, "port": 445,
"readOnly": false "readOnly": false
} }
}, },
{ {
"name": "webdav-volume", "name": "webdav-volume",
"config": { "config": {
"backend": "webdav", "backend": "webdav",
"server": "webdav.example.com", "server": "webdav.example.com",
"path": "/remote.php/webdav", "path": "/remote.php/webdav",
"username": "user", "username": "user",
"password": "${WEBDAV_PASSWORD}", "password": "${WEBDAV_PASSWORD}",
"port": 80, "port": 80,
"readOnly": false, "readOnly": false,
"ssl": true "ssl": true
} }
} },
], {
"repositories": [ "name": "sftp-volume",
{ "config": {
"name": "local-repo", "backend": "sftp",
"config": { "host": "sftp.example.com",
"backend": "local", "port": 22,
"path": "/var/lib/zerobyte/repositories" "username": "user",
}, "password": "${SFTP_PASSWORD}",
"compressionMode": "auto" "path": "/data",
}, "readOnly": false,
{ "skipHostKeyCheck": true
"name": "s3-repo", }
"config": { }
"backend": "s3", ],
"bucket": "mybucket", "repositories": [
"accessKeyId": "${ACCESS_KEY_ID}", {
"secretAccessKey": "${SECRET_ACCESS_KEY}" "name": "local-repo",
}, "config": {
"compressionMode": "auto" "backend": "local",
}, "path": "/var/lib/zerobyte/repositories"
{ },
"name": "gcs-repo", "compressionMode": "auto"
"config": { },
"backend": "gcs", {
"bucket": "mybucket", "name": "s3-repo",
"projectId": "my-gcp-project", "config": {
"credentialsJson": "${GCS_CREDENTIALS}" "backend": "s3",
} "bucket": "mybucket",
}, "accessKeyId": "${ACCESS_KEY_ID}",
{ "secretAccessKey": "${SECRET_ACCESS_KEY}"
"name": "azure-repo", },
"config": { "compressionMode": "auto"
"backend": "azure", },
"container": "mycontainer", {
"accountName": "myaccount", "name": "gcs-repo",
"accountKey": "${AZURE_KEY}" "config": {
} "backend": "gcs",
}, "bucket": "mybucket",
{ "projectId": "my-gcp-project",
"name": "rclone-repo", "credentialsJson": "${GCS_CREDENTIALS}"
"config": { }
"backend": "rclone", },
"remote": "myremote", {
"path": "backups/zerobyte" "name": "azure-repo",
} "config": {
}, "backend": "azure",
{ "container": "mycontainer",
"name": "webdav-repo", "accountName": "myaccount",
"config": { "accountKey": "${AZURE_KEY}"
"backend": "webdav", }
"server": "webdav.example.com", },
"path": "/remote.php/webdav", {
"username": "user", "name": "rclone-repo",
"password": "${WEBDAV_PASSWORD}", "config": {
"port": 80, "backend": "rclone",
"ssl": true "remote": "myremote",
} "path": "backups/zerobyte"
}, }
{ },
"name": "sftp-repo", {
"config": { "name": "webdav-repo",
"backend": "sftp", "config": {
"host": "sftp.example.com", "backend": "webdav",
"port": 22, "server": "webdav.example.com",
"user": "sftpuser", "path": "/remote.php/webdav",
"privateKey": "${SFTP_PRIVATE_KEY}", "username": "user",
"path": "/backups" "password": "${WEBDAV_PASSWORD}",
} "port": 80,
} "ssl": true
], }
"backupSchedules": [ },
{ {
"name": "local-volume-local-repo", "name": "sftp-repo",
"volume": "local-volume", "config": {
"repository": "local-repo", "backend": "sftp",
"cronExpression": "0 2 * * *", "host": "sftp.example.com",
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 }, "port": 22,
"includePatterns": ["important-folder"], "user": "sftpuser",
"excludePatterns": ["*.tmp", "*.log"], "privateKey": "${SFTP_PRIVATE_KEY}",
"excludeIfPresent": [".nobackup"], "path": "/backups"
"oneFileSystem": false, }
"enabled": true, }
"notifications": ["slack-alerts"], ],
"mirrors": [ "backupSchedules": [
{ "repository": "s3-repo" } {
] "name": "local-volume-local-repo",
} "volume": "local-volume",
], "repository": "local-repo",
"notificationDestinations": [ "cronExpression": "0 2 * * *",
{ "retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
"name": "slack-alerts", "includePatterns": ["important-folder"],
"config": { "excludePatterns": ["*.tmp", "*.log"],
"type": "slack", "excludeIfPresent": [".nobackup"],
"webhookUrl": "${SLACK_WEBHOOK_URL}", "oneFileSystem": false,
"channel": "#backups", "enabled": true,
"username": "zerobyte", "notifications": ["slack-alerts"],
"iconEmoji": ":floppy_disk:" "mirrors": [{ "repository": "s3-repo" }]
} }
}, ],
{ "notificationDestinations": [
"name": "email-admin", {
"config": { "name": "slack-alerts",
"type": "email", "config": {
"smtpHost": "smtp.example.com", "type": "slack",
"smtpPort": 587, "webhookUrl": "${SLACK_WEBHOOK_URL}",
"username": "admin@example.com", "channel": "#backups",
"password": "${EMAIL_PASSWORD}", "username": "zerobyte",
"from": "zerobyte@example.com", "iconEmoji": ":floppy_disk:"
"to": ["admin@example.com"], }
"useTLS": true },
} {
}, "name": "email-admin",
{ "config": {
"name": "discord-backups", "type": "email",
"config": { "smtpHost": "smtp.example.com",
"type": "discord", "smtpPort": 587,
"webhookUrl": "${DISCORD_WEBHOOK_URL}", "username": "admin@example.com",
"username": "zerobyte", "password": "${EMAIL_PASSWORD}",
"avatarUrl": "https://example.com/avatar.png", "from": "zerobyte@example.com",
"threadId": "1234567890" "to": ["admin@example.com"],
} "useTLS": true
}, }
{ },
"name": "gotify-notify", {
"config": { "name": "discord-backups",
"type": "gotify", "config": {
"serverUrl": "https://gotify.example.com", "type": "discord",
"token": "${GOTIFY_TOKEN}", "webhookUrl": "${DISCORD_WEBHOOK_URL}",
"path": "/message", "username": "zerobyte",
"priority": 5 "avatarUrl": "https://example.com/avatar.png",
} "threadId": "1234567890"
}, }
{ },
"name": "ntfy-notify", {
"config": { "name": "gotify-notify",
"type": "ntfy", "config": {
"serverUrl": "https://ntfy.example.com", "type": "gotify",
"topic": "zerobyte-backups", "serverUrl": "https://gotify.example.com",
"priority": "high", "token": "${GOTIFY_TOKEN}",
"username": "ntfyuser", "path": "/message",
"password": "${NTFY_PASSWORD}" "priority": 5
} }
}, },
{ {
"name": "pushover-notify", "name": "ntfy-notify",
"config": { "config": {
"type": "pushover", "type": "ntfy",
"userKey": "${PUSHOVER_USER_KEY}", "serverUrl": "https://ntfy.example.com",
"apiToken": "${PUSHOVER_API_TOKEN}", "topic": "zerobyte-backups",
"devices": "phone,tablet", "priority": "high",
"priority": 1 "username": "ntfyuser",
} "password": "${NTFY_PASSWORD}"
}, }
{ },
"name": "telegram-notify", {
"config": { "name": "pushover-notify",
"type": "telegram", "config": {
"botToken": "${TELEGRAM_BOT_TOKEN}", "type": "pushover",
"chatId": "123456789" "userKey": "${PUSHOVER_USER_KEY}",
} "apiToken": "${PUSHOVER_API_TOKEN}",
}, "devices": "phone,tablet",
{ "priority": 1
"name": "custom-shoutrrr", }
"config": { },
"type": "custom", {
"shoutrrrUrl": "${SHOUTRRR_URL}" "name": "telegram-notify",
} "config": {
} "type": "telegram",
], "botToken": "${TELEGRAM_BOT_TOKEN}",
"recoveryKey": "${RECOVERY_KEY}", "chatId": "123456789"
"users": [ }
{ },
"username": "admin", {
"password": "${ADMIN_PASSWORD}" "name": "custom-shoutrrr",
} "config": {
] "type": "custom",
"shoutrrrUrl": "${SHOUTRRR_URL}"
}
}
],
"recoveryKey": "${RECOVERY_KEY}",
"users": [
{
"username": "admin",
"password": "${ADMIN_PASSWORD}"
}
]
} }