Merge branch 'main' into config-export-feature
This commit is contained in:
commit
e15fff2c23
54 changed files with 9594 additions and 7027 deletions
|
|
@ -1,9 +1,7 @@
|
|||
name: Checks
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
|
@ -11,8 +9,11 @@ on:
|
|||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
checks:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
@ -24,6 +25,10 @@ jobs:
|
|||
- name: Install dependencies
|
||||
uses: "./.github/actions/install-dependencies"
|
||||
|
||||
- name: Run lint
|
||||
shell: bash
|
||||
run: bun run lint:ci
|
||||
|
||||
- name: Run type checks
|
||||
shell: bash
|
||||
run: bun run tsc
|
||||
5
.github/workflows/release.yml
vendored
5
.github/workflows/release.yml
vendored
|
|
@ -31,9 +31,12 @@ jobs:
|
|||
echo "release_type=release" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
checks:
|
||||
uses: ./.github/workflows/checks.yml
|
||||
|
||||
build-images:
|
||||
timeout-minutes: 15
|
||||
needs: [determine-release-type]
|
||||
needs: [determine-release-type, checks]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
|
|||
|
|
@ -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 <20> service <20> 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 <20> service <20> 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
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ 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 tini
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "-s", "--"]
|
||||
|
||||
|
||||
# ------------------------------
|
||||
|
|
@ -64,7 +66,7 @@ CMD ["bun", "run", "dev"]
|
|||
# ------------------------------
|
||||
# PRODUCTION
|
||||
# ------------------------------
|
||||
FROM oven/bun:${BUN_VERSION} AS builder
|
||||
FROM oven/bun:${BUN_VERSION}-alpine AS builder
|
||||
|
||||
ARG APP_VERSION=dev
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1390,6 +1477,7 @@ export type ListBackupSchedulesResponses = {
|
|||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
shortId: string;
|
||||
updatedAt: number;
|
||||
volume: {
|
||||
autoRemount: boolean;
|
||||
|
|
@ -1409,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;
|
||||
|
|
@ -1436,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;
|
||||
|
|
@ -1500,6 +1599,7 @@ export type CreateBackupScheduleResponses = {
|
|||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
shortId: string;
|
||||
updatedAt: number;
|
||||
volumeId: number;
|
||||
};
|
||||
|
|
@ -1614,8 +1714,10 @@ export type GetBackupScheduleResponses = {
|
|||
privateKey: string;
|
||||
user: string;
|
||||
port?: number;
|
||||
skipHostKeyCheck?: boolean;
|
||||
customPassword?: string;
|
||||
isExistingRepository?: boolean;
|
||||
knownHosts?: string;
|
||||
};
|
||||
createdAt: number;
|
||||
id: string;
|
||||
|
|
@ -1637,6 +1739,7 @@ export type GetBackupScheduleResponses = {
|
|||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
shortId: string;
|
||||
updatedAt: number;
|
||||
volume: {
|
||||
autoRemount: boolean;
|
||||
|
|
@ -1656,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;
|
||||
|
|
@ -1683,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;
|
||||
|
|
@ -1748,6 +1862,7 @@ export type UpdateBackupScheduleResponses = {
|
|||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
shortId: string;
|
||||
updatedAt: number;
|
||||
volumeId: number;
|
||||
};
|
||||
|
|
@ -1842,8 +1957,10 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||
privateKey: string;
|
||||
user: string;
|
||||
port?: number;
|
||||
skipHostKeyCheck?: boolean;
|
||||
customPassword?: string;
|
||||
isExistingRepository?: boolean;
|
||||
knownHosts?: string;
|
||||
};
|
||||
createdAt: number;
|
||||
id: string;
|
||||
|
|
@ -1865,6 +1982,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||
keepWithinDuration?: string;
|
||||
keepYearly?: number;
|
||||
} | null;
|
||||
shortId: string;
|
||||
updatedAt: number;
|
||||
volume: {
|
||||
autoRemount: boolean;
|
||||
|
|
@ -1884,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;
|
||||
|
|
@ -1911,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;
|
||||
|
|
@ -2238,8 +2367,10 @@ export type GetScheduleMirrorsResponses = {
|
|||
privateKey: string;
|
||||
user: string;
|
||||
port?: number;
|
||||
skipHostKeyCheck?: boolean;
|
||||
customPassword?: string;
|
||||
isExistingRepository?: boolean;
|
||||
knownHosts?: string;
|
||||
};
|
||||
createdAt: number;
|
||||
id: string;
|
||||
|
|
@ -2342,8 +2473,10 @@ export type UpdateScheduleMirrorsResponses = {
|
|||
privateKey: string;
|
||||
user: string;
|
||||
port?: number;
|
||||
skipHostKeyCheck?: boolean;
|
||||
customPassword?: string;
|
||||
isExistingRepository?: boolean;
|
||||
knownHosts?: string;
|
||||
};
|
||||
createdAt: number;
|
||||
id: string;
|
||||
|
|
|
|||
|
|
@ -85,8 +85,7 @@ export const SnapshotsTable = ({ snapshots, repositoryId, backups }: Props) => {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{snapshots.map((snapshot) => {
|
||||
const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag));
|
||||
const backup = backups.find((b) => backupIds.includes(b.id));
|
||||
const backup = backups.find((b) => snapshot.tags.includes(b.shortId));
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<span className={`flex items-center gap-2 rounded-md px-2 py-1`}>
|
||||
|
|
|
|||
40
app/client/modules/auth/components/reset-password-dialog.tsx
Normal file
40
app/client/modules/auth/components/reset-password-dialog.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { toast } from "sonner";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
|
||||
import { copyToClipboard } from "~/utils/clipboard";
|
||||
|
||||
const RESET_PASSWORD_COMMAND = "docker exec -it zerobyte bun run cli reset-password";
|
||||
|
||||
type ResetPasswordDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export const ResetPasswordDialog = ({ open, onOpenChange }: ResetPasswordDialogProps) => {
|
||||
const handleCopy = async () => {
|
||||
await copyToClipboard(RESET_PASSWORD_COMMAND);
|
||||
toast.success("Command copied to clipboard");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset your password</DialogTitle>
|
||||
<DialogDescription>
|
||||
To reset your password, run the following command on the server where Zerobyte is installed.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">{RESET_PASSWORD_COMMAND}</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This command will start an interactive session where you can enter a new password for your account.
|
||||
</p>
|
||||
<Button onClick={handleCopy} variant="outline" className="w-full">
|
||||
Copy Command
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,13 +7,12 @@ import { useNavigate } from "react-router";
|
|||
import { toast } from "sonner";
|
||||
import { AuthLayout } from "~/client/components/auth-layout";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/client/components/ui/dialog";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { authMiddleware } from "~/middleware/auth";
|
||||
import type { Route } from "./+types/login";
|
||||
import { loginMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { copyToClipboard } from "~/utils/clipboard";
|
||||
import { ResetPasswordDialog } from "../components/reset-password-dialog";
|
||||
|
||||
export const clientMiddleware = [authMiddleware];
|
||||
|
||||
|
|
@ -119,34 +118,3 @@ export default function LoginPage() {
|
|||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const RESET_PASSWORD_COMMAND = "docker exec -it zerobyte bun run cli reset-password";
|
||||
|
||||
function ResetPasswordDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
||||
const handleCopy = async () => {
|
||||
await copyToClipboard(RESET_PASSWORD_COMMAND);
|
||||
toast.success("Command copied to clipboard");
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reset your password</DialogTitle>
|
||||
<DialogDescription>
|
||||
To reset your password, run the following command on the server where Zerobyte is installed.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">{RESET_PASSWORD_COMMAND}</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This command will start an interactive session where you can enter a new password for your account.
|
||||
</p>
|
||||
<Button onClick={handleCopy} variant="outline" className="w-full">
|
||||
Copy Command
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||
isLoading,
|
||||
failureReason,
|
||||
} = useQuery({
|
||||
...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.id.toString() } }),
|
||||
...listSnapshotsOptions({ path: { id: schedule.repository.id }, query: { backupId: schedule.shortId } }),
|
||||
});
|
||||
|
||||
const updateSchedule = useMutation({
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="backups/ironmount" {...field} />
|
||||
<Input placeholder="backups/zerobyte" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Repository path on the SFTP server. </FormDescription>
|
||||
<FormMessage />
|
||||
|
|
@ -96,6 +97,47 @@ export const SftpRepositoryForm = ({ form }: Props) => {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skipHostKeyCheck"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||
<FormDescription>
|
||||
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!form.watch("skipHostKeyCheck") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="knownHosts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Known Hosts</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||
className="font-mono text-xs"
|
||||
rows={3}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The contents of the <code>known_hosts</code> file for this server.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { cn } from "~/client/lib/utils";
|
|||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||
import { Loader2, Stethoscope, Trash2, X } from "lucide-react";
|
||||
import { Loader2, Stethoscope, Trash2 } from "lucide-react";
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: (match: Route.MetaArgs) => [
|
||||
|
|
@ -185,13 +185,13 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the repository <strong>{data.name}</strong>? This will not remove the
|
||||
actual data from the backend storage, only the repository configuration will be deleted.
|
||||
<br />
|
||||
<br />
|
||||
All backup schedules associated with this repository will also be removed.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<div className="flex gap-3 justify-end">
|
||||
<AlertDialogCancel>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
|
|
|
|||
|
|
@ -116,8 +116,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps
|
|||
{(value) => {
|
||||
if (!value.data) return null;
|
||||
|
||||
const backupIds = value.data.tags.map(Number).filter((tag) => !Number.isNaN(tag));
|
||||
const backupSchedule = schedules.data?.find((s) => backupIds.includes(s.id));
|
||||
const backupSchedule = schedules.data?.find((s) => value.data.tags.includes(s.shortId));
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||
);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
|
||||
const isImportedLocal = repository.type === "local" && repository.config.isExistingRepository;
|
||||
|
||||
const updateMutation = useMutation({
|
||||
...updateRepositoryMutation(),
|
||||
onSuccess: () => {
|
||||
|
|
@ -77,13 +75,8 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||
placeholder="Repository name"
|
||||
maxLength={32}
|
||||
minLength={2}
|
||||
disabled={isImportedLocal}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isImportedLocal
|
||||
? "Imported local repositories cannot be renamed."
|
||||
: "Unique identifier for the repository."}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">Unique identifier for the repository.</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="compressionMode">Compression mode</Label>
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
|||
if (!searchQuery) return true;
|
||||
const searchLower = searchQuery.toLowerCase();
|
||||
|
||||
const backupIds = snapshot.tags.map(Number).filter((tag) => !Number.isNaN(tag));
|
||||
const backup = schedules.data?.find((b) => backupIds.includes(b.id));
|
||||
const backup = schedules.data?.find((b) => snapshot.tags.includes(b.shortId));
|
||||
|
||||
return (
|
||||
snapshot.short_id.toLowerCase().includes(searchLower) ||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { volumeConfigSchemaBase } from "~/schemas/volumes";
|
|||
import { testConnectionMutation } from "../../../api-client/@tanstack/react-query.gen";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../components/ui/tooltip";
|
||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||
import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm } from "./volume-forms";
|
||||
import { DirectoryForm, NFSForm, SMBForm, WebDAVForm, RcloneForm, SFTPForm } from "./volume-forms";
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
|
|
@ -46,6 +46,7 @@ const defaultValuesForType = {
|
|||
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
|
||||
webdav: { backend: "webdav" as const, port: 80, ssl: false, path: "/webdav" },
|
||||
rclone: { backend: "rclone" as const, path: "/" },
|
||||
sftp: { backend: "sftp" as const, port: 22, path: "/", skipHostKeyCheck: false },
|
||||
};
|
||||
|
||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
||||
|
|
@ -96,7 +97,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||
const handleTestConnection = async () => {
|
||||
const formValues = getValues();
|
||||
|
||||
if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
|
||||
if (
|
||||
formValues.backend === "nfs" ||
|
||||
formValues.backend === "smb" ||
|
||||
formValues.backend === "webdav" ||
|
||||
formValues.backend === "sftp"
|
||||
) {
|
||||
testBackendConnection.mutate({
|
||||
body: { config: formValues },
|
||||
});
|
||||
|
|
@ -177,6 +183,18 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||
<p>Remote mounts require SYS_ADMIN capability</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<SelectItem disabled={!capabilities.sysAdmin} value="sftp">
|
||||
SFTP
|
||||
</SelectItem>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className={cn({ hidden: capabilities.sysAdmin })}>
|
||||
<p>Remote mounts require SYS_ADMIN capability</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
|
|
@ -204,6 +222,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||
{watchedBackend === "webdav" && <WebDAVForm form={form} />}
|
||||
{watchedBackend === "smb" && <SMBForm form={form} />}
|
||||
{watchedBackend === "rclone" && <RcloneForm form={form} />}
|
||||
{watchedBackend === "sftp" && <SFTPForm form={form} />}
|
||||
{watchedBackend && watchedBackend !== "directory" && watchedBackend !== "rclone" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export { NFSForm } from "./nfs-form";
|
|||
export { SMBForm } from "./smb-form";
|
||||
export { WebDAVForm } from "./webdav-form";
|
||||
export { RcloneForm } from "./rclone-form";
|
||||
export { SFTPForm } from "./sftp-form";
|
||||
|
|
|
|||
161
app/client/modules/volumes/components/volume-forms/sftp-form.tsx
Normal file
161
app/client/modules/volumes/components/volume-forms/sftp-form.tsx
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import type { UseFormReturn } from "react-hook-form";
|
||||
import type { FormValues } from "../create-volume-form";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "../../../../components/ui/form";
|
||||
import { Input } from "../../../../components/ui/input";
|
||||
import { SecretInput } from "../../../../components/ui/secret-input";
|
||||
import { Textarea } from "../../../../components/ui/textarea";
|
||||
import { Switch } from "../../../../components/ui/switch";
|
||||
|
||||
type Props = {
|
||||
form: UseFormReturn<FormValues>;
|
||||
};
|
||||
|
||||
export const SFTPForm = ({ form }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>SFTP 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}
|
||||
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>SFTP server port (default: 22).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Username for SFTP authentication.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<SecretInput placeholder="••••••••" value={field.value ?? ""} onChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormDescription>Password for SFTP authentication (optional if using private key).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="privateKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Private Key (Optional)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
className="font-mono text-xs"
|
||||
rows={5}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>SSH private key for authentication (optional if using password).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="/backups" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Path to the directory on the SFTP server.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="skipHostKeyCheck"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Skip Host Key Verification</FormLabel>
|
||||
<FormDescription>
|
||||
Disable SSH host key checking. Useful for servers with dynamic IPs or self-signed keys.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{!form.watch("skipHostKeyCheck") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="knownHosts"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Known Hosts</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ..."
|
||||
className="font-mono text-xs"
|
||||
rows={3}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The contents of the <code>known_hosts</code> file for this server.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
10
app/drizzle/0026_migrate-local-repo-paths.sql
Normal file
10
app/drizzle/0026_migrate-local-repo-paths.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- Migrate local imported repository paths to include the repository name
|
||||
-- Previously, the path and name were concatenated at runtime. Now the path should be the full path.
|
||||
|
||||
UPDATE `repositories_table`
|
||||
SET `config` = json_set(`config`, '$.path', rtrim(json_extract(`config`, '$.path'), '/') || '/' || json_extract(`config`, '$.name')),
|
||||
`updated_at` = (unixepoch() * 1000)
|
||||
WHERE `type` = 'local'
|
||||
AND json_extract(`config`, '$.isExistingRepository') = true
|
||||
AND json_extract(`config`, '$.path') IS NOT NULL
|
||||
AND json_extract(`config`, '$.name') IS NOT NULL;
|
||||
3
app/drizzle/0027_careful_cammi.sql
Normal file
3
app/drizzle/0027_careful_cammi.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE `backup_schedules_table` ADD `short_id` text;--> statement-breakpoint
|
||||
UPDATE `backup_schedules_table` SET `short_id` = lower(hex(randomblob(4))) WHERE `short_id` IS NULL;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`);
|
||||
31
app/drizzle/0028_third_amazoness.sql
Normal file
31
app/drizzle/0028_third_amazoness.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_backup_schedules_table` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`short_id` text NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`volume_id` integer NOT NULL,
|
||||
`repository_id` text NOT NULL,
|
||||
`enabled` integer DEFAULT true NOT NULL,
|
||||
`cron_expression` text NOT NULL,
|
||||
`retention_policy` text,
|
||||
`exclude_patterns` text DEFAULT '[]',
|
||||
`exclude_if_present` text DEFAULT '[]',
|
||||
`include_patterns` text DEFAULT '[]',
|
||||
`last_backup_at` integer,
|
||||
`last_backup_status` text,
|
||||
`last_backup_error` text,
|
||||
`next_backup_at` integer,
|
||||
`one_file_system` integer DEFAULT false NOT NULL,
|
||||
`sort_order` integer DEFAULT 0 NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
||||
FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_backup_schedules_table`("id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at") SELECT "id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at" FROM `backup_schedules_table`;--> statement-breakpoint
|
||||
DROP TABLE `backup_schedules_table`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`);
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
786
app/drizzle/meta/0026_snapshot.json
Normal file
786
app/drizzle/meta/0026_snapshot.json
Normal file
|
|
@ -0,0 +1,786 @@
|
|||
{
|
||||
"id": "19421265-4e3a-46b8-9ca1-a01d1e293dbc",
|
||||
"prevId": "ca46a423-51ca-45ae-9470-f82172a67bd3",
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_mirrors_table": {
|
||||
"name": "backup_schedule_mirrors_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_copy_at": {
|
||||
"name": "last_copy_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_status": {
|
||||
"name": "last_copy_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_error": {
|
||||
"name": "last_copy_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
|
||||
"columns": ["schedule_id", "repository_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"tableTo": "repositories_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_notifications_table": {
|
||||
"name": "backup_schedule_notifications_table",
|
||||
"columns": {
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destination_id": {
|
||||
"name": "destination_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notify_on_start": {
|
||||
"name": "notify_on_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_success": {
|
||||
"name": "notify_on_success",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_warning": {
|
||||
"name": "notify_on_warning",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"notify_on_failure": {
|
||||
"name": "notify_on_failure",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"columnsFrom": ["destination_id"],
|
||||
"tableTo": "notification_destinations_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||
"columns": ["schedule_id", "destination_id"],
|
||||
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedules_table": {
|
||||
"name": "backup_schedules_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"volume_id": {
|
||||
"name": "volume_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"retention_policy": {
|
||||
"name": "retention_policy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exclude_patterns": {
|
||||
"name": "exclude_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"exclude_if_present": {
|
||||
"name": "exclude_if_present",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"include_patterns": {
|
||||
"name": "include_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"last_backup_at": {
|
||||
"name": "last_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_status": {
|
||||
"name": "last_backup_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_error": {
|
||||
"name": "last_backup_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_backup_at": {
|
||||
"name": "next_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"one_file_system": {
|
||||
"name": "one_file_system",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedules_table_name_unique": {
|
||||
"name": "backup_schedules_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedules_table_volume_id_volumes_table_id_fk": {
|
||||
"name": "backup_schedules_table_volume_id_volumes_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"columnsFrom": ["volume_id"],
|
||||
"tableTo": "volumes_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
},
|
||||
"backup_schedules_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"tableTo": "repositories_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_destinations_table": {
|
||||
"name": "notification_destinations_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"notification_destinations_table_name_unique": {
|
||||
"name": "notification_destinations_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repositories_table": {
|
||||
"name": "repositories_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"compression_mode": {
|
||||
"name": "compression_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'auto'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'unknown'"
|
||||
},
|
||||
"last_checked": {
|
||||
"name": "last_checked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"repositories_table_short_id_unique": {
|
||||
"name": "repositories_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions_table": {
|
||||
"name": "sessions_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_table_user_id_users_table_id_fk": {
|
||||
"name": "sessions_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "sessions_table",
|
||||
"columnsFrom": ["user_id"],
|
||||
"tableTo": "users_table",
|
||||
"columnsTo": ["id"],
|
||||
"onUpdate": "no action",
|
||||
"onDelete": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_downloaded_restic_password": {
|
||||
"name": "has_downloaded_restic_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'unmounted'"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_health_check": {
|
||||
"name": "last_health_check",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_remount": {
|
||||
"name": "auto_remount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_short_id_unique": {
|
||||
"name": "volumes_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
798
app/drizzle/meta/0027_snapshot.json
Normal file
798
app/drizzle/meta/0027_snapshot.json
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "94ebc8c3-f11f-48cb-9814-d8d550422d26",
|
||||
"prevId": "19421265-4e3a-46b8-9ca1-a01d1e293dbc",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_mirrors_table": {
|
||||
"name": "backup_schedule_mirrors_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_copy_at": {
|
||||
"name": "last_copy_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_status": {
|
||||
"name": "last_copy_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_error": {
|
||||
"name": "last_copy_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
|
||||
"columns": ["schedule_id", "repository_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_notifications_table": {
|
||||
"name": "backup_schedule_notifications_table",
|
||||
"columns": {
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destination_id": {
|
||||
"name": "destination_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notify_on_start": {
|
||||
"name": "notify_on_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_success": {
|
||||
"name": "notify_on_success",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_warning": {
|
||||
"name": "notify_on_warning",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"notify_on_failure": {
|
||||
"name": "notify_on_failure",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "notification_destinations_table",
|
||||
"columnsFrom": ["destination_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||
"columns": ["schedule_id", "destination_id"],
|
||||
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedules_table": {
|
||||
"name": "backup_schedules_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"volume_id": {
|
||||
"name": "volume_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"retention_policy": {
|
||||
"name": "retention_policy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exclude_patterns": {
|
||||
"name": "exclude_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"exclude_if_present": {
|
||||
"name": "exclude_if_present",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"include_patterns": {
|
||||
"name": "include_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"last_backup_at": {
|
||||
"name": "last_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_status": {
|
||||
"name": "last_backup_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_error": {
|
||||
"name": "last_backup_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_backup_at": {
|
||||
"name": "next_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"one_file_system": {
|
||||
"name": "one_file_system",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedules_table_short_id_unique": {
|
||||
"name": "backup_schedules_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"backup_schedules_table_name_unique": {
|
||||
"name": "backup_schedules_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedules_table_volume_id_volumes_table_id_fk": {
|
||||
"name": "backup_schedules_table_volume_id_volumes_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "volumes_table",
|
||||
"columnsFrom": ["volume_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedules_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_destinations_table": {
|
||||
"name": "notification_destinations_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"notification_destinations_table_name_unique": {
|
||||
"name": "notification_destinations_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repositories_table": {
|
||||
"name": "repositories_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"compression_mode": {
|
||||
"name": "compression_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'auto'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'unknown'"
|
||||
},
|
||||
"last_checked": {
|
||||
"name": "last_checked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"repositories_table_short_id_unique": {
|
||||
"name": "repositories_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions_table": {
|
||||
"name": "sessions_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_table_user_id_users_table_id_fk": {
|
||||
"name": "sessions_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "sessions_table",
|
||||
"tableTo": "users_table",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_downloaded_restic_password": {
|
||||
"name": "has_downloaded_restic_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'unmounted'"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_health_check": {
|
||||
"name": "last_health_check",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_remount": {
|
||||
"name": "auto_remount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_short_id_unique": {
|
||||
"name": "volumes_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
798
app/drizzle/meta/0028_snapshot.json
Normal file
798
app/drizzle/meta/0028_snapshot.json
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "2837bed4-34fb-4d16-b331-7b6d483979bc",
|
||||
"prevId": "94ebc8c3-f11f-48cb-9814-d8d550422d26",
|
||||
"tables": {
|
||||
"app_metadata": {
|
||||
"name": "app_metadata",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_mirrors_table": {
|
||||
"name": "backup_schedule_mirrors_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"last_copy_at": {
|
||||
"name": "last_copy_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_status": {
|
||||
"name": "last_copy_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_copy_error": {
|
||||
"name": "last_copy_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedule_mirrors_table_schedule_id_repository_id_unique": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
|
||||
"columns": ["schedule_id", "repository_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedule_mirrors_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedule_notifications_table": {
|
||||
"name": "backup_schedule_notifications_table",
|
||||
"columns": {
|
||||
"schedule_id": {
|
||||
"name": "schedule_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"destination_id": {
|
||||
"name": "destination_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"notify_on_start": {
|
||||
"name": "notify_on_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_success": {
|
||||
"name": "notify_on_success",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"notify_on_warning": {
|
||||
"name": "notify_on_warning",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"notify_on_failure": {
|
||||
"name": "notify_on_failure",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "backup_schedules_table",
|
||||
"columnsFrom": ["schedule_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": {
|
||||
"name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
|
||||
"tableFrom": "backup_schedule_notifications_table",
|
||||
"tableTo": "notification_destinations_table",
|
||||
"columnsFrom": ["destination_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"backup_schedule_notifications_table_schedule_id_destination_id_pk": {
|
||||
"columns": ["schedule_id", "destination_id"],
|
||||
"name": "backup_schedule_notifications_table_schedule_id_destination_id_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"backup_schedules_table": {
|
||||
"name": "backup_schedules_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"volume_id": {
|
||||
"name": "volume_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"repository_id": {
|
||||
"name": "repository_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"cron_expression": {
|
||||
"name": "cron_expression",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"retention_policy": {
|
||||
"name": "retention_policy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exclude_patterns": {
|
||||
"name": "exclude_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"exclude_if_present": {
|
||||
"name": "exclude_if_present",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"include_patterns": {
|
||||
"name": "include_patterns",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'[]'"
|
||||
},
|
||||
"last_backup_at": {
|
||||
"name": "last_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_status": {
|
||||
"name": "last_backup_status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_backup_error": {
|
||||
"name": "last_backup_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"next_backup_at": {
|
||||
"name": "next_backup_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"one_file_system": {
|
||||
"name": "one_file_system",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"sort_order": {
|
||||
"name": "sort_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"backup_schedules_table_short_id_unique": {
|
||||
"name": "backup_schedules_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"backup_schedules_table_name_unique": {
|
||||
"name": "backup_schedules_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"backup_schedules_table_volume_id_volumes_table_id_fk": {
|
||||
"name": "backup_schedules_table_volume_id_volumes_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "volumes_table",
|
||||
"columnsFrom": ["volume_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"backup_schedules_table_repository_id_repositories_table_id_fk": {
|
||||
"name": "backup_schedules_table_repository_id_repositories_table_id_fk",
|
||||
"tableFrom": "backup_schedules_table",
|
||||
"tableTo": "repositories_table",
|
||||
"columnsFrom": ["repository_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"notification_destinations_table": {
|
||||
"name": "notification_destinations_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"notification_destinations_table_name_unique": {
|
||||
"name": "notification_destinations_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"repositories_table": {
|
||||
"name": "repositories_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"compression_mode": {
|
||||
"name": "compression_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'auto'"
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'unknown'"
|
||||
},
|
||||
"last_checked": {
|
||||
"name": "last_checked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"repositories_table_short_id_unique": {
|
||||
"name": "repositories_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions_table": {
|
||||
"name": "sessions_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_table_user_id_users_table_id_fk": {
|
||||
"name": "sessions_table_user_id_users_table_id_fk",
|
||||
"tableFrom": "sessions_table",
|
||||
"tableTo": "users_table",
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users_table": {
|
||||
"name": "users_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"has_downloaded_restic_password": {
|
||||
"name": "has_downloaded_restic_password",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_table_username_unique": {
|
||||
"name": "users_table_username_unique",
|
||||
"columns": ["username"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"volumes_table": {
|
||||
"name": "volumes_table",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"short_id": {
|
||||
"name": "short_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'unmounted'"
|
||||
},
|
||||
"last_error": {
|
||||
"name": "last_error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"last_health_check": {
|
||||
"name": "last_health_check",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(unixepoch() * 1000)"
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"auto_remount": {
|
||||
"name": "auto_remount",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"volumes_table_short_id_unique": {
|
||||
"name": "volumes_table_short_id_unique",
|
||||
"columns": ["short_id"],
|
||||
"isUnique": true
|
||||
},
|
||||
"volumes_table_name_unique": {
|
||||
"name": "volumes_table_name_unique",
|
||||
"columns": ["name"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,188 +1,209 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1755765658194,
|
||||
"tag": "0000_known_madelyne_pryor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1755775437391,
|
||||
"tag": "0001_far_frank_castle",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1756930554198,
|
||||
"tag": "0002_cheerful_randall",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1758653407064,
|
||||
"tag": "0003_mature_hellcat",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1758961535488,
|
||||
"tag": "0004_wealthy_tomas",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1759416698274,
|
||||
"tag": "0005_simple_alice",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1760734377440,
|
||||
"tag": "0006_secret_micromacro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1761224911352,
|
||||
"tag": "0007_watery_sersi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1761414054481,
|
||||
"tag": "0008_silent_lady_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1762095226041,
|
||||
"tag": "0009_little_adam_warlock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "6",
|
||||
"when": 1762610065889,
|
||||
"tag": "0010_perfect_proemial_gods",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "6",
|
||||
"when": 1763644043601,
|
||||
"tag": "0011_familiar_stone_men",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1764100562084,
|
||||
"tag": "0012_add_short_ids",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1764182159797,
|
||||
"tag": "0013_elite_sprite",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "6",
|
||||
"when": 1764182405089,
|
||||
"tag": "0014_wild_echo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "6",
|
||||
"when": 1764182465287,
|
||||
"tag": "0015_jazzy_sersi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "6",
|
||||
"when": 1764194697035,
|
||||
"tag": "0016_fix-timestamps-to-ms",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "6",
|
||||
"when": 1764357897219,
|
||||
"tag": "0017_fix-compression-modes",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1764794371040,
|
||||
"tag": "0018_breezy_invaders",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "6",
|
||||
"when": 1764839917446,
|
||||
"tag": "0019_secret_nomad",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "6",
|
||||
"when": 1764847918249,
|
||||
"tag": "0020_even_dexter_bennett",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "6",
|
||||
"when": 1765307881092,
|
||||
"tag": "0021_steady_viper",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "6",
|
||||
"when": 1765794552191,
|
||||
"tag": "0022_woozy_shen",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "6",
|
||||
"when": 1766320570509,
|
||||
"tag": "0023_special_thor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "6",
|
||||
"when": 1766325504548,
|
||||
"tag": "0024_schedules-one-fs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "6",
|
||||
"when": 1766431021321,
|
||||
"tag": "0025_remarkable_pete_wisdom",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1755765658194,
|
||||
"tag": "0000_known_madelyne_pryor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1755775437391,
|
||||
"tag": "0001_far_frank_castle",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1756930554198,
|
||||
"tag": "0002_cheerful_randall",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1758653407064,
|
||||
"tag": "0003_mature_hellcat",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1758961535488,
|
||||
"tag": "0004_wealthy_tomas",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1759416698274,
|
||||
"tag": "0005_simple_alice",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1760734377440,
|
||||
"tag": "0006_secret_micromacro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "6",
|
||||
"when": 1761224911352,
|
||||
"tag": "0007_watery_sersi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1761414054481,
|
||||
"tag": "0008_silent_lady_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 9,
|
||||
"version": "6",
|
||||
"when": 1762095226041,
|
||||
"tag": "0009_little_adam_warlock",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 10,
|
||||
"version": "6",
|
||||
"when": 1762610065889,
|
||||
"tag": "0010_perfect_proemial_gods",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 11,
|
||||
"version": "6",
|
||||
"when": 1763644043601,
|
||||
"tag": "0011_familiar_stone_men",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "6",
|
||||
"when": 1764100562084,
|
||||
"tag": "0012_add_short_ids",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 13,
|
||||
"version": "6",
|
||||
"when": 1764182159797,
|
||||
"tag": "0013_elite_sprite",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "6",
|
||||
"when": 1764182405089,
|
||||
"tag": "0014_wild_echo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "6",
|
||||
"when": 1764182465287,
|
||||
"tag": "0015_jazzy_sersi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "6",
|
||||
"when": 1764194697035,
|
||||
"tag": "0016_fix-timestamps-to-ms",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "6",
|
||||
"when": 1764357897219,
|
||||
"tag": "0017_fix-compression-modes",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "6",
|
||||
"when": 1764794371040,
|
||||
"tag": "0018_breezy_invaders",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 19,
|
||||
"version": "6",
|
||||
"when": 1764839917446,
|
||||
"tag": "0019_secret_nomad",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "6",
|
||||
"when": 1764847918249,
|
||||
"tag": "0020_even_dexter_bennett",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "6",
|
||||
"when": 1765307881092,
|
||||
"tag": "0021_steady_viper",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 22,
|
||||
"version": "6",
|
||||
"when": 1765794552191,
|
||||
"tag": "0022_woozy_shen",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "6",
|
||||
"when": 1766320570509,
|
||||
"tag": "0023_special_thor",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 24,
|
||||
"version": "6",
|
||||
"when": 1766325504548,
|
||||
"tag": "0024_schedules-one-fs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "6",
|
||||
"when": 1766431021321,
|
||||
"tag": "0025_remarkable_pete_wisdom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 26,
|
||||
"version": "6",
|
||||
"when": 1766765013108,
|
||||
"tag": "0026_migrate-local-repo-paths",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "6",
|
||||
"when": 1766778073418,
|
||||
"tag": "0027_careful_cammi",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "6",
|
||||
"when": 1766778162985,
|
||||
"tag": "0028_third_amazoness",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ export const sftpRepositoryConfigSchema = type({
|
|||
user: "string",
|
||||
path: "string",
|
||||
privateKey: "string",
|
||||
skipHostKeyCheck: "boolean = true",
|
||||
knownHosts: "string?",
|
||||
}).and(baseRepositoryConfigSchema);
|
||||
|
||||
export const repositoryConfigSchemaBase = s3RepositoryConfigSchema
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const BACKEND_TYPES = {
|
|||
directory: "directory",
|
||||
webdav: "webdav",
|
||||
rclone: "rclone",
|
||||
sftp: "sftp",
|
||||
} as const;
|
||||
|
||||
export type BackendType = keyof typeof BACKEND_TYPES;
|
||||
|
|
@ -55,11 +56,25 @@ export const rcloneConfigSchema = type({
|
|||
readOnly: "boolean?",
|
||||
});
|
||||
|
||||
export const sftpConfigSchema = type({
|
||||
backend: "'sftp'",
|
||||
host: "string",
|
||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(22),
|
||||
username: "string",
|
||||
password: "string?",
|
||||
privateKey: "string?",
|
||||
path: "string",
|
||||
readOnly: "boolean?",
|
||||
skipHostKeyCheck: "boolean = true",
|
||||
knownHosts: "string?",
|
||||
});
|
||||
|
||||
export const volumeConfigSchemaBase = nfsConfigSchema
|
||||
.or(smbConfigSchema)
|
||||
.or(webdavConfigSchema)
|
||||
.or(directoryConfigSchema)
|
||||
.or(rcloneConfigSchema);
|
||||
.or(rcloneConfigSchema)
|
||||
.or(sftpConfigSchema);
|
||||
|
||||
export const volumeConfigSchema = volumeConfigSchemaBase.onUndeclaredKey("delete");
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,17 @@ const envSchema = type({
|
|||
NODE_ENV: type.enumerated("development", "production", "test").default("production"),
|
||||
SERVER_IP: 'string = "localhost"',
|
||||
SERVER_IDLE_TIMEOUT: 'string.integer.parse = "60"',
|
||||
RESTIC_HOSTNAME: "string = 'zerobyte'",
|
||||
PORT: 'string.integer.parse = "4096"',
|
||||
MIGRATIONS_PATH: "string?",
|
||||
}).pipe((s) => ({
|
||||
__prod__: s.NODE_ENV === "production",
|
||||
environment: s.NODE_ENV,
|
||||
serverIp: s.SERVER_IP,
|
||||
serverIdleTimeout: s.SERVER_IDLE_TIMEOUT,
|
||||
resticHostname: s.RESTIC_HOSTNAME,
|
||||
port: s.PORT,
|
||||
migrationsPath: s.MIGRATIONS_PATH,
|
||||
}));
|
||||
|
||||
const parseConfig = (env: unknown) => {
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ export const RESTIC_PASS_FILE = "/var/lib/zerobyte/data/restic.pass";
|
|||
|
||||
export const DEFAULT_EXCLUDES = [DATABASE_URL, RESTIC_PASS_FILE, REPOSITORY_BASE];
|
||||
|
||||
export const REQUIRED_MIGRATIONS = ["v0.14.0"];
|
||||
export const REQUIRED_MIGRATIONS = ["v0.21.0"];
|
||||
|
|
|
|||
|
|
@ -14,9 +14,13 @@ const sqlite = new Database(DATABASE_URL);
|
|||
export const db = drizzle({ client: sqlite, schema });
|
||||
|
||||
export const runDbMigrations = () => {
|
||||
let migrationsFolder = path.join("/app", "assets", "migrations");
|
||||
let migrationsFolder: string;
|
||||
|
||||
if (!config.__prod__) {
|
||||
if (config.migrationsPath) {
|
||||
migrationsFolder = config.migrationsPath;
|
||||
} else if (config.__prod__) {
|
||||
migrationsFolder = path.join("/app", "assets", "migrations");
|
||||
} else {
|
||||
migrationsFolder = path.join("/app", "app", "drizzle");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
|
|||
*/
|
||||
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
||||
id: int().primaryKey({ autoIncrement: true }),
|
||||
shortId: text("short_id").notNull().unique(),
|
||||
name: text().notNull().unique(),
|
||||
volumeId: int("volume_id")
|
||||
.notNull()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { createHonoServer } from "react-router-hono-server/bun";
|
||||
import { runDbMigrations } from "./db/db";
|
||||
import { startup } from "./modules/lifecycle/startup";
|
||||
import { migrateToShortIds } from "./modules/lifecycle/migration";
|
||||
import { retagSnapshots } from "./modules/lifecycle/migration";
|
||||
import { logger } from "./utils/logger";
|
||||
import { shutdown } from "./modules/lifecycle/shutdown";
|
||||
import { REQUIRED_MIGRATIONS } from "./core/constants";
|
||||
|
|
@ -13,12 +13,12 @@ const app = createApp();
|
|||
|
||||
runDbMigrations();
|
||||
|
||||
await migrateToShortIds();
|
||||
await retagSnapshots();
|
||||
await validateRequiredMigrations(REQUIRED_MIGRATIONS);
|
||||
|
||||
startup();
|
||||
|
||||
logger.info(`Server is running at http://localhost:4096`);
|
||||
logger.info(`Server is running at http://localhost:${config.port}`);
|
||||
|
||||
export type AppType = typeof app;
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ process.on("SIGINT", async () => {
|
|||
|
||||
export default await createHonoServer({
|
||||
app,
|
||||
port: 4096,
|
||||
port: config.port,
|
||||
customBunServer: {
|
||||
idleTimeout: config.serverIdleTimeout,
|
||||
error(err) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { makeNfsBackend } from "./nfs/nfs-backend";
|
|||
import { makeRcloneBackend } from "./rclone/rclone-backend";
|
||||
import { makeSmbBackend } from "./smb/smb-backend";
|
||||
import { makeWebdavBackend } from "./webdav/webdav-backend";
|
||||
import { makeSftpBackend } from "./sftp/sftp-backend";
|
||||
|
||||
type OperationResult = {
|
||||
error?: string;
|
||||
|
|
@ -37,5 +38,8 @@ export const createVolumeBackend = (volume: Volume): VolumeBackend => {
|
|||
case "rclone": {
|
||||
return makeRcloneBackend(volume.config, path);
|
||||
}
|
||||
case "sftp": {
|
||||
return makeSftpBackend(volume.config, path);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
180
app/server/modules/backends/sftp/sftp-backend.ts
Normal file
180
app/server/modules/backends/sftp/sftp-backend.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
import { withTimeout } from "../../../utils/timeout";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const SSH_KEYS_DIR = "/var/lib/zerobyte/ssh";
|
||||
|
||||
const getPrivateKeyPath = (mountPath: string) => {
|
||||
const name = path.basename(mountPath);
|
||||
return path.join(SSH_KEYS_DIR, `${name}.key`);
|
||||
};
|
||||
|
||||
const getKnownHostsPath = (mountPath: string) => {
|
||||
const name = path.basename(mountPath);
|
||||
return path.join(SSH_KEYS_DIR, `${name}.known_hosts`);
|
||||
};
|
||||
|
||||
const mount = async (config: BackendConfig, mountPath: string) => {
|
||||
logger.debug(`Mounting SFTP volume ${mountPath}...`);
|
||||
|
||||
if (config.backend !== "sftp") {
|
||||
logger.error("Provided config is not for SFTP backend");
|
||||
return { status: BACKEND_STATUS.error, error: "Provided config is not for SFTP backend" };
|
||||
}
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("SFTP mounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "SFTP mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(mountPath);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
if (status === "error") {
|
||||
logger.debug(`Trying to unmount any existing mounts at ${mountPath} before mounting...`);
|
||||
await unmount(mountPath);
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(mountPath, { recursive: true });
|
||||
await fs.mkdir(SSH_KEYS_DIR, { recursive: true });
|
||||
|
||||
const { uid, gid } = os.userInfo();
|
||||
const options = [
|
||||
"reconnect",
|
||||
"ServerAliveInterval=15",
|
||||
"ServerAliveCountMax=3",
|
||||
"allow_other",
|
||||
`uid=${uid}`,
|
||||
`gid=${gid}`,
|
||||
];
|
||||
|
||||
if (config.skipHostKeyCheck || !config.knownHosts) {
|
||||
options.push("StrictHostKeyChecking=no", "UserKnownHostsFile=/dev/null");
|
||||
} else if (config.knownHosts) {
|
||||
const knownHostsPath = getKnownHostsPath(mountPath);
|
||||
await fs.writeFile(knownHostsPath, config.knownHosts, { mode: 0o600 });
|
||||
options.push(`UserKnownHostsFile=${knownHostsPath}`, "StrictHostKeyChecking=yes");
|
||||
}
|
||||
|
||||
if (config.readOnly) {
|
||||
options.push("ro");
|
||||
}
|
||||
|
||||
if (config.port) {
|
||||
options.push(`port=${config.port}`);
|
||||
}
|
||||
|
||||
const keyPath = getPrivateKeyPath(mountPath);
|
||||
if (config.privateKey) {
|
||||
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
||||
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
||||
if (!normalizedKey.endsWith("\n")) {
|
||||
normalizedKey += "\n";
|
||||
}
|
||||
await fs.writeFile(keyPath, normalizedKey, { mode: 0o600 });
|
||||
options.push(`IdentityFile=${keyPath}`);
|
||||
}
|
||||
|
||||
const source = `${config.username}@${config.host}:${config.path || ""}`;
|
||||
const args = [source, mountPath, "-o", options.join(",")];
|
||||
|
||||
logger.debug(`Mounting SFTP volume ${mountPath}...`);
|
||||
|
||||
let result: $.ShellOutput;
|
||||
if (config.password) {
|
||||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
args.push("-o", "password_stdin");
|
||||
logger.info(`Executing sshfs: echo "******" | sshfs ${args.join(" ")}`);
|
||||
result = await $`echo ${password} | sshfs ${args}`.nothrow();
|
||||
} else {
|
||||
logger.info(`Executing sshfs: sshfs ${args.join(" ")}`);
|
||||
result = await $`sshfs ${args}`.nothrow();
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
const errorMsg = result.stderr.toString() || result.stdout.toString() || "Unknown error";
|
||||
throw new Error(`Failed to mount SFTP volume: ${errorMsg}`);
|
||||
}
|
||||
|
||||
logger.info(`SFTP volume at ${mountPath} mounted successfully.`);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT * 2, "SFTP mount");
|
||||
} catch (error) {
|
||||
const errorMsg = toMessage(error);
|
||||
logger.error("Error mounting SFTP volume", { error: errorMsg });
|
||||
return { status: BACKEND_STATUS.error, error: errorMsg };
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (mountPath: string) => {
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("SFTP unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "SFTP unmounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
const mount = await getMountForPath(mountPath);
|
||||
if (!mount || mount.mountPoint !== mountPath) {
|
||||
logger.debug(`Path ${mountPath} is not a mount point. Skipping unmount.`);
|
||||
} else {
|
||||
await executeUnmount(mountPath);
|
||||
}
|
||||
|
||||
const keyPath = getPrivateKeyPath(mountPath);
|
||||
await fs.unlink(keyPath).catch(() => {});
|
||||
|
||||
const knownHostsPath = getKnownHostsPath(mountPath);
|
||||
await fs.unlink(knownHostsPath).catch(() => {});
|
||||
|
||||
await fs.rmdir(mountPath).catch(() => {});
|
||||
|
||||
logger.info(`SFTP volume at ${mountPath} unmounted successfully.`);
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
};
|
||||
|
||||
try {
|
||||
return await withTimeout(run(), OPERATION_TIMEOUT, "SFTP unmount");
|
||||
} catch (error) {
|
||||
logger.error("Error unmounting SFTP volume", { mountPath, error: toMessage(error) });
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (mountPath: string) => {
|
||||
const mount = await getMountForPath(mountPath);
|
||||
|
||||
if (!mount || mount.mountPoint !== mountPath) {
|
||||
return { status: BACKEND_STATUS.unmounted };
|
||||
}
|
||||
|
||||
if (mount.fstype !== "fuse.sshfs") {
|
||||
return {
|
||||
status: BACKEND_STATUS.error,
|
||||
error: `Invalid filesystem type: ${mount.fstype} (expected fuse.sshfs)`,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
export const makeSftpBackend = (config: BackendConfig, mountPath: string): VolumeBackend => ({
|
||||
mount: () => mount(config, mountPath),
|
||||
unmount: () => unmount(mountPath),
|
||||
checkHealth: () => checkHealth(mountPath),
|
||||
});
|
||||
|
|
@ -39,13 +39,14 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
|
||||
const source = `//${config.server}/${config.share}`;
|
||||
const { uid, gid } = os.userInfo();
|
||||
const options = [
|
||||
`user=${config.username}`,
|
||||
`pass=${password}`,
|
||||
`vers=${config.vers}`,
|
||||
`port=${config.port}`,
|
||||
"uid=1000",
|
||||
"gid=1000",
|
||||
`uid=${uid}`,
|
||||
`gid=${gid}`,
|
||||
];
|
||||
|
||||
if (config.domain) {
|
||||
|
|
|
|||
|
|
@ -43,9 +43,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
const port = config.port !== defaultPort ? `:${config.port}` : "";
|
||||
const source = `${protocol}://${config.server}${port}${config.path}`;
|
||||
|
||||
const { uid, gid } = os.userInfo();
|
||||
const options = config.readOnly
|
||||
? ["uid=1000", "gid=1000", "file_mode=0444", "dir_mode=0555", "ro"]
|
||||
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||
? [`uid=${uid}`, `gid=${gid}`, "file_mode=0444", "dir_mode=0555", "ro"]
|
||||
: [`uid=${uid}`, `gid=${gid}`, "file_mode=0664", "dir_mode=0775"];
|
||||
|
||||
if (config.username && config.password) {
|
||||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer;
|
|||
|
||||
const backupScheduleSchema = type({
|
||||
id: "number",
|
||||
shortId: "string",
|
||||
name: "string",
|
||||
volumeId: "number",
|
||||
repositoryId: "string",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { notificationsService } from "../notifications/notifications.service";
|
|||
import { repoMutex } from "../../core/repository-mutex";
|
||||
import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility";
|
||||
import path from "node:path";
|
||||
import { generateShortId } from "~/server/utils/id";
|
||||
|
||||
const runningBackups = new Map<number, AbortController>();
|
||||
|
||||
|
|
@ -126,6 +127,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||
includePatterns: data.includePatterns ?? [],
|
||||
oneFileSystem: data.oneFileSystem,
|
||||
nextBackupAt: nextBackupAt,
|
||||
shortId: generateShortId(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
|
@ -277,7 +279,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||
oneFileSystem?: boolean;
|
||||
signal?: AbortSignal;
|
||||
} = {
|
||||
tags: [schedule.id.toString()],
|
||||
tags: [schedule.shortId],
|
||||
oneFileSystem: schedule.oneFileSystem,
|
||||
signal: abortController.signal,
|
||||
};
|
||||
|
|
@ -476,7 +478,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => {
|
|||
logger.info(`running retention policy (forget) for schedule ${scheduleId}`);
|
||||
const releaseLock = await repoMutex.acquireExclusive(repository.id, `forget:${scheduleId}`);
|
||||
try {
|
||||
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.id.toString() });
|
||||
await restic.forget(repository.config, schedule.retentionPolicy, { tag: schedule.shortId });
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
|
|
@ -570,6 +572,14 @@ const copyToMirrors = async (
|
|||
sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] },
|
||||
retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"],
|
||||
) => {
|
||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||
where: eq(backupSchedulesTable.id, scheduleId),
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundError("Backup schedule not found");
|
||||
}
|
||||
|
||||
const mirrors = await db.query.backupScheduleMirrorsTable.findMany({
|
||||
where: eq(backupScheduleMirrorsTable.scheduleId, scheduleId),
|
||||
with: { repository: true },
|
||||
|
|
@ -599,7 +609,7 @@ const copyToMirrors = async (
|
|||
const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`);
|
||||
|
||||
try {
|
||||
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: scheduleId.toString() });
|
||||
await restic.copy(sourceRepository.config, mirror.repository.config, { tag: schedule.shortId });
|
||||
} finally {
|
||||
releaseSource();
|
||||
releaseMirror();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../../db/db";
|
||||
import { repositoriesTable } from "../../db/schema";
|
||||
import { VOLUME_MOUNT_BASE, REPOSITORY_BASE } from "../../core/constants";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { hasMigrationCheckpoint, recordMigrationCheckpoint } from "./checkpoint";
|
||||
import type { RepositoryConfig } from "~/schemas/restic";
|
||||
import { toMessage } from "~/server/utils/errors";
|
||||
import { safeSpawn } from "~/server/utils/spawn";
|
||||
import { addCommonArgs, buildEnv, buildRepoUrl, cleanupTemporaryKeys } from "~/server/utils/restic";
|
||||
|
||||
const MIGRATION_VERSION = "v0.14.0";
|
||||
const MIGRATION_VERSION = "v0.21.0";
|
||||
|
||||
interface MigrationResult {
|
||||
success: boolean;
|
||||
|
|
@ -28,19 +27,17 @@ export class MigrationError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export const migrateToShortIds = async () => {
|
||||
export const retagSnapshots = async () => {
|
||||
const alreadyMigrated = await hasMigrationCheckpoint(MIGRATION_VERSION);
|
||||
if (alreadyMigrated) {
|
||||
logger.debug(`Migration ${MIGRATION_VERSION} already completed, skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Starting short ID migration (${MIGRATION_VERSION})...`);
|
||||
logger.info(`Starting snapshots retagging migration (${MIGRATION_VERSION})...`);
|
||||
|
||||
const volumeResult = await migrateVolumeFolders();
|
||||
const repoResult = await migrateRepositoryFolders();
|
||||
|
||||
const allErrors = [...volumeResult.errors, ...repoResult.errors];
|
||||
const result = await migrateSnapshotsToShortIdTag();
|
||||
const allErrors = [...result.errors];
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
for (const err of allErrors) {
|
||||
|
|
@ -51,148 +48,45 @@ export const migrateToShortIds = async () => {
|
|||
|
||||
await recordMigrationCheckpoint(MIGRATION_VERSION);
|
||||
|
||||
logger.info(`Short ID migration (${MIGRATION_VERSION}) complete.`);
|
||||
logger.info(`Snapshots retagging migration (${MIGRATION_VERSION}) complete.`);
|
||||
};
|
||||
|
||||
const migrateVolumeFolders = async (): Promise<MigrationResult> => {
|
||||
const migrateSnapshotsToShortIdTag = async (): Promise<MigrationResult> => {
|
||||
const errors: Array<{ name: string; error: string }> = [];
|
||||
const volumes = await db.query.volumesTable.findMany({});
|
||||
const backupSchedules = await db.query.backupSchedulesTable.findMany({});
|
||||
|
||||
for (const volume of volumes) {
|
||||
if (volume.config.backend === "directory") {
|
||||
for (const schedule of backupSchedules) {
|
||||
const oldTag = schedule.id.toString();
|
||||
const newTag = schedule.shortId;
|
||||
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.id, schedule.repositoryId),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
errors.push({ name: `schedule:${schedule.name}`, error: `Associated repository not found` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldPath = path.join(VOLUME_MOUNT_BASE, volume.name);
|
||||
const newPath = path.join(VOLUME_MOUNT_BASE, volume.shortId);
|
||||
const repoUrl = buildRepoUrl(repository.config);
|
||||
const env = await buildEnv(repository.config);
|
||||
|
||||
const oldExists = await pathExists(oldPath);
|
||||
const newExists = await pathExists(newPath);
|
||||
const args = ["--repo", repoUrl, "tag", "--tag", oldTag, "--add", newTag, "--remove", oldTag];
|
||||
|
||||
if (oldExists && !newExists) {
|
||||
try {
|
||||
logger.info(`Migrating volume folder: ${oldPath} -> ${newPath}`);
|
||||
await fs.rename(oldPath, newPath);
|
||||
logger.info(`Successfully migrated volume folder for "${volume.name}"`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push({ name: `volume:${volume.name}`, error: errorMessage });
|
||||
}
|
||||
} else if (oldExists && newExists) {
|
||||
logger.warn(
|
||||
`Both old (${oldPath}) and new (${newPath}) paths exist for volume "${volume.name}". Manual intervention may be required.`,
|
||||
);
|
||||
addCommonArgs(args, env);
|
||||
|
||||
logger.info(`Migrating snapshots for schedule '${schedule.name}' from tag '${oldTag}' to '${newTag}'`);
|
||||
const res = await safeSpawn({ command: "restic", args, env });
|
||||
await cleanupTemporaryKeys(repository.config, env);
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic tag failed: ${res.stderr}`);
|
||||
errors.push({ name: `schedule:${schedule.name}`, error: `Restic tag command failed: ${toMessage(res.stderr)}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info(`Migrated snapshots for schedule '${schedule.name}' from tag '${oldTag}' to '${newTag}'`);
|
||||
}
|
||||
|
||||
return { success: errors.length === 0, errors };
|
||||
};
|
||||
|
||||
const migrateRepositoryFolders = async (): Promise<MigrationResult> => {
|
||||
const errors: Array<{ name: string; error: string }> = [];
|
||||
const repositories = await db.query.repositoriesTable.findMany({});
|
||||
|
||||
for (const repo of repositories) {
|
||||
if (repo.config.backend !== "local") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const config = repo.config as Extract<RepositoryConfig, { backend: "local" }>;
|
||||
|
||||
if (config.isExistingRepository) {
|
||||
logger.debug(`Skipping imported repository "${repo.name}" - folder path is user-defined`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.name === repo.shortId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const basePath = config.path || REPOSITORY_BASE;
|
||||
const oldPath = path.join(basePath, config.name);
|
||||
const newPath = path.join(basePath, repo.shortId);
|
||||
|
||||
const oldExists = await pathExists(oldPath);
|
||||
const newExists = await pathExists(newPath);
|
||||
|
||||
if (oldExists && !newExists) {
|
||||
try {
|
||||
logger.info(`Migrating repository folder: ${oldPath} -> ${newPath}`);
|
||||
await fs.rename(oldPath, newPath);
|
||||
|
||||
const updatedConfig: RepositoryConfig = {
|
||||
...config,
|
||||
name: repo.shortId,
|
||||
};
|
||||
|
||||
await db
|
||||
.update(repositoriesTable)
|
||||
.set({
|
||||
config: updatedConfig,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(repositoriesTable.id, repo.id));
|
||||
|
||||
logger.info(`Successfully migrated repository folder and config for "${repo.name}"`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push({ name: `repository:${repo.name}`, error: errorMessage });
|
||||
}
|
||||
} else if (oldExists && newExists) {
|
||||
logger.warn(
|
||||
`Both old (${oldPath}) and new (${newPath}) paths exist for repository "${repo.name}". Manual intervention may be required.`,
|
||||
);
|
||||
} else if (!oldExists && !newExists) {
|
||||
try {
|
||||
logger.info(`Updating config.name for repository "${repo.name}" (no folder exists yet)`);
|
||||
|
||||
const updatedConfig: RepositoryConfig = {
|
||||
...config,
|
||||
name: repo.shortId,
|
||||
};
|
||||
|
||||
await db
|
||||
.update(repositoriesTable)
|
||||
.set({
|
||||
config: updatedConfig,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(repositoriesTable.id, repo.id));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push({ name: `repository:${repo.name}`, error: errorMessage });
|
||||
}
|
||||
} else if (newExists && !oldExists && config.name !== repo.shortId) {
|
||||
try {
|
||||
logger.info(`Folder already at new path, updating config.name for repository "${repo.name}"`);
|
||||
|
||||
const updatedConfig: RepositoryConfig = {
|
||||
...config,
|
||||
name: repo.shortId,
|
||||
};
|
||||
|
||||
await db
|
||||
.update(repositoriesTable)
|
||||
.set({
|
||||
config: updatedConfig,
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
.where(eq(repositoriesTable.id, repo.id));
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
errors.push({ name: `repository:${repo.name}`, error: errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: errors.length === 0, errors };
|
||||
};
|
||||
|
||||
const pathExists = async (p: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import crypto from "node:crypto";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import { InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||
import { db } from "../../db/db";
|
||||
import { repositoriesTable } from "../../db/schema";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
|
|
@ -161,7 +161,7 @@ const listSnapshots = async (id: string, backupId?: string) => {
|
|||
let snapshots = [];
|
||||
|
||||
if (backupId) {
|
||||
snapshots = await restic.snapshots(repository.config, { tags: [backupId.toString()] });
|
||||
snapshots = await restic.snapshots(repository.config, { tags: [backupId] });
|
||||
} else {
|
||||
snapshots = await restic.snapshots(repository.config);
|
||||
}
|
||||
|
|
@ -396,15 +396,6 @@ const updateRepository = async (id: string, updates: { name?: string; compressio
|
|||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
if (
|
||||
updates.name !== undefined &&
|
||||
updates.name !== existing.name &&
|
||||
existing.config.backend === "local" &&
|
||||
existing.config.isExistingRepository
|
||||
) {
|
||||
throw new ConflictError("Cannot rename an imported local repository");
|
||||
}
|
||||
|
||||
const newConfig = repositoryConfigSchema(existing.config);
|
||||
if (newConfig instanceof type.errors) {
|
||||
throw new InternalServerError("Invalid repository configuration");
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ async function encryptSensitiveFields(config: BackendConfig): Promise<BackendCon
|
|||
...config,
|
||||
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
|
||||
};
|
||||
case "sftp":
|
||||
return {
|
||||
...config,
|
||||
password: config.password ? await cryptoUtils.sealSecret(config.password) : undefined,
|
||||
privateKey: config.privateKey ? await cryptoUtils.sealSecret(config.privateKey) : undefined,
|
||||
};
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import crypto from "node:crypto";
|
||||
|
||||
export const generateShortId = (length = 5): string => {
|
||||
export const generateShortId = (length = 8): string => {
|
||||
const bytesNeeded = Math.ceil((length * 3) / 4);
|
||||
return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length).toLowerCase();
|
||||
return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import os from "node:os";
|
|||
import { throttle } from "es-toolkit";
|
||||
import { type } from "arktype";
|
||||
import { REPOSITORY_BASE, RESTIC_PASS_FILE, DEFAULT_EXCLUDES } from "../core/constants";
|
||||
import { config as appConfig } from "../core/config";
|
||||
import { logger } from "./logger";
|
||||
import { cryptoUtils } from "./crypto";
|
||||
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||
|
|
@ -71,9 +72,14 @@ const ensurePassfile = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const buildRepoUrl = (config: RepositoryConfig): string => {
|
||||
export const buildRepoUrl = (config: RepositoryConfig): string => {
|
||||
switch (config.backend) {
|
||||
case "local":
|
||||
if (config.isExistingRepository) {
|
||||
if (!config.path) throw new Error("Path is required for existing local repositories");
|
||||
return config.path;
|
||||
}
|
||||
|
||||
return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`;
|
||||
case "s3":
|
||||
return `s3:${config.endpoint}/${config.bucket}`;
|
||||
|
|
@ -99,7 +105,7 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||
}
|
||||
};
|
||||
|
||||
const buildEnv = async (config: RepositoryConfig) => {
|
||||
export const buildEnv = async (config: RepositoryConfig) => {
|
||||
const env: Record<string, string> = {
|
||||
RESTIC_CACHE_DIR: "/var/lib/zerobyte/restic/cache",
|
||||
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
||||
|
|
@ -153,7 +159,7 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||
}
|
||||
case "sftp": {
|
||||
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
||||
const keyPath = path.join("/tmp", `ironmount-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
||||
const keyPath = path.join("/tmp", `zerobyte-ssh-${crypto.randomBytes(8).toString("hex")}`);
|
||||
|
||||
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
||||
if (!normalizedKey.endsWith("\n")) {
|
||||
|
|
@ -170,10 +176,6 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||
env._SFTP_KEY_PATH = keyPath;
|
||||
|
||||
const sshArgs = [
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-o",
|
||||
"LogLevel=VERBOSE",
|
||||
"-o",
|
||||
|
|
@ -184,6 +186,15 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||
keyPath,
|
||||
];
|
||||
|
||||
if (config.skipHostKeyCheck || !config.knownHosts) {
|
||||
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
|
||||
} else if (config.knownHosts) {
|
||||
const knownHostsPath = path.join("/tmp", `zerobyte-known-hosts-${crypto.randomBytes(8).toString("hex")}`);
|
||||
await fs.writeFile(knownHostsPath, config.knownHosts, { mode: 0o600 });
|
||||
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
|
||||
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
|
||||
}
|
||||
|
||||
if (config.port && config.port !== 22) {
|
||||
sshArgs.push("-p", String(config.port));
|
||||
}
|
||||
|
|
@ -257,6 +268,10 @@ const backup = async (
|
|||
args.push("--one-file-system");
|
||||
}
|
||||
|
||||
if (appConfig.resticHostname) {
|
||||
args.push("--host", appConfig.resticHostname);
|
||||
}
|
||||
|
||||
if (options?.tags && options.tags.length > 0) {
|
||||
for (const tag of options.tags) {
|
||||
args.push("--tag", tag);
|
||||
|
|
@ -795,9 +810,14 @@ const copy = async (
|
|||
};
|
||||
};
|
||||
|
||||
const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string, string>) => {
|
||||
if (config.backend === "sftp" && env._SFTP_KEY_PATH) {
|
||||
await fs.unlink(env._SFTP_KEY_PATH).catch(() => {});
|
||||
export const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string, string>) => {
|
||||
if (config.backend === "sftp") {
|
||||
if (env._SFTP_KEY_PATH) {
|
||||
await fs.unlink(env._SFTP_KEY_PATH).catch(() => {});
|
||||
}
|
||||
if (env._SFTP_KNOWN_HOSTS_PATH) {
|
||||
await fs.unlink(env._SFTP_KNOWN_HOSTS_PATH).catch(() => {});
|
||||
}
|
||||
} else if (config.isExistingRepository && config.customPassword && env.RESTIC_PASSWORD_FILE) {
|
||||
await fs.unlink(env.RESTIC_PASSWORD_FILE).catch(() => {});
|
||||
} else if (config.backend === "gcs" && env.GOOGLE_APPLICATION_CREDENTIALS) {
|
||||
|
|
@ -805,7 +825,7 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string
|
|||
}
|
||||
};
|
||||
|
||||
const addCommonArgs = (args: string[], env: Record<string, string>) => {
|
||||
export const addCommonArgs = (args: string[], env: Record<string, string>) => {
|
||||
args.push("--json");
|
||||
|
||||
if (env._SFTP_SSH_ARGS) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const createTestBackupSchedule = async (overrides: Partial<BackupSchedule
|
|||
cronExpression: "0 0 * * *",
|
||||
repositoryId: "repo_123",
|
||||
volumeId: 1,
|
||||
shortId: faker.string.uuid(),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue