mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
1 Commits
7091f1f296
...
v0.16.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdeab9f7fe |
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -1 +0,0 @@
|
|||||||
- This project uses the AGENTS.md file to give detailed information about the repository structure and development commands. Make sure to read this file before starting development.
|
|
||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -62,6 +62,8 @@ jobs:
|
|||||||
type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
type=semver,pattern={{major}}.{{minor}}.{{patch}},prefix=v,enable=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
latest=${{ needs.determine-release-type.outputs.release_type == 'release' }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
|
||||||
|
cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
|
||||||
|
|
||||||
- name: Build and push images
|
- name: Build and push images
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -74,8 +76,6 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: |
|
build-args: |
|
||||||
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
||||||
cache-from: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache
|
|
||||||
cache-to: type=registry,ref=ghcr.io/nicotsx/zerobyte:buildcache,mode=max
|
|
||||||
|
|
||||||
publish-release:
|
publish-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
267
AGENTS.md
267
AGENTS.md
@@ -1,267 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Important instructions
|
|
||||||
|
|
||||||
- Never create migration files manually. Always use the provided command to generate migrations
|
|
||||||
- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
- **Runtime**: Bun 1.3.1
|
|
||||||
- **Server**: Hono (web framework) with Bun runtime
|
|
||||||
- **Client**: React Router v7 (SSR) with React 19
|
|
||||||
- **Database**: SQLite with Drizzle ORM
|
|
||||||
- **Validation**: ArkType for runtime schema validation
|
|
||||||
- **Styling**: Tailwind CSS v4 + Radix UI components
|
|
||||||
- **Architecture**: Unified application structure (not a monorepo)
|
|
||||||
- **Code Quality**: Biome (formatter & linter)
|
|
||||||
- **Containerization**: Docker with multi-stage builds
|
|
||||||
|
|
||||||
## Repository Structure
|
|
||||||
|
|
||||||
This is a unified application with the following structure:
|
|
||||||
|
|
||||||
- `app/server` - Bun-based API server with Hono
|
|
||||||
- `app/client` - React Router SSR frontend components and modules
|
|
||||||
- `app/schemas` - Shared ArkType schemas for validation
|
|
||||||
- `app/drizzle` - Database migrations
|
|
||||||
|
|
||||||
### Type Checking
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run type checking and generate React Router types
|
|
||||||
bun run tsc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build for production
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate new migration from schema changes
|
|
||||||
bun gen:migrations
|
|
||||||
|
|
||||||
# Generate a custom empty migration
|
|
||||||
bunx drizzle-kit generate --custom --name=fix-timestamps-to-ms
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Client Generation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generate TypeScript API client from OpenAPI spec
|
|
||||||
# Note: Server is always running don't need to start it separately
|
|
||||||
bun run gen:api-client
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Format and lint (Biome)
|
|
||||||
bunx biome check --write .
|
|
||||||
|
|
||||||
# Format only
|
|
||||||
bunx biome format --write .
|
|
||||||
|
|
||||||
# Lint only
|
|
||||||
bunx biome lint .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Server Architecture
|
|
||||||
|
|
||||||
The server follows a modular service-oriented architecture:
|
|
||||||
|
|
||||||
**Entry Point**: `app/server/index.ts`
|
|
||||||
|
|
||||||
- Initializes servers using `react-router-hono-server`:
|
|
||||||
1. Main API server on port 4096 (REST API + serves static frontend)
|
|
||||||
2. Docker volume plugin server on Unix socket `/run/docker/plugins/zerobyte.sock` (optional, if Docker is available)
|
|
||||||
|
|
||||||
**Modules** (`app/server/modules/`):
|
|
||||||
Each module follows a controller <20> service <20> database pattern:
|
|
||||||
|
|
||||||
- `auth/` - User authentication and session management
|
|
||||||
- `volumes/` - Volume mounting/unmounting (NFS, SMB, WebDAV, 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)
|
|
||||||
- `driver/` - Docker volume plugin implementation
|
|
||||||
- `events/` - Server-Sent Events for real-time updates
|
|
||||||
- `system/` - System information and capabilities
|
|
||||||
- `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).
|
|
||||||
|
|
||||||
**Jobs** (`app/server/jobs/`):
|
|
||||||
Cron-based background jobs managed by the Scheduler:
|
|
||||||
|
|
||||||
- `backup-execution.ts` - Runs scheduled backups (every minute)
|
|
||||||
- `cleanup-dangling.ts` - Removes stale mounts (hourly)
|
|
||||||
- `healthchecks.ts` - Checks volume health (every 5 minutes)
|
|
||||||
- `repository-healthchecks.ts` - Validates repositories (every 10 minutes)
|
|
||||||
- `cleanup-sessions.ts` - Expires old sessions (daily)
|
|
||||||
|
|
||||||
**Core** (`app/server/core/`):
|
|
||||||
|
|
||||||
- `scheduler.ts` - Job scheduling system using node-cron
|
|
||||||
- `capabilities.ts` - Detects available system features (Docker support, etc.)
|
|
||||||
- `constants.ts` - Application-wide constants
|
|
||||||
|
|
||||||
**Utils** (`app/server/utils/`):
|
|
||||||
|
|
||||||
- `restic.ts` - Restic CLI wrapper with type-safe output parsing
|
|
||||||
- `spawn.ts` - Safe subprocess execution helpers
|
|
||||||
- `logger.ts` - Winston-based logging
|
|
||||||
- `crypto.ts` - Encryption utilities
|
|
||||||
- `errors.ts` - Error handling middleware
|
|
||||||
|
|
||||||
**Database** (`app/server/db/`):
|
|
||||||
|
|
||||||
- Uses Drizzle ORM with SQLite
|
|
||||||
- Schema in `schema.ts` defines: volumes, repositories, backup schedules, notifications, users, sessions
|
|
||||||
- Migrations: `app/drizzle/`
|
|
||||||
|
|
||||||
### Client Architecture
|
|
||||||
|
|
||||||
**Framework**: React Router v7 with SSR
|
|
||||||
**Entry Point**: `app/root.tsx`
|
|
||||||
|
|
||||||
The client uses:
|
|
||||||
|
|
||||||
- TanStack Query for server state management
|
|
||||||
- Auto-generated API client from OpenAPI spec (in `app/client/api-client/`)
|
|
||||||
- Radix UI primitives with custom Tailwind styling
|
|
||||||
- Server-Sent Events hook (`use-server-events.ts`) for real-time updates
|
|
||||||
|
|
||||||
Routes are organized in feature modules at `app/client/modules/*/routes/`.
|
|
||||||
|
|
||||||
### Shared Schemas
|
|
||||||
|
|
||||||
`app/schemas/` contains ArkType schemas used by both client and server:
|
|
||||||
|
|
||||||
- Volume configurations (NFS, SMB, WebDAV, directory)
|
|
||||||
- Repository configurations (S3, Azure, GCS, local, rclone)
|
|
||||||
- Restic command output parsing types
|
|
||||||
- Backend status types
|
|
||||||
|
|
||||||
These schemas provide runtime validation and TypeScript types.
|
|
||||||
|
|
||||||
## Restic Integration
|
|
||||||
|
|
||||||
Zerobyte is a wrapper around Restic for backup operations. Key integration points:
|
|
||||||
|
|
||||||
**Repository Management**:
|
|
||||||
|
|
||||||
- Creates/initializes Restic repositories via `restic init`
|
|
||||||
- Supports multiple backends: local, S3, Azure Blob Storage, Google Cloud Storage, or any rclone-supported backend
|
|
||||||
- Stores single encryption password in `/var/lib/zerobyte/restic/password` (auto-generated on first run)
|
|
||||||
|
|
||||||
**Backup Operations**:
|
|
||||||
|
|
||||||
- Executes `restic backup` with user-defined schedules (cron expressions)
|
|
||||||
- Supports include/exclude patterns for selective backups
|
|
||||||
- Parses JSON output for progress tracking and statistics
|
|
||||||
- Implements retention policies via `restic forget --prune`
|
|
||||||
|
|
||||||
**Repository Utilities** (`utils/restic.ts`):
|
|
||||||
|
|
||||||
- `buildRepoUrl()` - Constructs repository URLs for different backends
|
|
||||||
- `buildEnv()` - Sets environment variables (credentials, cache dir)
|
|
||||||
- `ensurePassfile()` - Manages encryption password file
|
|
||||||
- Type-safe parsing of Restic JSON output using ArkType schemas
|
|
||||||
|
|
||||||
**Rclone Integration** (`app/server/modules/repositories/`):
|
|
||||||
|
|
||||||
- Allows using any rclone backend as a Restic repository
|
|
||||||
- Dynamically generates rclone config and passes via environment variables
|
|
||||||
- Supports backends like Dropbox, Google Drive, OneDrive, Backblaze B2, etc.
|
|
||||||
|
|
||||||
## Docker Volume Plugin
|
|
||||||
|
|
||||||
When Docker socket is available (`/var/run/docker.sock`), Zerobyte registers as a Docker volume plugin:
|
|
||||||
|
|
||||||
**Plugin Location**: `/run/docker/plugins/zerobyte.sock`
|
|
||||||
**Implementation**: `app/server/modules/driver/driver.controller.ts`
|
|
||||||
|
|
||||||
This allows other containers to mount Zerobyte volumes using Docker.
|
|
||||||
|
|
||||||
The plugin implements the Docker Volume Plugin API v1.
|
|
||||||
|
|
||||||
## Environment & Configuration
|
|
||||||
|
|
||||||
**Runtime Environment Variables**:
|
|
||||||
|
|
||||||
- Database path: `./data/zerobyte.db` (configurable via `drizzle.config.ts`)
|
|
||||||
- Restic cache: `/var/lib/zerobyte/restic/cache`
|
|
||||||
- Restic password: `/var/lib/zerobyte/restic/password`
|
|
||||||
- Volume mounts: `/var/lib/zerobyte/mounts/<volume-name>`
|
|
||||||
- Local repositories: `/var/lib/zerobyte/repositories/<repo-name>`
|
|
||||||
|
|
||||||
**Capabilities Detection**:
|
|
||||||
On startup, the server detects available capabilities (see `core/capabilities.ts`):
|
|
||||||
|
|
||||||
- **Docker**: Requires `/var/run/docker.sock` access
|
|
||||||
- System will gracefully degrade if capabilities are unavailable
|
|
||||||
|
|
||||||
## Common Workflows
|
|
||||||
|
|
||||||
### Adding a New Volume Backend
|
|
||||||
|
|
||||||
1. Create backend implementation in `app/server/modules/backends/<backend>/`
|
|
||||||
2. Implement `mount()` and `unmount()` methods
|
|
||||||
3. Add schema to `app/schemas/volumes.ts`
|
|
||||||
4. Update `volumeConfigSchema` discriminated union
|
|
||||||
5. Update backend factory in `app/server/modules/backends/backend.ts`
|
|
||||||
|
|
||||||
### Adding a New Repository Backend
|
|
||||||
|
|
||||||
1. Add backend type to `app/schemas/restic.ts`
|
|
||||||
2. Update `buildRepoUrl()` in `app/server/utils/restic.ts`
|
|
||||||
3. Update `buildEnv()` to handle credentials/configuration
|
|
||||||
4. Add DTO schemas in `app/server/modules/repositories/repositories.dto.ts`
|
|
||||||
5. Update repository service to handle new backend
|
|
||||||
|
|
||||||
### Adding a New Scheduled Job
|
|
||||||
|
|
||||||
1. Create job class in `app/server/jobs/<job-name>.ts` extending `Job`
|
|
||||||
2. Implement `run()` method
|
|
||||||
3. Register in `app/server/modules/lifecycle/startup.ts` with cron expression:
|
|
||||||
```typescript
|
|
||||||
Scheduler.build(YourJob).schedule("* * * * *");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Code Style**: Uses Biome with tabs (not spaces), 120 char line width, double quotes
|
|
||||||
- **Imports**: Organize imports is disabled in Biome - do not auto-organize
|
|
||||||
- **TypeScript**: Uses `"type": "module"` - all imports must include extensions when targeting Node/Bun
|
|
||||||
- **Validation**: Prefer ArkType over Zod - it's used throughout the codebase
|
|
||||||
- **Database**: Timestamps are stored as Unix epoch integers, not ISO strings
|
|
||||||
- **Security**: Restic password file has 0600 permissions - never expose it
|
|
||||||
- **Mounting**: Requires privileged container or CAP_SYS_ADMIN for FUSE mounts
|
|
||||||
- **API Documentation**: OpenAPI spec auto-generated at `/api/v1/openapi.json`, docs at `/api/v1/docs`
|
|
||||||
|
|
||||||
## Docker Development Setup
|
|
||||||
|
|
||||||
The `docker-compose.yml` defines two services:
|
|
||||||
|
|
||||||
- `zerobyte-dev` - Development with hot reload (uses `development` stage)
|
|
||||||
- `zerobyte-prod` - Production build (uses `production` stage)
|
|
||||||
|
|
||||||
Both mount:
|
|
||||||
|
|
||||||
- `/var/lib/zerobyte` for persistent data
|
|
||||||
- `/dev/fuse` device for FUSE mounting
|
|
||||||
- Optionally `/var/run/docker.sock` for Docker plugin functionality
|
|
||||||
@@ -14,7 +14,7 @@ WORKDIR /deps
|
|||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG RESTIC_VERSION="0.18.1"
|
ARG RESTIC_VERSION="0.18.1"
|
||||||
ARG SHOUTRRR_VERSION="0.12.1"
|
ARG SHOUTRRR_VERSION="0.12.0"
|
||||||
ENV TARGETARCH=${TARGETARCH}
|
ENV TARGETARCH=${TARGETARCH}
|
||||||
|
|
||||||
RUN apk add --no-cache curl bzip2 unzip tar
|
RUN apk add --no-cache curl bzip2 unzip tar
|
||||||
|
|||||||
@@ -87,10 +87,12 @@ const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions
|
|||||||
if (options?.query) {
|
if (options?.query) {
|
||||||
params.query = options.query;
|
params.query = options.query;
|
||||||
}
|
}
|
||||||
return [params];
|
return [
|
||||||
|
params
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMeQueryKey = (options?: Options<GetMeData>) => createQueryKey('getMe', options);
|
export const getMeQueryKey = (options?: Options<GetMeData>) => createQueryKey("getMe", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current authenticated user
|
* Get current authenticated user
|
||||||
@@ -108,7 +110,7 @@ export const getMeOptions = (options?: Options<GetMeData>) => queryOptions<GetMe
|
|||||||
queryKey: getMeQueryKey(options)
|
queryKey: getMeQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getStatusQueryKey = (options?: Options<GetStatusData>) => createQueryKey('getStatus', options);
|
export const getStatusQueryKey = (options?: Options<GetStatusData>) => createQueryKey("getStatus", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get authentication system status
|
* Get authentication system status
|
||||||
@@ -143,7 +145,7 @@ export const changePasswordMutation = (options?: Partial<Options<ChangePasswordD
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey('listVolumes', options);
|
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey("listVolumes", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all volumes
|
* List all volumes
|
||||||
@@ -212,7 +214,7 @@ export const deleteVolumeMutation = (options?: Partial<Options<DeleteVolumeData>
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getVolumeQueryKey = (options: Options<GetVolumeData>) => createQueryKey('getVolume', options);
|
export const getVolumeQueryKey = (options: Options<GetVolumeData>) => createQueryKey("getVolume", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a volume by name
|
* Get a volume by name
|
||||||
@@ -247,7 +249,7 @@ export const updateVolumeMutation = (options?: Partial<Options<UpdateVolumeData>
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContainersUsingVolumeQueryKey = (options: Options<GetContainersUsingVolumeData>) => createQueryKey('getContainersUsingVolume', options);
|
export const getContainersUsingVolumeQueryKey = (options: Options<GetContainersUsingVolumeData>) => createQueryKey("getContainersUsingVolume", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get containers using a volume by name
|
* Get containers using a volume by name
|
||||||
@@ -316,7 +318,7 @@ export const healthCheckVolumeMutation = (options?: Partial<Options<HealthCheckV
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listFilesQueryKey = (options: Options<ListFilesData>) => createQueryKey('listFiles', options);
|
export const listFilesQueryKey = (options: Options<ListFilesData>) => createQueryKey("listFiles", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List files in a volume directory
|
* List files in a volume directory
|
||||||
@@ -334,7 +336,7 @@ export const listFilesOptions = (options: Options<ListFilesData>) => queryOption
|
|||||||
queryKey: listFilesQueryKey(options)
|
queryKey: listFilesQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const browseFilesystemQueryKey = (options?: Options<BrowseFilesystemData>) => createQueryKey('browseFilesystem', options);
|
export const browseFilesystemQueryKey = (options?: Options<BrowseFilesystemData>) => createQueryKey("browseFilesystem", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse directories on the host filesystem
|
* Browse directories on the host filesystem
|
||||||
@@ -352,7 +354,7 @@ export const browseFilesystemOptions = (options?: Options<BrowseFilesystemData>)
|
|||||||
queryKey: browseFilesystemQueryKey(options)
|
queryKey: browseFilesystemQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listRepositoriesQueryKey = (options?: Options<ListRepositoriesData>) => createQueryKey('listRepositories', options);
|
export const listRepositoriesQueryKey = (options?: Options<ListRepositoriesData>) => createQueryKey("listRepositories", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all repositories
|
* List all repositories
|
||||||
@@ -387,7 +389,7 @@ export const createRepositoryMutation = (options?: Partial<Options<CreateReposit
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) => createQueryKey('listRcloneRemotes', options);
|
export const listRcloneRemotesQueryKey = (options?: Options<ListRcloneRemotesData>) => createQueryKey("listRcloneRemotes", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all configured rclone remotes on the host system
|
* List all configured rclone remotes on the host system
|
||||||
@@ -422,7 +424,7 @@ export const deleteRepositoryMutation = (options?: Partial<Options<DeleteReposit
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRepositoryQueryKey = (options: Options<GetRepositoryData>) => createQueryKey('getRepository', options);
|
export const getRepositoryQueryKey = (options: Options<GetRepositoryData>) => createQueryKey("getRepository", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single repository by name
|
* Get a single repository by name
|
||||||
@@ -457,7 +459,7 @@ export const updateRepositoryMutation = (options?: Partial<Options<UpdateReposit
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listSnapshotsQueryKey = (options: Options<ListSnapshotsData>) => createQueryKey('listSnapshots', options);
|
export const listSnapshotsQueryKey = (options: Options<ListSnapshotsData>) => createQueryKey("listSnapshots", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all snapshots in a repository
|
* List all snapshots in a repository
|
||||||
@@ -492,7 +494,7 @@ export const deleteSnapshotMutation = (options?: Partial<Options<DeleteSnapshotD
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSnapshotDetailsQueryKey = (options: Options<GetSnapshotDetailsData>) => createQueryKey('getSnapshotDetails', options);
|
export const getSnapshotDetailsQueryKey = (options: Options<GetSnapshotDetailsData>) => createQueryKey("getSnapshotDetails", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get details of a specific snapshot
|
* Get details of a specific snapshot
|
||||||
@@ -510,7 +512,7 @@ export const getSnapshotDetailsOptions = (options: Options<GetSnapshotDetailsDat
|
|||||||
queryKey: getSnapshotDetailsQueryKey(options)
|
queryKey: getSnapshotDetailsQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listSnapshotFilesQueryKey = (options: Options<ListSnapshotFilesData>) => createQueryKey('listSnapshotFiles', options);
|
export const listSnapshotFilesQueryKey = (options: Options<ListSnapshotFilesData>) => createQueryKey("listSnapshotFiles", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List files and directories in a snapshot
|
* List files and directories in a snapshot
|
||||||
@@ -562,7 +564,7 @@ export const doctorRepositoryMutation = (options?: Partial<Options<DoctorReposit
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', options);
|
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey("listBackupSchedules", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all backup schedules
|
* List all backup schedules
|
||||||
@@ -614,7 +616,7 @@ export const deleteBackupScheduleMutation = (options?: Partial<Options<DeleteBac
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBackupScheduleQueryKey = (options: Options<GetBackupScheduleData>) => createQueryKey('getBackupSchedule', options);
|
export const getBackupScheduleQueryKey = (options: Options<GetBackupScheduleData>) => createQueryKey("getBackupSchedule", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a backup schedule by ID
|
* Get a backup schedule by ID
|
||||||
@@ -649,7 +651,7 @@ export const updateBackupScheduleMutation = (options?: Partial<Options<UpdateBac
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBackupScheduleForVolumeQueryKey = (options: Options<GetBackupScheduleForVolumeData>) => createQueryKey('getBackupScheduleForVolume', options);
|
export const getBackupScheduleForVolumeQueryKey = (options: Options<GetBackupScheduleForVolumeData>) => createQueryKey("getBackupScheduleForVolume", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a backup schedule for a specific volume
|
* Get a backup schedule for a specific volume
|
||||||
@@ -718,7 +720,7 @@ export const runForgetMutation = (options?: Partial<Options<RunForgetData>>): Us
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getScheduleNotificationsQueryKey = (options: Options<GetScheduleNotificationsData>) => createQueryKey('getScheduleNotifications', options);
|
export const getScheduleNotificationsQueryKey = (options: Options<GetScheduleNotificationsData>) => createQueryKey("getScheduleNotifications", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notification assignments for a backup schedule
|
* Get notification assignments for a backup schedule
|
||||||
@@ -753,7 +755,7 @@ export const updateScheduleNotificationsMutation = (options?: Partial<Options<Up
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getScheduleMirrorsQueryKey = (options: Options<GetScheduleMirrorsData>) => createQueryKey('getScheduleMirrors', options);
|
export const getScheduleMirrorsQueryKey = (options: Options<GetScheduleMirrorsData>) => createQueryKey("getScheduleMirrors", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mirror repository assignments for a backup schedule
|
* Get mirror repository assignments for a backup schedule
|
||||||
@@ -788,7 +790,7 @@ export const updateScheduleMirrorsMutation = (options?: Partial<Options<UpdateSc
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMirrorCompatibilityQueryKey = (options: Options<GetMirrorCompatibilityData>) => createQueryKey('getMirrorCompatibility', options);
|
export const getMirrorCompatibilityQueryKey = (options: Options<GetMirrorCompatibilityData>) => createQueryKey("getMirrorCompatibility", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
||||||
@@ -806,7 +808,7 @@ export const getMirrorCompatibilityOptions = (options: Options<GetMirrorCompatib
|
|||||||
queryKey: getMirrorCompatibilityQueryKey(options)
|
queryKey: getMirrorCompatibilityQueryKey(options)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listNotificationDestinationsQueryKey = (options?: Options<ListNotificationDestinationsData>) => createQueryKey('listNotificationDestinations', options);
|
export const listNotificationDestinationsQueryKey = (options?: Options<ListNotificationDestinationsData>) => createQueryKey("listNotificationDestinations", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all notification destinations
|
* List all notification destinations
|
||||||
@@ -858,7 +860,7 @@ export const deleteNotificationDestinationMutation = (options?: Partial<Options<
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNotificationDestinationQueryKey = (options: Options<GetNotificationDestinationData>) => createQueryKey('getNotificationDestination', options);
|
export const getNotificationDestinationQueryKey = (options: Options<GetNotificationDestinationData>) => createQueryKey("getNotificationDestination", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a notification destination by ID
|
* Get a notification destination by ID
|
||||||
@@ -910,7 +912,7 @@ export const testNotificationDestinationMutation = (options?: Partial<Options<Te
|
|||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSystemInfoQueryKey = (options?: Options<GetSystemInfoData>) => createQueryKey('getSystemInfo', options);
|
export const getSystemInfoQueryKey = (options?: Options<GetSystemInfoData>) => createQueryKey("getSystemInfo", options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get system information including available capabilities
|
* Get system information including available capabilities
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ import type { ClientOptions as ClientOptions2 } from './types.gen';
|
|||||||
*/
|
*/
|
||||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||||
|
|
||||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://192.168.2.42:4096' }));
|
export const client = createClient(createConfig<ClientOptions2>({
|
||||||
|
baseUrl: 'http://192.168.2.42:4096'
|
||||||
|
}));
|
||||||
|
|||||||
@@ -21,371 +21,583 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
|
|||||||
/**
|
/**
|
||||||
* Register a new user
|
* Register a new user
|
||||||
*/
|
*/
|
||||||
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({
|
export const register = <ThrowOnError extends boolean = false>(options?: Options<RegisterData, ThrowOnError>) => {
|
||||||
url: '/api/v1/auth/register',
|
return (options?.client ?? client).post<RegisterResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/auth/register',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login with username and password
|
* Login with username and password
|
||||||
*/
|
*/
|
||||||
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({
|
export const login = <ThrowOnError extends boolean = false>(options?: Options<LoginData, ThrowOnError>) => {
|
||||||
url: '/api/v1/auth/login',
|
return (options?.client ?? client).post<LoginResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/auth/login',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout current user
|
* Logout current user
|
||||||
*/
|
*/
|
||||||
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/logout', ...options });
|
export const logout = <ThrowOnError extends boolean = false>(options?: Options<LogoutData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).post<LogoutResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/auth/logout',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current authenticated user
|
* Get current authenticated user
|
||||||
*/
|
*/
|
||||||
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/me', ...options });
|
export const getMe = <ThrowOnError extends boolean = false>(options?: Options<GetMeData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<GetMeResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/auth/me',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get authentication system status
|
* Get authentication system status
|
||||||
*/
|
*/
|
||||||
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
|
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/auth/status',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change current user password
|
* Change current user password
|
||||||
*/
|
*/
|
||||||
export const changePassword = <ThrowOnError extends boolean = false>(options?: Options<ChangePasswordData, ThrowOnError>) => (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({
|
export const changePassword = <ThrowOnError extends boolean = false>(options?: Options<ChangePasswordData, ThrowOnError>) => {
|
||||||
url: '/api/v1/auth/change-password',
|
return (options?.client ?? client).post<ChangePasswordResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/auth/change-password',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all volumes
|
* List all volumes
|
||||||
*/
|
*/
|
||||||
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes', ...options });
|
export const listVolumes = <ThrowOnError extends boolean = false>(options?: Options<ListVolumesData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<ListVolumesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new volume
|
* Create a new volume
|
||||||
*/
|
*/
|
||||||
export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
|
export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => {
|
||||||
url: '/api/v1/volumes',
|
return (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/volumes',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection to backend
|
* Test connection to backend
|
||||||
*/
|
*/
|
||||||
export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
|
export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => {
|
||||||
url: '/api/v1/volumes/test-connection',
|
return (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/volumes/test-connection',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a volume
|
* Delete a volume
|
||||||
*/
|
*/
|
||||||
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{name}', ...options });
|
export const deleteVolume = <ThrowOnError extends boolean = false>(options: Options<DeleteVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteVolumeResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a volume by name
|
* Get a volume by name
|
||||||
*/
|
*/
|
||||||
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{name}', ...options });
|
export const getVolume = <ThrowOnError extends boolean = false>(options: Options<GetVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetVolumeResponses, GetVolumeErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a volume's configuration
|
* Update a volume's configuration
|
||||||
*/
|
*/
|
||||||
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
|
export const updateVolume = <ThrowOnError extends boolean = false>(options: Options<UpdateVolumeData, ThrowOnError>) => {
|
||||||
url: '/api/v1/volumes/{name}',
|
return (options.client ?? client).put<UpdateVolumeResponses, UpdateVolumeErrors, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/volumes/{name}',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get containers using a volume by name
|
* Get containers using a volume by name
|
||||||
*/
|
*/
|
||||||
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(options: Options<GetContainersUsingVolumeData, ThrowOnError>) => (options.client ?? client).get<GetContainersUsingVolumeResponses, GetContainersUsingVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{name}/containers', ...options });
|
export const getContainersUsingVolume = <ThrowOnError extends boolean = false>(options: Options<GetContainersUsingVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetContainersUsingVolumeResponses, GetContainersUsingVolumeErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}/containers',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mount a volume
|
* Mount a volume
|
||||||
*/
|
*/
|
||||||
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{name}/mount', ...options });
|
export const mountVolume = <ThrowOnError extends boolean = false>(options: Options<MountVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<MountVolumeResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}/mount',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmount a volume
|
* Unmount a volume
|
||||||
*/
|
*/
|
||||||
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{name}/unmount', ...options });
|
export const unmountVolume = <ThrowOnError extends boolean = false>(options: Options<UnmountVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<UnmountVolumeResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}/unmount',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a health check on a volume
|
* Perform a health check on a volume
|
||||||
*/
|
*/
|
||||||
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({ url: '/api/v1/volumes/{name}/health-check', ...options });
|
export const healthCheckVolume = <ThrowOnError extends boolean = false>(options: Options<HealthCheckVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<HealthCheckVolumeResponses, HealthCheckVolumeErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}/health-check',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List files in a volume directory
|
* List files in a volume directory
|
||||||
*/
|
*/
|
||||||
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/{name}/files', ...options });
|
export const listFiles = <ThrowOnError extends boolean = false>(options: Options<ListFilesData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<ListFilesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/{name}/files',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse directories on the host filesystem
|
* Browse directories on the host filesystem
|
||||||
*/
|
*/
|
||||||
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({ url: '/api/v1/volumes/filesystem/browse', ...options });
|
export const browseFilesystem = <ThrowOnError extends boolean = false>(options?: Options<BrowseFilesystemData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<BrowseFilesystemResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/volumes/filesystem/browse',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all repositories
|
* List all repositories
|
||||||
*/
|
*/
|
||||||
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories', ...options });
|
export const listRepositories = <ThrowOnError extends boolean = false>(options?: Options<ListRepositoriesData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<ListRepositoriesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new restic repository
|
* Create a new restic repository
|
||||||
*/
|
*/
|
||||||
export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
|
export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => {
|
||||||
url: '/api/v1/repositories',
|
return (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/repositories',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all configured rclone remotes on the host system
|
* List all configured rclone remotes on the host system
|
||||||
*/
|
*/
|
||||||
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/rclone-remotes', ...options });
|
export const listRcloneRemotes = <ThrowOnError extends boolean = false>(options?: Options<ListRcloneRemotesData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<ListRcloneRemotesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/rclone-remotes',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a repository
|
* Delete a repository
|
||||||
*/
|
*/
|
||||||
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}', ...options });
|
export const deleteRepository = <ThrowOnError extends boolean = false>(options: Options<DeleteRepositoryData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteRepositoryResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single repository by name
|
* Get a single repository by name
|
||||||
*/
|
*/
|
||||||
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}', ...options });
|
export const getRepository = <ThrowOnError extends boolean = false>(options: Options<GetRepositoryData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetRepositoryResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a repository's name or settings
|
* Update a repository's name or settings
|
||||||
*/
|
*/
|
||||||
export const updateRepository = <ThrowOnError extends boolean = false>(options: Options<UpdateRepositoryData, ThrowOnError>) => (options.client ?? client).patch<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError>({
|
export const updateRepository = <ThrowOnError extends boolean = false>(options: Options<UpdateRepositoryData, ThrowOnError>) => {
|
||||||
url: '/api/v1/repositories/{name}',
|
return (options.client ?? client).patch<UpdateRepositoryResponses, UpdateRepositoryErrors, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/repositories/{name}',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all snapshots in a repository
|
* List all snapshots in a repository
|
||||||
*/
|
*/
|
||||||
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}/snapshots', ...options });
|
export const listSnapshots = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<ListSnapshotsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a specific snapshot from a repository
|
* Delete a specific snapshot from a repository
|
||||||
*/
|
*/
|
||||||
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options });
|
export const deleteSnapshot = <ThrowOnError extends boolean = false>(options: Options<DeleteSnapshotData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteSnapshotResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get details of a specific snapshot
|
* Get details of a specific snapshot
|
||||||
*/
|
*/
|
||||||
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}', ...options });
|
export const getSnapshotDetails = <ThrowOnError extends boolean = false>(options: Options<GetSnapshotDetailsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetSnapshotDetailsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List files and directories in a snapshot
|
* List files and directories in a snapshot
|
||||||
*/
|
*/
|
||||||
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files', ...options });
|
export const listSnapshotFiles = <ThrowOnError extends boolean = false>(options: Options<ListSnapshotFilesData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<ListSnapshotFilesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}/files',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore a snapshot to a target path on the filesystem
|
* Restore a snapshot to a target path on the filesystem
|
||||||
*/
|
*/
|
||||||
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
|
export const restoreSnapshot = <ThrowOnError extends boolean = false>(options: Options<RestoreSnapshotData, ThrowOnError>) => {
|
||||||
url: '/api/v1/repositories/{name}/restore',
|
return (options.client ?? client).post<RestoreSnapshotResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/repositories/{name}/restore',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors.
|
* Run doctor operations on a repository to fix common issues (unlock, check, repair index). Use this when the repository is locked or has errors.
|
||||||
*/
|
*/
|
||||||
export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({ url: '/api/v1/repositories/{name}/doctor', ...options });
|
export const doctorRepository = <ThrowOnError extends boolean = false>(options: Options<DoctorRepositoryData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<DoctorRepositoryResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/repositories/{name}/doctor',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all backup schedules
|
* List all backup schedules
|
||||||
*/
|
*/
|
||||||
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({ url: '/api/v1/backups', ...options });
|
export const listBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ListBackupSchedulesData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<ListBackupSchedulesResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new backup schedule for a volume
|
* Create a new backup schedule for a volume
|
||||||
*/
|
*/
|
||||||
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
|
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => {
|
||||||
url: '/api/v1/backups',
|
return (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/backups',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a backup schedule
|
* Delete a backup schedule
|
||||||
*/
|
*/
|
||||||
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}', ...options });
|
export const deleteBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<DeleteBackupScheduleData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteBackupScheduleResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a backup schedule by ID
|
* Get a backup schedule by ID
|
||||||
*/
|
*/
|
||||||
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}', ...options });
|
export const getBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetBackupScheduleResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a backup schedule
|
* Update a backup schedule
|
||||||
*/
|
*/
|
||||||
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
|
export const updateBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<UpdateBackupScheduleData, ThrowOnError>) => {
|
||||||
url: '/api/v1/backups/{scheduleId}',
|
return (options.client ?? client).patch<UpdateBackupScheduleResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/backups/{scheduleId}',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a backup schedule for a specific volume
|
* Get a backup schedule for a specific volume
|
||||||
*/
|
*/
|
||||||
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/volume/{volumeId}', ...options });
|
export const getBackupScheduleForVolume = <ThrowOnError extends boolean = false>(options: Options<GetBackupScheduleForVolumeData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetBackupScheduleForVolumeResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/volume/{volumeId}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger a backup immediately for a schedule
|
* Trigger a backup immediately for a schedule
|
||||||
*/
|
*/
|
||||||
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/run', ...options });
|
export const runBackupNow = <ThrowOnError extends boolean = false>(options: Options<RunBackupNowData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<RunBackupNowResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/run',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop a backup that is currently in progress
|
* Stop a backup that is currently in progress
|
||||||
*/
|
*/
|
||||||
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/stop', ...options });
|
export const stopBackup = <ThrowOnError extends boolean = false>(options: Options<StopBackupData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<StopBackupResponses, StopBackupErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/stop',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually apply retention policy to clean up old snapshots
|
* Manually apply retention policy to clean up old snapshots
|
||||||
*/
|
*/
|
||||||
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/forget', ...options });
|
export const runForget = <ThrowOnError extends boolean = false>(options: Options<RunForgetData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<RunForgetResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/forget',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get notification assignments for a backup schedule
|
* Get notification assignments for a backup schedule
|
||||||
*/
|
*/
|
||||||
export const getScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<GetScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleNotificationsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/notifications', ...options });
|
export const getScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<GetScheduleNotificationsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetScheduleNotificationsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/notifications',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update notification assignments for a backup schedule
|
* Update notification assignments for a backup schedule
|
||||||
*/
|
*/
|
||||||
export const updateScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleNotificationsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleNotificationsResponses, unknown, ThrowOnError>({
|
export const updateScheduleNotifications = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleNotificationsData, ThrowOnError>) => {
|
||||||
url: '/api/v1/backups/{scheduleId}/notifications',
|
return (options.client ?? client).put<UpdateScheduleNotificationsResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/backups/{scheduleId}/notifications',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mirror repository assignments for a backup schedule
|
* Get mirror repository assignments for a backup schedule
|
||||||
*/
|
*/
|
||||||
export const getScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<GetScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/mirrors', ...options });
|
export const getScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<GetScheduleMirrorsData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update mirror repository assignments for a backup schedule
|
* Update mirror repository assignments for a backup schedule
|
||||||
*/
|
*/
|
||||||
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>) => (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
|
export const updateScheduleMirrors = <ThrowOnError extends boolean = false>(options: Options<UpdateScheduleMirrorsData, ThrowOnError>) => {
|
||||||
url: '/api/v1/backups/{scheduleId}/mirrors',
|
return (options.client ?? client).put<UpdateScheduleMirrorsResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/backups/{scheduleId}/mirrors',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
* Get mirror compatibility info for all repositories relative to a backup schedule's primary repository
|
||||||
*/
|
*/
|
||||||
export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(options: Options<GetMirrorCompatibilityData, ThrowOnError>) => (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({ url: '/api/v1/backups/{scheduleId}/mirrors/compatibility', ...options });
|
export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(options: Options<GetMirrorCompatibilityData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetMirrorCompatibilityResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/backups/{scheduleId}/mirrors/compatibility',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all notification destinations
|
* List all notification destinations
|
||||||
*/
|
*/
|
||||||
export const listNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ListNotificationDestinationsData, ThrowOnError>) => (options?.client ?? client).get<ListNotificationDestinationsResponses, unknown, ThrowOnError>({ url: '/api/v1/notifications/destinations', ...options });
|
export const listNotificationDestinations = <ThrowOnError extends boolean = false>(options?: Options<ListNotificationDestinationsData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<ListNotificationDestinationsResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/notifications/destinations',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification destination
|
* Create a new notification destination
|
||||||
*/
|
*/
|
||||||
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options?: Options<CreateNotificationDestinationData, ThrowOnError>) => (options?.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
|
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options?: Options<CreateNotificationDestinationData, ThrowOnError>) => {
|
||||||
url: '/api/v1/notifications/destinations',
|
return (options?.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/notifications/destinations',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a notification destination
|
* Delete a notification destination
|
||||||
*/
|
*/
|
||||||
export const deleteNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<DeleteNotificationDestinationData, ThrowOnError>) => (options.client ?? client).delete<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
export const deleteNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<DeleteNotificationDestinationData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).delete<DeleteNotificationDestinationResponses, DeleteNotificationDestinationErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/notifications/destinations/{id}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a notification destination by ID
|
* Get a notification destination by ID
|
||||||
*/
|
*/
|
||||||
export const getNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<GetNotificationDestinationData, ThrowOnError>) => (options.client ?? client).get<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}', ...options });
|
export const getNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<GetNotificationDestinationData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).get<GetNotificationDestinationResponses, GetNotificationDestinationErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/notifications/destinations/{id}',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a notification destination
|
* Update a notification destination
|
||||||
*/
|
*/
|
||||||
export const updateNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<UpdateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).patch<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError>({
|
export const updateNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<UpdateNotificationDestinationData, ThrowOnError>) => {
|
||||||
url: '/api/v1/notifications/destinations/{id}',
|
return (options.client ?? client).patch<UpdateNotificationDestinationResponses, UpdateNotificationDestinationErrors, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/notifications/destinations/{id}',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test a notification destination by sending a test message
|
* Test a notification destination by sending a test message
|
||||||
*/
|
*/
|
||||||
export const testNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<TestNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError>({ url: '/api/v1/notifications/destinations/{id}/test', ...options });
|
export const testNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<TestNotificationDestinationData, ThrowOnError>) => {
|
||||||
|
return (options.client ?? client).post<TestNotificationDestinationResponses, TestNotificationDestinationErrors, ThrowOnError>({
|
||||||
|
url: '/api/v1/notifications/destinations/{id}/test',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get system information including available capabilities
|
* Get system information including available capabilities
|
||||||
*/
|
*/
|
||||||
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({ url: '/api/v1/system/info', ...options });
|
export const getSystemInfo = <ThrowOnError extends boolean = false>(options?: Options<GetSystemInfoData, ThrowOnError>) => {
|
||||||
|
return (options?.client ?? client).get<GetSystemInfoResponses, unknown, ThrowOnError>({
|
||||||
|
url: '/api/v1/system/info',
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the Restic password file for backup recovery. Requires password re-authentication.
|
* Download the Restic password file for backup recovery. Requires password re-authentication.
|
||||||
*/
|
*/
|
||||||
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options?: Options<DownloadResticPasswordData, ThrowOnError>) => (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
|
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options?: Options<DownloadResticPasswordData, ThrowOnError>) => {
|
||||||
url: '/api/v1/system/restic-password',
|
return (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
|
||||||
...options,
|
url: '/api/v1/system/restic-password',
|
||||||
headers: {
|
...options,
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
...options?.headers
|
'Content-Type': 'application/json',
|
||||||
}
|
...options?.headers
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1291,14 +1291,12 @@ export type ListBackupSchedulesResponses = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
excludeIfPresent: Array<string> | null;
|
|
||||||
excludePatterns: Array<string> | null;
|
excludePatterns: Array<string> | null;
|
||||||
id: number;
|
id: number;
|
||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
name: string;
|
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||||
@@ -1437,10 +1435,8 @@ export type CreateBackupScheduleData = {
|
|||||||
body?: {
|
body?: {
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
name: string;
|
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
volumeId: number;
|
volumeId: number;
|
||||||
excludeIfPresent?: Array<string>;
|
|
||||||
excludePatterns?: Array<string>;
|
excludePatterns?: Array<string>;
|
||||||
includePatterns?: Array<string>;
|
includePatterns?: Array<string>;
|
||||||
retentionPolicy?: {
|
retentionPolicy?: {
|
||||||
@@ -1467,14 +1463,12 @@ export type CreateBackupScheduleResponses = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
excludeIfPresent: Array<string> | null;
|
|
||||||
excludePatterns: Array<string> | null;
|
excludePatterns: Array<string> | null;
|
||||||
id: number;
|
id: number;
|
||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
name: string;
|
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
retentionPolicy: {
|
retentionPolicy: {
|
||||||
@@ -1530,14 +1524,12 @@ export type GetBackupScheduleResponses = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
excludeIfPresent: Array<string> | null;
|
|
||||||
excludePatterns: Array<string> | null;
|
excludePatterns: Array<string> | null;
|
||||||
id: number;
|
id: number;
|
||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
name: string;
|
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||||
@@ -1677,10 +1669,8 @@ export type UpdateBackupScheduleData = {
|
|||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
excludeIfPresent?: Array<string>;
|
|
||||||
excludePatterns?: Array<string>;
|
excludePatterns?: Array<string>;
|
||||||
includePatterns?: Array<string>;
|
includePatterns?: Array<string>;
|
||||||
name?: string;
|
|
||||||
retentionPolicy?: {
|
retentionPolicy?: {
|
||||||
keepDaily?: number;
|
keepDaily?: number;
|
||||||
keepHourly?: number;
|
keepHourly?: number;
|
||||||
@@ -1707,14 +1697,12 @@ export type UpdateBackupScheduleResponses = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
excludeIfPresent: Array<string> | null;
|
|
||||||
excludePatterns: Array<string> | null;
|
excludePatterns: Array<string> | null;
|
||||||
id: number;
|
id: number;
|
||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
name: string;
|
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
retentionPolicy: {
|
retentionPolicy: {
|
||||||
@@ -1750,14 +1738,12 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
cronExpression: string;
|
cronExpression: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
excludeIfPresent: Array<string> | null;
|
|
||||||
excludePatterns: Array<string> | null;
|
excludePatterns: Array<string> | null;
|
||||||
id: number;
|
id: number;
|
||||||
includePatterns: Array<string> | null;
|
includePatterns: Array<string> | null;
|
||||||
lastBackupAt: number | null;
|
lastBackupAt: number | null;
|
||||||
lastBackupError: string | null;
|
lastBackupError: string | null;
|
||||||
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
lastBackupStatus: 'error' | 'in_progress' | 'success' | 'warning' | null;
|
||||||
name: string;
|
|
||||||
nextBackupAt: number | null;
|
nextBackupAt: number | null;
|
||||||
repository: {
|
repository: {
|
||||||
compressionMode: 'auto' | 'max' | 'off' | null;
|
compressionMode: 'auto' | 'max' | 'off' | null;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { arktypeResolver } from "@hookform/resolvers/arktype";
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Check, Pencil, Save, X } from "lucide-react";
|
|
||||||
import { cn, slugify } from "~/client/lib/utils";
|
import { cn, slugify } from "~/client/lib/utils";
|
||||||
import { deepClean } from "~/utils/object";
|
import { deepClean } from "~/utils/object";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
@@ -268,7 +267,6 @@ export const CreateRepositoryForm = ({
|
|||||||
{form.watch("path") || "/var/lib/zerobyte/repositories"}
|
{form.watch("path") || "/var/lib/zerobyte/repositories"}
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" onClick={() => setShowPathWarning(true)} size="sm">
|
<Button type="button" variant="outline" onClick={() => setShowPathWarning(true)} size="sm">
|
||||||
<Pencil className="h-4 w-4 mr-2" />
|
|
||||||
Change
|
Change
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,7 +278,7 @@ export const CreateRepositoryForm = ({
|
|||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle className="flex items-center gap-2">
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||||
Important: Host mount required
|
Important: Host Mount Required
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription className="space-y-3">
|
<AlertDialogDescription className="space-y-3">
|
||||||
<p>When selecting a custom path, ensure it is mounted from the host machine into the container.</p>
|
<p>When selecting a custom path, ensure it is mounted from the host machine into the container.</p>
|
||||||
@@ -322,14 +320,8 @@ export const CreateRepositoryForm = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>Done</AlertDialogAction>
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>
|
|
||||||
<Check className="h-4 w-4 mr-2" />
|
|
||||||
Done
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
@@ -783,7 +775,6 @@ export const CreateRepositoryForm = ({
|
|||||||
|
|
||||||
{mode === "update" && (
|
{mode === "update" && (
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
import { arktypeResolver } from "@hookform/resolvers/arktype";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { CheckCircle, Loader2, Pencil, Plug, Save, XCircle } from "lucide-react";
|
import { CheckCircle, Loader2, XCircle } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { cn, slugify } from "~/client/lib/utils";
|
import { cn, slugify } from "~/client/lib/utils";
|
||||||
@@ -152,7 +152,6 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
<div className="text-sm font-mono break-all">{field.value}</div>
|
<div className="text-sm font-mono break-all">{field.value}</div>
|
||||||
</div>
|
</div>
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange("")}>
|
<Button type="button" variant="outline" size="sm" onClick={() => field.onChange("")}>
|
||||||
<Pencil className="h-4 w-4 mr-2" />
|
|
||||||
Change
|
Change
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -562,7 +561,6 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
|
{!testBackendConnection.isPending && testMessage && !testMessage.success && (
|
||||||
<XCircle className="mr-2 h-4 w-4 text-red-500" />
|
<XCircle className="mr-2 h-4 w-4 text-red-500" />
|
||||||
)}
|
)}
|
||||||
{!testBackendConnection.isPending && !testMessage && <Plug className="mr-2 h-4 w-4" />}
|
|
||||||
{testBackendConnection.isPending
|
{testBackendConnection.isPending
|
||||||
? "Testing..."
|
? "Testing..."
|
||||||
: testMessage
|
: testMessage
|
||||||
@@ -586,7 +584,6 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
)}
|
)}
|
||||||
{mode === "update" && (
|
{mode === "update" && (
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ export function RestoreForm({ snapshot, repositoryName, snapshotId, returnPath }
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleRestore} disabled={isRestoring || !canRestore}>
|
<Button variant="primary" onClick={handleRestore} disabled={isRestoring || !canRestore}>
|
||||||
<RotateCcw className="h-4 w-4 mr-2" />
|
|
||||||
{isRestoring
|
{isRestoring
|
||||||
? "Restoring..."
|
? "Restoring..."
|
||||||
: selectedPaths.size > 0
|
: selectedPaths.size > 0
|
||||||
|
|||||||
@@ -23,11 +23,8 @@ import type { BackupSchedule, Volume } from "~/client/lib/types";
|
|||||||
import { deepClean } from "~/utils/object";
|
import { deepClean } from "~/utils/object";
|
||||||
|
|
||||||
const internalFormSchema = type({
|
const internalFormSchema = type({
|
||||||
name: "1 <= string <= 32",
|
|
||||||
repositoryId: "string",
|
repositoryId: "string",
|
||||||
excludePatternsText: "string?",
|
excludePatternsText: "string?",
|
||||||
excludeIfPresentText: "string?",
|
|
||||||
includePatternsText: "string?",
|
|
||||||
includePatterns: "string[]?",
|
includePatterns: "string[]?",
|
||||||
frequency: "string",
|
frequency: "string",
|
||||||
dailyTime: "string?",
|
dailyTime: "string?",
|
||||||
@@ -53,12 +50,8 @@ export const weeklyDays = [
|
|||||||
|
|
||||||
type InternalFormValues = typeof internalFormSchema.infer;
|
type InternalFormValues = typeof internalFormSchema.infer;
|
||||||
|
|
||||||
export type BackupScheduleFormValues = Omit<
|
export type BackupScheduleFormValues = Omit<InternalFormValues, "excludePatternsText"> & {
|
||||||
InternalFormValues,
|
|
||||||
"excludePatternsText" | "excludeIfPresentText" | "includePatternsText"
|
|
||||||
> & {
|
|
||||||
excludePatterns?: string[];
|
excludePatterns?: string[];
|
||||||
excludeIfPresent?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -86,21 +79,13 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValu
|
|||||||
|
|
||||||
const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined;
|
const weeklyDay = frequency === "weekly" ? dayOfWeekPart : undefined;
|
||||||
|
|
||||||
const patterns = schedule.includePatterns || [];
|
|
||||||
const isGlobPattern = (p: string) => /[*?[\]]/.test(p);
|
|
||||||
const fileBrowserPaths = patterns.filter((p) => !isGlobPattern(p));
|
|
||||||
const textPatterns = patterns.filter(isGlobPattern);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: schedule.name,
|
|
||||||
repositoryId: schedule.repositoryId,
|
repositoryId: schedule.repositoryId,
|
||||||
frequency,
|
frequency,
|
||||||
dailyTime,
|
dailyTime,
|
||||||
weeklyDay,
|
weeklyDay,
|
||||||
includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined,
|
includePatterns: schedule.includePatterns || undefined,
|
||||||
includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined,
|
|
||||||
excludePatternsText: schedule.excludePatterns?.join("\n") || undefined,
|
excludePatternsText: schedule.excludePatterns?.join("\n") || undefined,
|
||||||
excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined,
|
|
||||||
...schedule.retentionPolicy,
|
...schedule.retentionPolicy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -113,40 +98,18 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(data: InternalFormValues) => {
|
(data: InternalFormValues) => {
|
||||||
const {
|
// Convert excludePatternsText string to excludePatterns array
|
||||||
excludePatternsText,
|
const { excludePatternsText, ...rest } = data;
|
||||||
excludeIfPresentText,
|
|
||||||
includePatternsText,
|
|
||||||
includePatterns: fileBrowserPatterns,
|
|
||||||
...rest
|
|
||||||
} = data;
|
|
||||||
const excludePatterns = excludePatternsText
|
const excludePatterns = excludePatternsText
|
||||||
? excludePatternsText
|
? excludePatternsText
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((p) => p.trim())
|
.map((p) => p.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: [];
|
: undefined;
|
||||||
|
|
||||||
const excludeIfPresent = excludeIfPresentText
|
|
||||||
? excludeIfPresentText
|
|
||||||
.split("\n")
|
|
||||||
.map((p) => p.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const textPatterns = includePatternsText
|
|
||||||
? includePatternsText
|
|
||||||
.split("\n")
|
|
||||||
.map((p) => p.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
: [];
|
|
||||||
const includePatterns = [...(fileBrowserPatterns || []), ...textPatterns];
|
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
...rest,
|
...rest,
|
||||||
includePatterns: includePatterns.length > 0 ? includePatterns : [],
|
|
||||||
excludePatterns,
|
excludePatterns,
|
||||||
excludeIfPresent,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onSubmit],
|
[onSubmit],
|
||||||
@@ -185,21 +148,6 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-6 md:grid-cols-2">
|
<CardContent className="grid gap-6 md:grid-cols-2">
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="md:col-span-2">
|
|
||||||
<FormLabel>Backup name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My backup" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>A unique name to identify this backup schedule.</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="repositoryId"
|
name="repositoryId"
|
||||||
@@ -312,7 +260,6 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<VolumeFileBrowser
|
<VolumeFileBrowser
|
||||||
key={volume.id}
|
|
||||||
volumeName={volume.name}
|
volumeName={volume.name}
|
||||||
selectedPaths={selectedPaths}
|
selectedPaths={selectedPaths}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
@@ -332,27 +279,6 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="includePatternsText"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-6">
|
|
||||||
<FormLabel>Additional include patterns</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
{...field}
|
|
||||||
placeholder="/data/** /config/*.json *.db"
|
|
||||||
className="font-mono text-sm min-h-[100px]"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Optionally add custom include patterns using glob syntax. Enter one pattern per line. These will
|
|
||||||
be combined with the paths selected above.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -394,28 +320,6 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="excludeIfPresentText"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="mt-6">
|
|
||||||
<FormLabel>Exclude if file present</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
{...field}
|
|
||||||
placeholder=".nobackup .exclude-from-backup CACHEDIR.TAG"
|
|
||||||
className="font-mono text-sm min-h-20"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Exclude folders containing a file with the specified name. Enter one filename per line. For
|
|
||||||
example, use <code className="bg-muted px-1 rounded">.nobackup</code> to skip any folder
|
|
||||||
containing a <code className="bg-muted px-1 rounded">.nobackup</code> file.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -578,27 +482,18 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
{repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}
|
{repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{(formValues.includePatterns && formValues.includePatterns.length > 0) ||
|
{formValues.includePatterns && formValues.includePatterns.length > 0 && (
|
||||||
formValues.includePatternsText ? (
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Include paths/patterns</p>
|
<p className="text-xs uppercase text-muted-foreground">Include paths</p>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{formValues.includePatterns?.map((path) => (
|
{formValues.includePatterns.map((path) => (
|
||||||
<span key={path} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
<span key={path} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
||||||
{path}
|
{path}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{formValues.includePatternsText
|
|
||||||
?.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((pattern) => (
|
|
||||||
<span key={pattern} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
|
||||||
{pattern.trim()}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
)}
|
||||||
{formValues.excludePatternsText && (
|
{formValues.excludePatternsText && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Exclude patterns</p>
|
<p className="text-xs uppercase text-muted-foreground">Exclude patterns</p>
|
||||||
@@ -614,21 +509,6 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{formValues.excludeIfPresentText && (
|
|
||||||
<div>
|
|
||||||
<p className="text-xs uppercase text-muted-foreground">Exclude if present</p>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{formValues.excludeIfPresentText
|
|
||||||
.split("\n")
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((filename) => (
|
|
||||||
<span key={filename} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
|
||||||
{filename.trim()}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Retention</p>
|
<p className="text-xs uppercase text-muted-foreground">Retention</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Check, Database, Eraser, HardDrive, Pencil, Play, Square, Trash2, X } from "lucide-react";
|
import { Eraser, Pencil, Play, Square, Trash2 } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { OnOff } from "~/client/components/onoff";
|
import { OnOff } from "~/client/components/onoff";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
@@ -18,7 +18,6 @@ import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError } from "~/client/lib/errors";
|
||||||
import { Link } from "react-router";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schedule: BackupSchedule;
|
schedule: BackupSchedule;
|
||||||
@@ -83,17 +82,10 @@ export const ScheduleSummary = (props: Props) => {
|
|||||||
<CardHeader className="space-y-4">
|
<CardHeader className="space-y-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{schedule.name}</CardTitle>
|
<CardTitle>Backup schedule</CardTitle>
|
||||||
<CardDescription className="mt-1">
|
<CardDescription>
|
||||||
<Link to={`/volumes/${schedule.volume.name}`} className="hover:underline">
|
Automated backup configuration for volume
|
||||||
<HardDrive className="inline h-4 w-4 mr-2" />
|
<strong className="text-strong-accent">{schedule.volume.name}</strong>
|
||||||
<span>{schedule.volume.name}</span>
|
|
||||||
</Link>
|
|
||||||
<span className="mx-2">→</span>
|
|
||||||
<Link to={`/repositories/${schedule.repository.name}`} className="hover:underline">
|
|
||||||
<Database className="inline h-4 w-4 mr-2 text-strong-accent" />
|
|
||||||
<span className="text-strong-accent">{schedule.repository.name}</span>
|
|
||||||
</Link>
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 justify-between sm:justify-start">
|
<div className="flex items-center gap-2 justify-between sm:justify-start">
|
||||||
@@ -228,14 +220,8 @@ export const ScheduleSummary = (props: Props) => {
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<AlertDialogAction onClick={handleConfirmForget}>Run cleanup</AlertDialogAction>
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleConfirmForget}>
|
|
||||||
<Check className="h-4 w-4 mr-2" />
|
|
||||||
Run cleanup
|
|
||||||
</AlertDialogAction>
|
|
||||||
</div>
|
</div>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { FileIcon, RotateCcw, Trash2 } from "lucide-react";
|
import { FileIcon } from "lucide-react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { FileTree } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
@@ -98,7 +98,6 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
}
|
}
|
||||||
className={buttonVariants({ variant: "primary", size: "sm" })}
|
className={buttonVariants({ variant: "primary", size: "sm" })}
|
||||||
>
|
>
|
||||||
<RotateCcw className="h-4 w-4" />
|
|
||||||
Restore
|
Restore
|
||||||
</Link>
|
</Link>
|
||||||
{onDeleteSnapshot && (
|
{onDeleteSnapshot && (
|
||||||
@@ -109,7 +108,6 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
disabled={isDeletingSnapshot}
|
disabled={isDeletingSnapshot}
|
||||||
loading={isDeletingSnapshot}
|
loading={isDeletingSnapshot}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
{isDeletingSnapshot ? "Deleting..." : "Delete Snapshot"}
|
{isDeletingSnapshot ? "Deleting..." : "Delete Snapshot"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useId, useState } from "react";
|
|||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { redirect, useNavigate } from "react-router";
|
import { redirect, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Save, X } from "lucide-react";
|
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -36,10 +35,10 @@ import { ScheduleMirrorsConfig } from "../components/schedule-mirrors-config";
|
|||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: (match: Route.MetaArgs) => {
|
breadcrumb: (match: Route.MetaArgs) => [
|
||||||
const data = match.loaderData;
|
{ label: "Backups", href: "/backups" },
|
||||||
return [{ label: "Backups", href: "/backups" }, { label: data.schedule.name }];
|
{ label: `Schedule #${match.params.id}` },
|
||||||
},
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
@@ -154,14 +153,12 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
updateSchedule.mutate({
|
updateSchedule.mutate({
|
||||||
path: { scheduleId: schedule.id.toString() },
|
path: { scheduleId: schedule.id.toString() },
|
||||||
body: {
|
body: {
|
||||||
name: formValues.name,
|
|
||||||
repositoryId: formValues.repositoryId,
|
repositoryId: formValues.repositoryId,
|
||||||
enabled: schedule.enabled,
|
enabled: schedule.enabled,
|
||||||
cronExpression,
|
cronExpression,
|
||||||
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
|
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
|
||||||
includePatterns: formValues.includePatterns,
|
includePatterns: formValues.includePatterns,
|
||||||
excludePatterns: formValues.excludePatterns,
|
excludePatterns: formValues.excludePatterns,
|
||||||
excludeIfPresent: formValues.excludeIfPresent,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -174,9 +171,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
enabled,
|
enabled,
|
||||||
cronExpression: schedule.cronExpression,
|
cronExpression: schedule.cronExpression,
|
||||||
retentionPolicy: schedule.retentionPolicy || undefined,
|
retentionPolicy: schedule.retentionPolicy || undefined,
|
||||||
includePatterns: schedule.includePatterns || [],
|
includePatterns: schedule.includePatterns || undefined,
|
||||||
excludePatterns: schedule.excludePatterns || [],
|
excludePatterns: schedule.excludePatterns || undefined,
|
||||||
excludeIfPresent: schedule.excludeIfPresent || [],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -207,11 +203,9 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
|||||||
<CreateScheduleForm volume={schedule.volume} initialValues={schedule} onSubmit={handleSubmit} formId={formId} />
|
<CreateScheduleForm volume={schedule.volume} initialValues={schedule} onSubmit={handleSubmit} formId={formId} />
|
||||||
<div className="flex justify-end mt-4 gap-2">
|
<div className="flex justify-end mt-4 gap-2">
|
||||||
<Button type="submit" className="ml-auto" variant="primary" form={formId} loading={updateSchedule.isPending}>
|
<Button type="submit" className="ml-auto" variant="primary" form={formId} loading={updateSchedule.isPending}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Update schedule
|
Update schedule
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => setIsEditMode(false)}>
|
<Button variant="outline" onClick={() => setIsEditMode(false)}>
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,11 +67,13 @@ export default function Backups({ loaderData }: Route.ComponentProps) {
|
|||||||
{schedules.map((schedule) => (
|
{schedules.map((schedule) => (
|
||||||
<Link key={schedule.id} to={`/backups/${schedule.id}`}>
|
<Link key={schedule.id} to={`/backups/${schedule.id}`}>
|
||||||
<Card key={schedule.id} className="flex flex-col h-full">
|
<Card key={schedule.id} className="flex flex-col h-full">
|
||||||
<CardHeader className="pb-3 overflow-hidden">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between gap-2 w-full">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex items-center gap-2 flex-1 min-w-0 w-0">
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<CalendarClock className="h-5 w-5 text-muted-foreground shrink-0" />
|
<HardDrive className="h-5 w-5 text-muted-foreground shrink-0" />
|
||||||
<CardTitle className="text-lg truncate">{schedule.name}</CardTitle>
|
<CardTitle className="text-lg truncate">
|
||||||
|
Volume <span className="text-strong-accent">{schedule.volume.name}</span>
|
||||||
|
</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<BackupStatusDot
|
<BackupStatusDot
|
||||||
enabled={schedule.enabled}
|
enabled={schedule.enabled}
|
||||||
@@ -79,12 +81,9 @@ export default function Backups({ loaderData }: Route.ComponentProps) {
|
|||||||
isInProgress={schedule.lastBackupStatus === "in_progress"}
|
isInProgress={schedule.lastBackupStatus === "in_progress"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CardDescription className="ml-0.5 flex items-center gap-2 text-xs">
|
<CardDescription className="flex items-center gap-2 mt-2">
|
||||||
<HardDrive className="h-3.5 w-3.5" />
|
<Database className="h-4 w-4" />
|
||||||
<span className="truncate">{schedule.volume.name}</span>
|
<span className="truncate">{schedule.repository.name}</span>
|
||||||
<span className="text-muted-foreground">→</span>
|
|
||||||
<Database className="h-3.5 w-3.5 text-strong-accent" />
|
|
||||||
<span className="truncate text-strong-accent">{schedule.repository.name}</span>
|
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex-1 space-y-4">
|
<CardContent className="flex-1 space-y-4">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useId, useState } from "react";
|
import { useId, useState } from "react";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Database, HardDrive, Plus } from "lucide-react";
|
import { Database, HardDrive } from "lucide-react";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -83,7 +83,6 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
|||||||
|
|
||||||
createSchedule.mutate({
|
createSchedule.mutate({
|
||||||
body: {
|
body: {
|
||||||
name: formValues.name,
|
|
||||||
volumeId: selectedVolumeId,
|
volumeId: selectedVolumeId,
|
||||||
repositoryId: formValues.repositoryId,
|
repositoryId: formValues.repositoryId,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -91,7 +90,6 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
|||||||
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
|
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
|
||||||
includePatterns: formValues.includePatterns,
|
includePatterns: formValues.includePatterns,
|
||||||
excludePatterns: formValues.excludePatterns,
|
excludePatterns: formValues.excludePatterns,
|
||||||
excludeIfPresent: formValues.excludeIfPresent,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -162,7 +160,6 @@ export default function CreateBackup({ loaderData }: Route.ComponentProps) {
|
|||||||
<CreateScheduleForm volume={selectedVolume} onSubmit={handleSubmit} formId={formId} />
|
<CreateScheduleForm volume={selectedVolume} onSubmit={handleSubmit} formId={formId} />
|
||||||
<div className="flex justify-end mt-4 gap-2">
|
<div className="flex justify-end mt-4 gap-2">
|
||||||
<Button type="submit" variant="primary" form={formId} loading={createSchedule.isPending}>
|
<Button type="submit" variant="primary" form={formId} loading={createSchedule.isPending}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Bell, Plus } from "lucide-react";
|
import { Bell } from "lucide-react";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -68,7 +68,6 @@ export default function CreateNotification() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form={formId} loading={createNotification.isPending}>
|
<Button type="submit" form={formId} loading={createNotification.isPending}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Create Destination
|
Create Destination
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { getNotificationDestination } from "~/client/api-client/sdk.gen";
|
|||||||
import type { Route } from "./+types/notification-details";
|
import type { Route } from "./+types/notification-details";
|
||||||
import { cn } from "~/client/lib/utils";
|
import { cn } from "~/client/lib/utils";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Bell, Save, TestTube2, Trash2, X } from "lucide-react";
|
import { Bell, TestTube2 } from "lucide-react";
|
||||||
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||||
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
|
import { CreateNotificationForm, type NotificationFormValues } from "../components/create-notification-form";
|
||||||
|
|
||||||
@@ -147,7 +147,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
loading={deleteDestination.isPending}
|
loading={deleteDestination.isPending}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,7 +174,6 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
|||||||
<CreateNotificationForm mode="update" formId={formId} onSubmit={handleSubmit} initialValues={data.config} />
|
<CreateNotificationForm mode="update" formId={formId} onSubmit={handleSubmit} initialValues={data.config} />
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||||
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
<Button type="submit" form={formId} loading={updateDestination.isPending}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,14 +190,8 @@ export default function NotificationDetailsPage({ loaderData }: Route.ComponentP
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<X className="h-4 w-4 mr-2" />
|
<AlertDialogAction onClick={handleConfirmDelete}>Delete</AlertDialogAction>
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={handleConfirmDelete}>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
Delete
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Database, Plus } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -79,8 +79,7 @@ export default function CreateRepository() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form={formId} loading={createRepository.isPending}>
|
<Button type="submit" form={formId} loading={createRepository.isPending}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
Create Repository
|
||||||
Create repository
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function Repositories({ loaderData }: Route.ComponentProps) {
|
|||||||
button={
|
button={
|
||||||
<Button onClick={() => navigate("/repositories/create")}>
|
<Button onClick={() => navigate("/repositories/create")}>
|
||||||
<Plus size={16} className="mr-2" />
|
<Plus size={16} className="mr-2" />
|
||||||
Create repository
|
Create Repository
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { cn } from "~/client/lib/utils";
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||||
import { RepositoryInfoTabContent } from "../tabs/info";
|
import { RepositoryInfoTabContent } from "../tabs/info";
|
||||||
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
|
||||||
import { Loader2, Stethoscope, Trash2, X } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export const handle = {
|
export const handle = {
|
||||||
breadcrumb: (match: Route.MetaArgs) => [
|
breadcrumb: (match: Route.MetaArgs) => [
|
||||||
@@ -149,17 +149,13 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
{doctorMutation.isPending ? (
|
{doctorMutation.isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Running doctor...
|
Running Doctor...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
"Run Doctor"
|
||||||
<Stethoscope className="h-4 w-4 mr-2" />
|
|
||||||
Run doctor
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
|
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteRepo.isPending}>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,15 +184,11 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<div className="flex gap-3 justify-end">
|
<div className="flex gap-3 justify-end">
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
Cancel
|
|
||||||
</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={handleConfirmDelete}
|
onClick={handleConfirmDelete}
|
||||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
Delete repository
|
Delete repository
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,7 +198,7 @@ export default function RepositoryDetailsPage({ loaderData }: Route.ComponentPro
|
|||||||
<AlertDialog open={showDoctorResults} onOpenChange={setShowDoctorResults}>
|
<AlertDialog open={showDoctorResults} onOpenChange={setShowDoctorResults}>
|
||||||
<AlertDialogContent className="max-w-2xl">
|
<AlertDialogContent className="max-w-2xl">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Doctor results</AlertDialogTitle>
|
<AlertDialogTitle>Doctor Results</AlertDialogTitle>
|
||||||
<AlertDialogDescription>Repository doctor operation completed</AlertDialogDescription>
|
<AlertDialogDescription>Repository doctor operation completed</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { Check, Save } from "lucide-react";
|
|
||||||
import { Card } from "~/client/components/ui/card";
|
import { Card } from "~/client/components/ui/card";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
@@ -147,7 +146,6 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||||||
|
|
||||||
<div className="flex justify-end pt-4 border-t">
|
<div className="flex justify-end pt-4 border-t">
|
||||||
<Button type="submit" disabled={!hasChanges || updateMutation.isPending} loading={updateMutation.isPending}>
|
<Button type="submit" disabled={!hasChanges || updateMutation.isPending} loading={updateMutation.isPending}>
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,15 +155,12 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
|||||||
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Update repository</AlertDialogTitle>
|
<AlertDialogTitle>Update Repository</AlertDialogTitle>
|
||||||
<AlertDialogDescription>Are you sure you want to update the repository settings?</AlertDialogDescription>
|
<AlertDialogDescription>Are you sure you want to update the repository settings?</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={confirmUpdate}>
|
<AlertDialogAction onClick={confirmUpdate}>Update</AlertDialogAction>
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
Update
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Database, X } from "lucide-react";
|
import { Database } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { listBackupSchedulesOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listBackupSchedulesOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
import { SnapshotsTable } from "~/client/components/snapshots-table";
|
||||||
@@ -128,7 +128,6 @@ export const RepositorySnapshotsTabContent = ({ repository }: Props) => {
|
|||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<p className="text-muted-foreground">No snapshots match your search.</p>
|
<p className="text-muted-foreground">No snapshots match your search.</p>
|
||||||
<Button onClick={() => setSearchQuery("")} variant="outline" size="sm">
|
<Button onClick={() => setSearchQuery("")} variant="outline" size="sm">
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
Clear search
|
Clear search
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { Download, KeyRound, User, X } from "lucide-react";
|
import { Download, KeyRound, User } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -195,7 +195,6 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" loading={changePassword.isPending} className="mt-4">
|
<Button type="submit" loading={changePassword.isPending} className="mt-4">
|
||||||
<KeyRound className="h-4 w-4 mr-2" />
|
|
||||||
Change Password
|
Change Password
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -253,11 +252,9 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
|
|||||||
setDownloadPassword("");
|
setDownloadPassword("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4 mr-2" />
|
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={downloadResticPassword.isPending}>
|
<Button type="submit" loading={downloadResticPassword.isPending}>
|
||||||
<Download className="h-4 w-4 mr-2" />
|
|
||||||
Download
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { Activity, HeartIcon } from "lucide-react";
|
import { HeartIcon } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { healthCheckVolumeMutation, updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { OnOff } from "~/client/components/onoff";
|
import { OnOff } from "~/client/components/onoff";
|
||||||
@@ -80,7 +80,6 @@ export const HealthchecksCard = ({ volume }: Props) => {
|
|||||||
loading={healthcheck.isPending}
|
loading={healthcheck.isPending}
|
||||||
onClick={() => healthcheck.mutate({ path: { name: volume.name } })}
|
onClick={() => healthcheck.mutate({ path: { name: volume.name } })}
|
||||||
>
|
>
|
||||||
<Activity className="h-4 w-4 mr-2" />
|
|
||||||
Run Health Check
|
Run Health Check
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { HardDrive, Plus } from "lucide-react";
|
import { HardDrive } from "lucide-react";
|
||||||
import { useId } from "react";
|
import { useId } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -73,7 +73,6 @@ export default function CreateVolume() {
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" form={formId} loading={createVolume.isPending}>
|
<Button type="submit" form={formId} loading={createVolume.isPending}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Create Volume
|
Create Volume
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|||||||
import { useNavigate, useParams, useSearchParams } from "react-router";
|
import { useNavigate, useParams, useSearchParams } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Plug, Unplug } from "lucide-react";
|
|
||||||
import { StatusDot } from "~/client/components/status-dot";
|
import { StatusDot } from "~/client/components/status-dot";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||||
@@ -149,7 +148,6 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
|||||||
loading={mountVol.isPending}
|
loading={mountVol.isPending}
|
||||||
className={cn({ hidden: volume.status === "mounted" })}
|
className={cn({ hidden: volume.status === "mounted" })}
|
||||||
>
|
>
|
||||||
<Plug className="h-4 w-4 mr-2" />
|
|
||||||
Mount
|
Mount
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -158,7 +156,6 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
|||||||
loading={unmountVol.isPending}
|
loading={unmountVol.isPending}
|
||||||
className={cn({ hidden: volume.status !== "mounted" })}
|
className={cn({ hidden: volume.status !== "mounted" })}
|
||||||
>
|
>
|
||||||
<Unplug className="h-4 w-4 mr-2" />
|
|
||||||
Unmount
|
Unmount
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
<Button variant="destructive" onClick={() => setShowDeleteConfirm(true)} disabled={deleteVol.isPending}>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Check } from "lucide-react";
|
|
||||||
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
|
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -95,10 +94,7 @@ export const VolumeInfoTabContent = ({ volume, statfs }: Props) => {
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={confirmUpdate}>
|
<AlertDialogAction onClick={confirmUpdate}>Update</AlertDialogAction>
|
||||||
<Check className="h-4 w-4" />
|
|
||||||
Update
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_backup_schedules_table` (
|
|
||||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
||||||
`name` text NOT NULL,
|
|
||||||
`volume_id` integer NOT NULL REFERENCES `volumes_table`(`id`) ON DELETE CASCADE,
|
|
||||||
`repository_id` text NOT NULL REFERENCES `repositories_table`(`id`) ON DELETE CASCADE,
|
|
||||||
`enabled` integer DEFAULT true NOT NULL,
|
|
||||||
`cron_expression` text NOT NULL,
|
|
||||||
`retention_policy` text,
|
|
||||||
`exclude_patterns` text DEFAULT '[]',
|
|
||||||
`include_patterns` text DEFAULT '[]',
|
|
||||||
`last_backup_at` integer,
|
|
||||||
`last_backup_status` text,
|
|
||||||
`last_backup_error` text,
|
|
||||||
`next_backup_at` integer,
|
|
||||||
`created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL,
|
|
||||||
`updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL
|
|
||||||
);--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_backup_schedules_table`(`id`, `name`, `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `created_at`, `updated_at`)
|
|
||||||
SELECT `id`, lower(hex(randomblob(3))), `volume_id`, `repository_id`, `enabled`, `cron_expression`, `retention_policy`, `exclude_patterns`, `include_patterns`, `last_backup_at`, `last_backup_status`, `last_backup_error`, `next_backup_at`, `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_name_unique` ON `backup_schedules_table` (`name`);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `backup_schedules_table` ADD `exclude_if_present` text DEFAULT '[]';
|
|
||||||
@@ -1,807 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "b5b3acff-51d7-45ae-b9d2-4b07a6286fc3",
|
|
||||||
"prevId": "d5a60aea-4490-423e-8725-6ace87a76c9b",
|
|
||||||
"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_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
|
|
||||||
},
|
|
||||||
"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": "'[]'"
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"repositories_table_name_unique": {
|
|
||||||
"name": "repositories_table_name_unique",
|
|
||||||
"columns": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"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,815 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "6",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "729d3ce9-b4b9-41f6-a270-d74c96510238",
|
|
||||||
"prevId": "b5b3acff-51d7-45ae-b9d2-4b07a6286fc3",
|
|
||||||
"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_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
|
|
||||||
},
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"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",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"repositories_table_name_unique": {
|
|
||||||
"name": "repositories_table_name_unique",
|
|
||||||
"columns": [
|
|
||||||
"name"
|
|
||||||
],
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -134,20 +134,6 @@
|
|||||||
"when": 1764794371040,
|
"when": 1764794371040,
|
||||||
"tag": "0018_breezy_invaders",
|
"tag": "0018_breezy_invaders",
|
||||||
"breakpoints": true
|
"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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -33,18 +33,6 @@ async function detectCapabilities(): Promise<SystemCapabilities> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseDockerHost = (dockerHost?: string) => {
|
|
||||||
const match = dockerHost?.match(/^(ssh|http|https):\/\/([^:]+)(?::(\d+))?$/);
|
|
||||||
if (match) {
|
|
||||||
const protocol = match[1] as "ssh" | "http" | "https";
|
|
||||||
const host = match[2];
|
|
||||||
const port = match[3] ? parseInt(match[3], 10) : undefined;
|
|
||||||
return { protocol, host, port };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if Docker is available by:
|
* Checks if Docker is available by:
|
||||||
* 1. Checking if /var/run/docker.sock exists and is accessible
|
* 1. Checking if /var/run/docker.sock exists and is accessible
|
||||||
@@ -52,7 +40,9 @@ export const parseDockerHost = (dockerHost?: string) => {
|
|||||||
*/
|
*/
|
||||||
async function detectDocker(): Promise<boolean> {
|
async function detectDocker(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const docker = new Docker(parseDockerHost(process.env.DOCKER_HOST));
|
await fs.access("/var/run/docker.sock");
|
||||||
|
|
||||||
|
const docker = new Docker();
|
||||||
await docker.ping();
|
await docker.ping();
|
||||||
|
|
||||||
logger.info("Docker capability: enabled");
|
logger.info("Docker capability: enabled");
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { type } from "arktype";
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
|
|
||||||
const envSchema = type({
|
const envSchema = type({
|
||||||
NODE_ENV: type.enumerated("development", "production", "test").default("production"),
|
NODE_ENV: type.enumerated("development", "production", "test").default("development"),
|
||||||
|
SESSION_SECRET: "string?",
|
||||||
}).pipe((s) => ({
|
}).pipe((s) => ({
|
||||||
__prod__: s.NODE_ENV === "production",
|
__prod__: s.NODE_ENV === "production",
|
||||||
environment: s.NODE_ENV,
|
environment: s.NODE_ENV,
|
||||||
|
sessionSecret: s.SESSION_SECRET || "change-me-in-production-please",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const parseConfig = (env: unknown) => {
|
const parseConfig = (env: unknown) => {
|
||||||
|
|||||||
@@ -6,17 +6,26 @@ import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|||||||
import { DATABASE_URL } from "../core/constants";
|
import { DATABASE_URL } from "../core/constants";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { config } from "../core/config";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
await fs.mkdir(path.dirname(DATABASE_URL), { recursive: true });
|
await fs.mkdir(path.dirname(DATABASE_URL), { recursive: true });
|
||||||
|
|
||||||
const sqlite = new Database(DATABASE_URL);
|
const sqlite = new Database(DATABASE_URL);
|
||||||
export const db = drizzle({ client: sqlite, schema });
|
export const db = drizzle({
|
||||||
|
client: sqlite,
|
||||||
|
schema,
|
||||||
|
logger: {
|
||||||
|
logQuery(query, params) {
|
||||||
|
logger.debug(`[Drizzle] ${query} -- [${params.join(",")}]`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const runDbMigrations = () => {
|
export const runDbMigrations = () => {
|
||||||
let migrationsFolder = path.join("/app", "assets", "migrations");
|
let migrationsFolder = path.join("/app", "assets", "migrations");
|
||||||
|
|
||||||
if (!config.__prod__) {
|
const { NODE_ENV } = process.env;
|
||||||
|
if (NODE_ENV !== "production") {
|
||||||
migrationsFolder = path.join("/app", "app", "drizzle");
|
migrationsFolder = path.join("/app", "app", "drizzle");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export type Repository = typeof repositoriesTable.$inferSelect;
|
|||||||
*/
|
*/
|
||||||
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
||||||
id: int().primaryKey({ autoIncrement: true }),
|
id: int().primaryKey({ autoIncrement: true }),
|
||||||
name: text().notNull().unique(),
|
|
||||||
volumeId: int("volume_id")
|
volumeId: int("volume_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => volumesTable.id, { onDelete: "cascade" }),
|
.references(() => volumesTable.id, { onDelete: "cascade" }),
|
||||||
@@ -86,7 +85,6 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
|
|||||||
keepWithinDuration?: string;
|
keepWithinDuration?: string;
|
||||||
}>(),
|
}>(),
|
||||||
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
excludePatterns: text("exclude_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||||
excludeIfPresent: text("exclude_if_present", { mode: "json" }).$type<string[]>().default([]),
|
|
||||||
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
includePatterns: text("include_patterns", { mode: "json" }).$type<string[]>().default([]),
|
||||||
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
lastBackupAt: int("last_backup_at", { mode: "number" }),
|
||||||
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
|
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning">(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as fs from "node:fs/promises";
|
import * as fs from "node:fs/promises";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||||
import { cryptoUtils } from "../../../utils/crypto";
|
|
||||||
import { toMessage } from "../../../utils/errors";
|
import { toMessage } from "../../../utils/errors";
|
||||||
import { logger } from "../../../utils/logger";
|
import { logger } from "../../../utils/logger";
|
||||||
import { getMountForPath } from "../../../utils/mountinfo";
|
import { getMountForPath } from "../../../utils/mountinfo";
|
||||||
@@ -34,12 +33,10 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
const run = async () => {
|
const run = async () => {
|
||||||
await fs.mkdir(path, { recursive: true });
|
await fs.mkdir(path, { recursive: true });
|
||||||
|
|
||||||
const password = await cryptoUtils.decrypt(config.password);
|
|
||||||
|
|
||||||
const source = `//${config.server}/${config.share}`;
|
const source = `//${config.server}/${config.share}`;
|
||||||
const options = [
|
const options = [
|
||||||
`user=${config.username}`,
|
`user=${config.username}`,
|
||||||
`pass=${password}`,
|
`pass=${config.password}`,
|
||||||
`vers=${config.vers}`,
|
`vers=${config.vers}`,
|
||||||
`port=${config.port}`,
|
`port=${config.port}`,
|
||||||
"uid=1000",
|
"uid=1000",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import * as fs from "node:fs/promises";
|
|||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||||
import { cryptoUtils } from "../../../utils/crypto";
|
|
||||||
import { toMessage } from "../../../utils/errors";
|
import { toMessage } from "../../../utils/errors";
|
||||||
import { logger } from "../../../utils/logger";
|
import { logger } from "../../../utils/logger";
|
||||||
import { getMountForPath } from "../../../utils/mountinfo";
|
import { getMountForPath } from "../../../utils/mountinfo";
|
||||||
@@ -50,9 +49,8 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||||||
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
: ["uid=1000", "gid=1000", "file_mode=0664", "dir_mode=0775"];
|
||||||
|
|
||||||
if (config.username && config.password) {
|
if (config.username && config.password) {
|
||||||
const password = await cryptoUtils.decrypt(config.password);
|
|
||||||
const secretsFile = "/etc/davfs2/secrets";
|
const secretsFile = "/etc/davfs2/secrets";
|
||||||
const secretsContent = `${source} ${config.username} ${password}\n`;
|
const secretsContent = `${source} ${config.username} ${config.password}\n`;
|
||||||
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ export type RetentionPolicy = typeof retentionPolicySchema.infer;
|
|||||||
|
|
||||||
const backupScheduleSchema = type({
|
const backupScheduleSchema = type({
|
||||||
id: "number",
|
id: "number",
|
||||||
name: "string",
|
|
||||||
volumeId: "number",
|
volumeId: "number",
|
||||||
repositoryId: "string",
|
repositoryId: "string",
|
||||||
enabled: "boolean",
|
enabled: "boolean",
|
||||||
cronExpression: "string",
|
cronExpression: "string",
|
||||||
retentionPolicy: retentionPolicySchema.or("null"),
|
retentionPolicy: retentionPolicySchema.or("null"),
|
||||||
excludePatterns: "string[] | null",
|
excludePatterns: "string[] | null",
|
||||||
excludeIfPresent: "string[] | null",
|
|
||||||
includePatterns: "string[] | null",
|
includePatterns: "string[] | null",
|
||||||
lastBackupAt: "number | null",
|
lastBackupAt: "number | null",
|
||||||
lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null",
|
lastBackupStatus: "'success' | 'error' | 'in_progress' | 'warning' | null",
|
||||||
@@ -122,14 +120,12 @@ export const getBackupScheduleForVolumeDto = describeRoute({
|
|||||||
* Create a new backup schedule
|
* Create a new backup schedule
|
||||||
*/
|
*/
|
||||||
export const createBackupScheduleBody = type({
|
export const createBackupScheduleBody = type({
|
||||||
name: "1 <= string <= 32",
|
|
||||||
volumeId: "number",
|
volumeId: "number",
|
||||||
repositoryId: "string",
|
repositoryId: "string",
|
||||||
enabled: "boolean",
|
enabled: "boolean",
|
||||||
cronExpression: "string",
|
cronExpression: "string",
|
||||||
retentionPolicy: retentionPolicySchema.optional(),
|
retentionPolicy: retentionPolicySchema.optional(),
|
||||||
excludePatterns: "string[]?",
|
excludePatterns: "string[]?",
|
||||||
excludeIfPresent: "string[]?",
|
|
||||||
includePatterns: "string[]?",
|
includePatterns: "string[]?",
|
||||||
tags: "string[]?",
|
tags: "string[]?",
|
||||||
});
|
});
|
||||||
@@ -160,13 +156,11 @@ export const createBackupScheduleDto = describeRoute({
|
|||||||
* Update a backup schedule
|
* Update a backup schedule
|
||||||
*/
|
*/
|
||||||
export const updateBackupScheduleBody = type({
|
export const updateBackupScheduleBody = type({
|
||||||
name: "(1 <= string <= 32)?",
|
|
||||||
repositoryId: "string",
|
repositoryId: "string",
|
||||||
enabled: "boolean?",
|
enabled: "boolean?",
|
||||||
cronExpression: "string",
|
cronExpression: "string",
|
||||||
retentionPolicy: retentionPolicySchema.optional(),
|
retentionPolicy: retentionPolicySchema.optional(),
|
||||||
excludePatterns: "string[]?",
|
excludePatterns: "string[]?",
|
||||||
excludeIfPresent: "string[]?",
|
|
||||||
includePatterns: "string[]?",
|
includePatterns: "string[]?",
|
||||||
tags: "string[]?",
|
tags: "string[]?",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { and, eq, ne } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import cron from "node-cron";
|
import cron from "node-cron";
|
||||||
import { CronExpressionParser } from "cron-parser";
|
import { CronExpressionParser } from "cron-parser";
|
||||||
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
|
import { NotFoundError, BadRequestError, ConflictError } from "http-errors-enhanced";
|
||||||
@@ -44,7 +44,7 @@ const listSchedules = async () => {
|
|||||||
|
|
||||||
const getSchedule = async (scheduleId: number) => {
|
const getSchedule = async (scheduleId: number) => {
|
||||||
const schedule = await db.query.backupSchedulesTable.findFirst({
|
const schedule = await db.query.backupSchedulesTable.findFirst({
|
||||||
where: eq(backupSchedulesTable.id, scheduleId),
|
where: eq(volumesTable.id, scheduleId),
|
||||||
with: {
|
with: {
|
||||||
volume: true,
|
volume: true,
|
||||||
repository: true,
|
repository: true,
|
||||||
@@ -63,14 +63,6 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||||||
throw new BadRequestError("Invalid cron expression");
|
throw new BadRequestError("Invalid cron expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingName = await db.query.backupSchedulesTable.findFirst({
|
|
||||||
where: eq(backupSchedulesTable.name, data.name),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingName) {
|
|
||||||
throw new ConflictError("A backup schedule with this name already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
const volume = await db.query.volumesTable.findFirst({
|
const volume = await db.query.volumesTable.findFirst({
|
||||||
where: eq(volumesTable.id, data.volumeId),
|
where: eq(volumesTable.id, data.volumeId),
|
||||||
});
|
});
|
||||||
@@ -92,14 +84,12 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
|
|||||||
const [newSchedule] = await db
|
const [newSchedule] = await db
|
||||||
.insert(backupSchedulesTable)
|
.insert(backupSchedulesTable)
|
||||||
.values({
|
.values({
|
||||||
name: data.name,
|
|
||||||
volumeId: data.volumeId,
|
volumeId: data.volumeId,
|
||||||
repositoryId: data.repositoryId,
|
repositoryId: data.repositoryId,
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
cronExpression: data.cronExpression,
|
cronExpression: data.cronExpression,
|
||||||
retentionPolicy: data.retentionPolicy ?? null,
|
retentionPolicy: data.retentionPolicy ?? null,
|
||||||
excludePatterns: data.excludePatterns ?? [],
|
excludePatterns: data.excludePatterns ?? [],
|
||||||
excludeIfPresent: data.excludeIfPresent ?? [],
|
|
||||||
includePatterns: data.includePatterns ?? [],
|
includePatterns: data.includePatterns ?? [],
|
||||||
nextBackupAt: nextBackupAt,
|
nextBackupAt: nextBackupAt,
|
||||||
})
|
})
|
||||||
@@ -125,16 +115,6 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody
|
|||||||
throw new BadRequestError("Invalid cron expression");
|
throw new BadRequestError("Invalid cron expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.name) {
|
|
||||||
const existingName = await db.query.backupSchedulesTable.findFirst({
|
|
||||||
where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingName) {
|
|
||||||
throw new ConflictError("A backup schedule with this name already exists");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const repository = await db.query.repositoriesTable.findFirst({
|
const repository = await db.query.repositoriesTable.findFirst({
|
||||||
where: eq(repositoriesTable.id, data.repositoryId),
|
where: eq(repositoriesTable.id, data.repositoryId),
|
||||||
});
|
});
|
||||||
@@ -247,7 +227,6 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||||||
|
|
||||||
const backupOptions: {
|
const backupOptions: {
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
excludeIfPresent?: string[];
|
|
||||||
include?: string[];
|
include?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
@@ -260,10 +239,6 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
|||||||
backupOptions.exclude = schedule.excludePatterns;
|
backupOptions.exclude = schedule.excludePatterns;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schedule.excludeIfPresent && schedule.excludeIfPresent.length > 0) {
|
|
||||||
backupOptions.excludeIfPresent = schedule.excludeIfPresent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
|
if (schedule.includePatterns && schedule.includePatterns.length > 0) {
|
||||||
backupOptions.include = schedule.includePatterns;
|
backupOptions.include = schedule.includePatterns;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import Docker from "dockerode";
|
|||||||
import { and, eq, ne } from "drizzle-orm";
|
import { and, eq, ne } from "drizzle-orm";
|
||||||
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
import { ConflictError, InternalServerError, NotFoundError } from "http-errors-enhanced";
|
||||||
import slugify from "slugify";
|
import slugify from "slugify";
|
||||||
import { getCapabilities, parseDockerHost } from "../../core/capabilities";
|
import { getCapabilities } from "../../core/capabilities";
|
||||||
import { db } from "../../db/db";
|
import { db } from "../../db/db";
|
||||||
import { volumesTable } from "../../db/schema";
|
import { volumesTable } from "../../db/schema";
|
||||||
import { cryptoUtils } from "../../utils/crypto";
|
|
||||||
import { toMessage } from "../../utils/errors";
|
import { toMessage } from "../../utils/errors";
|
||||||
import { generateShortId } from "../../utils/id";
|
import { generateShortId } from "../../utils/id";
|
||||||
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||||
@@ -20,23 +19,6 @@ import { logger } from "../../utils/logger";
|
|||||||
import { serverEvents } from "../../core/events";
|
import { serverEvents } from "../../core/events";
|
||||||
import type { BackendConfig } from "~/schemas/volumes";
|
import type { BackendConfig } from "~/schemas/volumes";
|
||||||
|
|
||||||
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
|
|
||||||
switch (config.backend) {
|
|
||||||
case "smb":
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
password: await cryptoUtils.encrypt(config.password),
|
|
||||||
};
|
|
||||||
case "webdav":
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
password: config.password ? await cryptoUtils.encrypt(config.password) : undefined,
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listVolumes = async () => {
|
const listVolumes = async () => {
|
||||||
const volumes = await db.query.volumesTable.findMany({});
|
const volumes = await db.query.volumesTable.findMany({});
|
||||||
|
|
||||||
@@ -55,14 +37,13 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shortId = generateShortId();
|
const shortId = generateShortId();
|
||||||
const encryptedConfig = await encryptSensitiveFields(backendConfig);
|
|
||||||
|
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(volumesTable)
|
.insert(volumesTable)
|
||||||
.values({
|
.values({
|
||||||
shortId,
|
shortId,
|
||||||
name: slug,
|
name: slug,
|
||||||
config: encryptedConfig,
|
config: backendConfig,
|
||||||
type: backendConfig.backend,
|
type: backendConfig.backend,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -194,13 +175,11 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => {
|
|||||||
await backend.unmount();
|
await backend.unmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedConfig = volumeData.config ? await encryptSensitiveFields(volumeData.config) : undefined;
|
|
||||||
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(volumesTable)
|
.update(volumesTable)
|
||||||
.set({
|
.set({
|
||||||
name: newName,
|
name: newName,
|
||||||
config: encryptedConfig,
|
config: volumeData.config,
|
||||||
type: volumeData.config?.backend,
|
type: volumeData.config?.backend,
|
||||||
autoRemount: volumeData.autoRemount,
|
autoRemount: volumeData.autoRemount,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -298,8 +277,7 @@ const getContainersUsingVolume = async (name: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const docker = new Docker(parseDockerHost(process.env.DOCKER_HOST));
|
const docker = new Docker();
|
||||||
|
|
||||||
const containers = await docker.listContainers({ all: true });
|
const containers = await docker.listContainers({ all: true });
|
||||||
|
|
||||||
const usingContainers = [];
|
const usingContainers = [];
|
||||||
|
|||||||
@@ -6,26 +6,18 @@ const keyLength = 32;
|
|||||||
const encryptionPrefix = "encv1";
|
const encryptionPrefix = "encv1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a given string is encrypted by looking for the encryption prefix.
|
* Given a string, encrypts it using a randomly generated salt
|
||||||
*/
|
|
||||||
const isEncrypted = (val?: string): boolean => {
|
|
||||||
return typeof val === "string" && val.startsWith(encryptionPrefix);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a string, encrypts it using a randomly generated salt.
|
|
||||||
* Returns the input unchanged if it's empty or already encrypted.
|
|
||||||
*/
|
*/
|
||||||
const encrypt = async (data: string) => {
|
const encrypt = async (data: string) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEncrypted(data)) {
|
if (data.startsWith(encryptionPrefix)) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secret = await Bun.file(RESTIC_PASS_FILE).text();
|
const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
|
||||||
|
|
||||||
const salt = crypto.randomBytes(16);
|
const salt = crypto.randomBytes(16);
|
||||||
const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256");
|
const key = crypto.pbkdf2Sync(secret, salt, 100000, keyLength, "sha256");
|
||||||
@@ -39,15 +31,10 @@ const encrypt = async (data: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an encrypted string, decrypts it using the salt stored in the string.
|
* Given an encrypted string, decrypts it using the salt stored in the string
|
||||||
* Returns the input unchanged if it's not encrypted (for backward compatibility).
|
|
||||||
*/
|
*/
|
||||||
const decrypt = async (encryptedData: string) => {
|
const decrypt = async (encryptedData: string) => {
|
||||||
if (!isEncrypted(encryptedData)) {
|
const secret = await Bun.file(RESTIC_PASS_FILE).text();
|
||||||
return encryptedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = (await Bun.file(RESTIC_PASS_FILE).text()).trim();
|
|
||||||
|
|
||||||
const parts = encryptedData.split(":").slice(1); // Remove prefix
|
const parts = encryptedData.split(":").slice(1); // Remove prefix
|
||||||
const saltHex = parts.shift() as string;
|
const saltHex = parts.shift() as string;
|
||||||
@@ -71,5 +58,4 @@ const decrypt = async (encryptedData: string) => {
|
|||||||
export const cryptoUtils = {
|
export const cryptoUtils = {
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
isEncrypted,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { createLogger, format, transports } from "winston";
|
import { createLogger, format, transports } from "winston";
|
||||||
import { sanitizeSensitiveData } from "./sanitize";
|
import { sanitizeSensitiveData } from "./sanitize";
|
||||||
import { config } from "../core/config";
|
|
||||||
|
|
||||||
const { printf, combine, colorize } = format;
|
const { printf, combine, colorize } = format;
|
||||||
|
|
||||||
const printConsole = printf((info) => `${info.level} > ${info.message}`);
|
const printConsole = printf((info) => `${info.level} > ${info.message}`);
|
||||||
const consoleFormat = combine(colorize(), printConsole);
|
const consoleFormat = combine(colorize(), printConsole);
|
||||||
|
|
||||||
const defaultLevel = config.__prod__ ? "info" : "debug";
|
|
||||||
const winstonLogger = createLogger({
|
const winstonLogger = createLogger({
|
||||||
level: process.env.LOG_LEVEL || defaultLevel,
|
level: "debug",
|
||||||
format: format.json(),
|
format: format.json(),
|
||||||
transports: [new transports.Console({ level: process.env.LOG_LEVEL || defaultLevel, format: consoleFormat })],
|
transports: [new transports.Console({ level: "debug", format: consoleFormat })],
|
||||||
});
|
});
|
||||||
|
|
||||||
const log = (level: "info" | "warn" | "error" | "debug", messages: unknown[]) => {
|
const log = (level: "info" | "warn" | "error" | "debug", messages: unknown[]) => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
|
||||||
import { throttle } from "es-toolkit";
|
import { throttle } from "es-toolkit";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
@@ -235,7 +234,6 @@ const backup = async (
|
|||||||
source: string,
|
source: string,
|
||||||
options?: {
|
options?: {
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
excludeIfPresent?: string[];
|
|
||||||
include?: string[];
|
include?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
compressionMode?: CompressionMode;
|
compressionMode?: CompressionMode;
|
||||||
@@ -263,9 +261,8 @@ const backup = async (
|
|||||||
|
|
||||||
let includeFile: string | null = null;
|
let includeFile: string | null = null;
|
||||||
if (options?.include && options.include.length > 0) {
|
if (options?.include && options.include.length > 0) {
|
||||||
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-"));
|
const tmp = await fs.mkdtemp("restic-include");
|
||||||
includeFile = path.join(tmp, `include.txt`);
|
includeFile = path.join(tmp, `include.txt`);
|
||||||
|
|
||||||
const includePaths = options.include.map((p) => path.join(source, p));
|
const includePaths = options.include.map((p) => path.join(source, p));
|
||||||
|
|
||||||
await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8");
|
await fs.writeFile(includeFile, includePaths.join("\n"), "utf-8");
|
||||||
@@ -281,12 +278,6 @@ const backup = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.excludeIfPresent && options.excludeIfPresent.length > 0) {
|
|
||||||
for (const filename of options.excludeIfPresent) {
|
|
||||||
args.push("--exclude-if-present", filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addCommonArgs(args, env);
|
addCommonArgs(args, env);
|
||||||
|
|
||||||
const logData = throttle((data: string) => {
|
const logData = throttle((data: string) => {
|
||||||
@@ -795,7 +786,7 @@ const cleanupTemporaryKeys = async (config: RepositoryConfig, env: Record<string
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addCommonArgs = (args: string[], env: Record<string, string>) => {
|
const addCommonArgs = (args: string[], env: Record<string, string>) => {
|
||||||
args.push("--json");
|
args.push("--retry-lock", "1m", "--json");
|
||||||
|
|
||||||
if (env._SFTP_SSH_ARGS) {
|
if (env._SFTP_SSH_ARGS) {
|
||||||
args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
|
args.push("-o", `sftp.args=${env._SFTP_SSH_ARGS}`);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.5/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
"defaultBranch": "origin/main",
|
|
||||||
"useIgnoreFile": true
|
"useIgnoreFile": true
|
||||||
},
|
},
|
||||||
"files": {
|
"files": {
|
||||||
|
|||||||
10
bun.lock
10
bun.lock
@@ -32,7 +32,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"es-toolkit": "^1.42.0",
|
"es-toolkit": "^1.42.0",
|
||||||
"hono": "4.10.5",
|
"hono": "^4.10.7",
|
||||||
"hono-openapi": "^1.1.1",
|
"hono-openapi": "^1.1.1",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"isbot": "^5.1.32",
|
"isbot": "^5.1.32",
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-hook-form": "^7.67.0",
|
||||||
"react-router": "^7.10.0",
|
"react-router": "^7.10.0",
|
||||||
"react-router-hono-server": "^2.22.0",
|
"react-router-hono-server": "^2.22.0",
|
||||||
"recharts": "3.5.1",
|
"recharts": "3.5.1",
|
||||||
@@ -770,7 +770,7 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="],
|
"hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
|
||||||
|
|
||||||
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
|
"hono-openapi": ["hono-openapi@1.1.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.1.2", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.8", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-AC3HNhZYPHhnZdSy2Je7GDoTTNxPos6rKRQKVDBbSilY3cWJPqsxRnN6zA4pU7tfxmQEMTqkiLXbw6sAaemB8Q=="],
|
||||||
|
|
||||||
@@ -952,7 +952,7 @@
|
|||||||
|
|
||||||
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
"react-dom": ["react-dom@19.2.1", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.1" } }, "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.68.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-oNN3fjrZ/Xo40SWlHf1yCjlMK417JxoSJVUXQjGdvdRCU07NTFei1i1f8ApUAts+IVh14e4EdakeLEA+BEAs/Q=="],
|
"react-hook-form": ["react-hook-form@7.67.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-E55EOwKJHHIT/I6J9DmQbCWToAYSw9nN5R57MZw9rMtjh+YQreMDxRLfdjfxQbiJ3/qbg3Z02wGzBX4M+5fMtQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
|
"react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="],
|
||||||
|
|
||||||
@@ -1216,6 +1216,8 @@
|
|||||||
|
|
||||||
"react-router-hono-server/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
"react-router-hono-server/@drizzle-team/brocli": ["@drizzle-team/brocli@0.11.0", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
|
||||||
|
|
||||||
|
"react-router-hono-server/hono": ["hono@4.10.5", "", {}, "sha512-h/MXuTkoAK8NG1EfDp0jI1YLf6yGdDnfkebRO2pwEh5+hE3RAJFXkCsnD0vamSiARK4ZrB6MY+o3E/hCnOyHrQ=="],
|
||||||
|
|
||||||
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
|
|||||||
@@ -41,4 +41,3 @@ services:
|
|||||||
- /var/lib/zerobyte:/var/lib/zerobyte:rshared
|
- /var/lib/zerobyte:/var/lib/zerobyte:rshared
|
||||||
- /run/docker/plugins:/run/docker/plugins
|
- /run/docker/plugins:/run/docker/plugins
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ~/.config/rclone:/root/.config/rclone
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"start": "bun ./dist/server/index.js",
|
"start": "bun ./dist/server/index.js",
|
||||||
"tsc": "react-router typegen && tsc",
|
"tsc": "react-router typegen && tsc",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
"lint:ci": "biome ci . --changed --error-on-warnings --no-errors-on-unmatched",
|
"lint:ci": "biome check . --ci",
|
||||||
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
"start:dev": "docker compose down && docker compose up --build zerobyte-dev",
|
||||||
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
"start:prod": "docker compose down && docker compose up --build zerobyte-prod",
|
||||||
"gen:api-client": "openapi-ts",
|
"gen:api-client": "openapi-ts",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.44.7",
|
"drizzle-orm": "^0.44.7",
|
||||||
"es-toolkit": "^1.42.0",
|
"es-toolkit": "^1.42.0",
|
||||||
"hono": "4.10.5",
|
"hono": "^4.10.7",
|
||||||
"hono-openapi": "^1.1.1",
|
"hono-openapi": "^1.1.1",
|
||||||
"http-errors-enhanced": "^4.0.2",
|
"http-errors-enhanced": "^4.0.2",
|
||||||
"isbot": "^5.1.32",
|
"isbot": "^5.1.32",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hook-form": "^7.68.0",
|
"react-hook-form": "^7.67.0",
|
||||||
"react-router": "^7.10.0",
|
"react-router": "^7.10.0",
|
||||||
"react-router-hono-server": "^2.22.0",
|
"react-router-hono-server": "^2.22.0",
|
||||||
"recharts": "3.5.1",
|
"recharts": "3.5.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user