mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
8 Commits
v0.10.1-be
...
altendorfm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c2791102f | ||
|
|
b70f973c12 | ||
|
|
14dadc85e7 | ||
|
|
ff16c6914d | ||
|
|
b333489ae6 | ||
|
|
0efe57b62e | ||
|
|
0b6f64e16d | ||
|
|
eb28667d90 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -74,8 +74,6 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@@ -2,8 +2,11 @@ ARG BUN_VERSION="1.3.1"
|
||||
|
||||
FROM oven/bun:${BUN_VERSION}-alpine AS base
|
||||
|
||||
RUN apk add --no-cache davfs2=1.6.1-r2
|
||||
|
||||
RUN apk add --no-cache \
|
||||
davfs2=1.6.1-r2 \
|
||||
mariadb-client \
|
||||
mysql-client \
|
||||
postgresql-client
|
||||
|
||||
# ------------------------------
|
||||
# DEPENDENCIES
|
||||
@@ -59,9 +62,6 @@ CMD ["bun", "run", "dev"]
|
||||
# ------------------------------
|
||||
FROM oven/bun:${BUN_VERSION} AS builder
|
||||
|
||||
ARG APP_VERSION=dev
|
||||
ENV VITE_APP_VERSION=${APP_VERSION}
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./package.json ./bun.lock ./
|
||||
|
||||
18
README.md
18
README.md
@@ -36,7 +36,7 @@ In order to run Ironmount, you need to have Docker and Docker Compose installed
|
||||
```yaml
|
||||
services:
|
||||
ironmount:
|
||||
image: ghcr.io/nicotsx/ironmount:v0.10
|
||||
image: ghcr.io/nicotsx/ironmount:v0.9
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -46,13 +46,9 @@ services:
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/ironmount:/var/lib/ironmount
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> Do not try to change the location of the bind mount `/var/lib/ironmount` on your host or store it on a network share. You will likely face permission issues and strong performance degradation.
|
||||
|
||||
Then, run the following command to start Ironmount:
|
||||
|
||||
```bash
|
||||
@@ -72,7 +68,7 @@ If you want to track a local directory on the same server where Ironmount is run
|
||||
```diff
|
||||
services:
|
||||
ironmount:
|
||||
image: ghcr.io/nicotsx/ironmount:v0.10
|
||||
image: ghcr.io/nicotsx/ironmount:v0.9
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -82,7 +78,6 @@ services:
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/ironmount:/var/lib/ironmount
|
||||
+ - /path/to/your/directory:/mydata
|
||||
```
|
||||
@@ -138,7 +133,7 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro
|
||||
```diff
|
||||
services:
|
||||
ironmount:
|
||||
image: ghcr.io/nicotsx/ironmount:v0.10
|
||||
image: ghcr.io/nicotsx/ironmount:v0.9
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -148,7 +143,6 @@ Ironmount can use [rclone](https://rclone.org/) to support 40+ cloud storage pro
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/ironmount:/var/lib/ironmount
|
||||
+ - ~/.config/rclone:/root/.config/rclone
|
||||
```
|
||||
@@ -195,7 +189,7 @@ In order to enable this feature, you need to change your bind mount `/var/lib/ir
|
||||
```diff
|
||||
services:
|
||||
ironmount:
|
||||
image: ghcr.io/nicotsx/ironmount:v0.10
|
||||
image: ghcr.io/nicotsx/ironmount:v0.9
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -203,7 +197,6 @@ services:
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- - /var/lib/ironmount:/var/lib/ironmount
|
||||
+ - /var/lib/ironmount:/var/lib/ironmount:rshared
|
||||
```
|
||||
@@ -224,7 +217,7 @@ In order to enable this feature, you need to run Ironmount with several items sh
|
||||
```diff
|
||||
services:
|
||||
ironmount:
|
||||
image: ghcr.io/nicotsx/ironmount:v0.10
|
||||
image: ghcr.io/nicotsx/ironmount:v0.9
|
||||
container_name: ironmount
|
||||
restart: unless-stopped
|
||||
cap_add:
|
||||
@@ -234,7 +227,6 @@ services:
|
||||
devices:
|
||||
- /dev/fuse:/dev/fuse
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- - /var/lib/ironmount:/var/lib/ironmount
|
||||
+ - /var/lib/ironmount:/var/lib/ironmount:rshared
|
||||
+ - /run/docker/plugins:/run/docker/plugins
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||
|
||||
import { client } from '../client.gen';
|
||||
import { browseFilesystem, changePassword, createBackupSchedule, createRepository, createVolume, deleteBackupSchedule, deleteRepository, deleteSnapshot, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getRepository, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, unmountVolume, updateBackupSchedule, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetRepositoryData, GetRepositoryResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
import { browseFilesystem, changePassword, createBackupSchedule, createRepository, createVolume, deleteBackupSchedule, deleteRepository, deleteVolume, doctorRepository, downloadResticPassword, getBackupSchedule, getBackupScheduleForVolume, getContainersUsingVolume, getMe, getRepository, getSnapshotDetails, getStatus, getSystemInfo, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, login, logout, mountVolume, type Options, register, restoreSnapshot, runBackupNow, runForget, stopBackup, testConnection, unmountVolume, updateBackupSchedule, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, ChangePasswordData, ChangePasswordResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteVolumeData, DeleteVolumeResponse, DoctorRepositoryData, DoctorRepositoryResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetContainersUsingVolumeData, GetContainersUsingVolumeResponse, GetMeData, GetMeResponse, GetRepositoryData, GetRepositoryResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, LoginData, LoginResponse, LogoutData, LogoutResponse, MountVolumeData, MountVolumeResponse, RegisterData, RegisterResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, StopBackupData, StopBackupResponse, TestConnectionData, TestConnectionResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
|
||||
/**
|
||||
* Register a new user
|
||||
@@ -460,23 +460,6 @@ export const listSnapshotsOptions = (options: Options<ListSnapshotsData>) => que
|
||||
queryKey: listSnapshotsQueryKey(options)
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a specific snapshot from a repository
|
||||
*/
|
||||
export const deleteSnapshotMutation = (options?: Partial<Options<DeleteSnapshotData>>): UseMutationOptions<DeleteSnapshotResponse, DefaultError, Options<DeleteSnapshotData>> => {
|
||||
const mutationOptions: UseMutationOptions<DeleteSnapshotResponse, DefaultError, Options<DeleteSnapshotData>> = {
|
||||
mutationFn: async (fnOptions) => {
|
||||
const { data } = await deleteSnapshot({
|
||||
...options,
|
||||
...fnOptions,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getSnapshotDetailsQueryKey = (options: Options<GetSnapshotDetailsData>) => createQueryKey("getSnapshotDetails", options);
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, ChangePasswordData, ChangePasswordResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteVolumeData, DeleteVolumeResponses, DoctorRepositoryData, DoctorRepositoryResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetContainersUsingVolumeData, GetContainersUsingVolumeErrors, GetContainersUsingVolumeResponses, GetMeData, GetMeResponses, GetRepositoryData, GetRepositoryResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, LoginData, LoginResponses, LogoutData, LogoutResponses, MountVolumeData, MountVolumeResponses, RegisterData, RegisterResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TestConnectionData, TestConnectionResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
@@ -286,16 +286,6 @@ export const listSnapshots = <ThrowOnError extends boolean = false>(options: Opt
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a specific snapshot from a repository
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -157,6 +157,22 @@ export type ListVolumesResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -164,6 +180,15 @@ export type ListVolumesResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -190,7 +215,7 @@ export type ListVolumesResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
}>;
|
||||
};
|
||||
@@ -203,6 +228,22 @@ export type CreateVolumeData = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -210,6 +251,15 @@ export type CreateVolumeData = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -247,6 +297,22 @@ export type CreateVolumeResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -254,6 +320,15 @@ export type CreateVolumeResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -280,7 +355,7 @@ export type CreateVolumeResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
@@ -293,6 +368,22 @@ export type TestConnectionData = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -300,6 +391,15 @@ export type TestConnectionData = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -390,6 +490,22 @@ export type GetVolumeResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -397,6 +513,15 @@ export type GetVolumeResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -423,7 +548,7 @@ export type GetVolumeResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
@@ -438,6 +563,22 @@ export type UpdateVolumeData = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -445,6 +586,15 @@ export type UpdateVolumeData = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -490,6 +640,22 @@ export type UpdateVolumeResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -497,6 +663,15 @@ export type UpdateVolumeResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -523,7 +698,7 @@ export type UpdateVolumeResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
};
|
||||
@@ -965,27 +1140,6 @@ export type ListSnapshotsResponses = {
|
||||
|
||||
export type ListSnapshotsResponse = ListSnapshotsResponses[keyof ListSnapshotsResponses];
|
||||
|
||||
export type DeleteSnapshotData = {
|
||||
body?: never;
|
||||
path: {
|
||||
name: string;
|
||||
snapshotId: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/repositories/{name}/snapshots/{snapshotId}';
|
||||
};
|
||||
|
||||
export type DeleteSnapshotResponses = {
|
||||
/**
|
||||
* Snapshot deleted successfully
|
||||
*/
|
||||
200: {
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DeleteSnapshotResponse = DeleteSnapshotResponses[keyof DeleteSnapshotResponses];
|
||||
|
||||
export type GetSnapshotDetailsData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -1199,6 +1353,22 @@ export type ListBackupSchedulesResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -1206,6 +1376,15 @@ export type ListBackupSchedulesResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -1232,7 +1411,7 @@ export type ListBackupSchedulesResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
volumeId: number;
|
||||
@@ -1412,6 +1591,22 @@ export type GetBackupScheduleResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -1419,6 +1614,15 @@ export type GetBackupScheduleResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -1445,7 +1649,7 @@ export type GetBackupScheduleResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
volumeId: number;
|
||||
@@ -1606,6 +1810,22 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||
backend: 'directory';
|
||||
path: string;
|
||||
readOnly?: false;
|
||||
} | {
|
||||
backend: 'mariadb';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'mysql';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'nfs';
|
||||
exportPath: string;
|
||||
@@ -1613,6 +1833,15 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||
version: '3' | '4' | '4.1';
|
||||
port?: number;
|
||||
readOnly?: boolean;
|
||||
} | {
|
||||
backend: 'postgres';
|
||||
database: string;
|
||||
host: string;
|
||||
password: string;
|
||||
username: string;
|
||||
dumpFormat?: 'custom' | 'directory' | 'plain';
|
||||
port?: number;
|
||||
dumpOptions?: Array<string>;
|
||||
} | {
|
||||
backend: 'smb';
|
||||
password: string;
|
||||
@@ -1639,7 +1868,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
||||
lastHealthCheck: number;
|
||||
name: string;
|
||||
status: 'error' | 'mounted' | 'unmounted';
|
||||
type: 'directory' | 'nfs' | 'smb' | 'webdav';
|
||||
type: 'directory' | 'mariadb' | 'mysql' | 'nfs' | 'postgres' | 'smb' | 'webdav';
|
||||
updatedAt: number;
|
||||
};
|
||||
volumeId: number;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Link, NavLink } from "react-router";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarHeader,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
} from "~/client/components/ui/sidebar";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
import { APP_VERSION } from "~/client/lib/version";
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -87,15 +85,6 @@ export function AppSidebar() {
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="p-4 border-r border-t border-border/50">
|
||||
<div
|
||||
className={cn("text-xs text-muted-foreground transition-all duration-200", {
|
||||
"opacity-0 w-0 overflow-hidden": state === "collapsed",
|
||||
})}
|
||||
>
|
||||
Version {APP_VERSION}
|
||||
</div>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Calendar, Clock, Database, FolderTree, HardDrive, Trash2 } from "lucide-react";
|
||||
import { Calendar, Clock, Database, FolderTree, HardDrive } from "lucide-react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { ByteSize } from "~/client/components/bytes-size";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/client/components/ui/alert-dialog";
|
||||
import { formatDuration } from "~/utils/utils";
|
||||
import type { ListSnapshotsResponse } from "../api-client";
|
||||
import { deleteSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
|
||||
type Snapshot = ListSnapshotsResponse[number];
|
||||
|
||||
@@ -31,46 +15,12 @@ type Props = {
|
||||
|
||||
export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||
|
||||
const deleteSnapshot = useMutation({
|
||||
...deleteSnapshotMutation(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["listSnapshots"] });
|
||||
setShowDeleteConfirm(false);
|
||||
setSnapshotToDelete(null);
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent, snapshotId: string) => {
|
||||
e.stopPropagation();
|
||||
setSnapshotToDelete(snapshotId);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (snapshotToDelete) {
|
||||
toast.promise(
|
||||
deleteSnapshot.mutateAsync({
|
||||
path: { name: repositoryName, snapshotId: snapshotToDelete },
|
||||
}),
|
||||
{
|
||||
loading: "Deleting snapshot...",
|
||||
success: "Snapshot deleted successfully",
|
||||
error: (error) => parseError(error)?.message || "Failed to delete snapshot",
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRowClick = (snapshotId: string) => {
|
||||
navigate(`/repositories/${repositoryName}/${snapshotId}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="border-t">
|
||||
<TableHeader className="bg-card-header">
|
||||
@@ -80,7 +30,6 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
||||
<TableHead className="uppercase">Size</TableHead>
|
||||
<TableHead className="uppercase hidden md:table-cell text-right">Duration</TableHead>
|
||||
<TableHead className="uppercase hidden text-right lg:table-cell">Paths</TableHead>
|
||||
<TableHead className="uppercase text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -137,43 +86,10 @@ export const SnapshotsTable = ({ snapshots, repositoryName }: Props) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => handleDeleteClick(e, snapshot.short_id)}
|
||||
disabled={deleteSnapshot.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the snapshot and all its data from the
|
||||
repository.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleteSnapshot.isPending}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete snapshot
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cloud, Folder, Server, Share2 } from "lucide-react";
|
||||
import { Cloud, Database, Folder, Server, Share2 } from "lucide-react";
|
||||
import type { BackendType } from "~/schemas/volumes";
|
||||
|
||||
type VolumeIconProps = {
|
||||
@@ -32,6 +32,24 @@ const getIconAndColor = (backend: BackendType) => {
|
||||
color: "text-green-600 dark:text-green-400",
|
||||
label: "WebDAV",
|
||||
};
|
||||
case "mariadb":
|
||||
return {
|
||||
icon: Database,
|
||||
color: "text-teal-600 dark:text-teal-400",
|
||||
label: "MariaDB",
|
||||
};
|
||||
case "mysql":
|
||||
return {
|
||||
icon: Database,
|
||||
color: "text-cyan-600 dark:text-cyan-400",
|
||||
label: "MySQL",
|
||||
};
|
||||
case "postgres":
|
||||
return {
|
||||
icon: Database,
|
||||
color: "text-indigo-600 dark:text-indigo-400",
|
||||
label: "PostgreSQL",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: Folder,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || "dev";
|
||||
@@ -16,7 +16,7 @@ export const clientMiddleware = [authMiddleware];
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Download Recovery Key" },
|
||||
{ title: "Download Recovery Key" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Download your backup recovery key to ensure you can restore your data.",
|
||||
|
||||
@@ -16,7 +16,7 @@ export const clientMiddleware = [authMiddleware];
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Login" },
|
||||
{ title: "Login" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Sign in to your Ironmount account.",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const clientMiddleware = [authMiddleware];
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Onboarding" },
|
||||
{ title: "Onboarding" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Welcome to Ironmount. Create your admin account to get started.",
|
||||
|
||||
@@ -254,8 +254,8 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
||||
<CardHeader>
|
||||
<CardTitle>Backup paths</CardTitle>
|
||||
<CardDescription>
|
||||
Select which folders or files to include in the backup. If no paths are selected, the entire volume will
|
||||
be backed up.
|
||||
Select which folders or files to include in the backup. If no paths are selected, the entire volume will be
|
||||
backed up.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
|
||||
@@ -26,12 +26,10 @@ interface Props {
|
||||
snapshot: Snapshot;
|
||||
repositoryName: string;
|
||||
volume?: Volume;
|
||||
onDeleteSnapshot?: (snapshotId: string) => void;
|
||||
isDeletingSnapshot?: boolean;
|
||||
}
|
||||
|
||||
export const SnapshotFileBrowser = (props: Props) => {
|
||||
const { snapshot, repositoryName, volume, onDeleteSnapshot, isDeletingSnapshot } = props;
|
||||
const { snapshot, repositoryName, volume } = props;
|
||||
|
||||
const isReadOnly = volume?.config && "readOnly" in volume.config && volume.config.readOnly === true;
|
||||
|
||||
@@ -138,7 +136,6 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
<CardTitle>File Browser</CardTitle>
|
||||
<CardDescription>{`Viewing snapshot from ${new Date(snapshot?.time ?? 0).toLocaleString()}`}</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedPaths.size > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -163,18 +160,6 @@ export const SnapshotFileBrowser = (props: Props) => {
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{onDeleteSnapshot && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => onDeleteSnapshot(snapshot.short_id)}
|
||||
disabled={isDeletingSnapshot}
|
||||
loading={isDeletingSnapshot}
|
||||
>
|
||||
{isDeletingSnapshot ? "Deleting..." : "Delete Snapshot"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 overflow-hidden flex flex-col p-0">
|
||||
|
||||
@@ -3,16 +3,6 @@ import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { redirect, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/client/components/ui/alert-dialog";
|
||||
import {
|
||||
getBackupScheduleOptions,
|
||||
runBackupNowMutation,
|
||||
@@ -20,7 +10,6 @@ import {
|
||||
listSnapshotsOptions,
|
||||
updateBackupScheduleMutation,
|
||||
stopBackupMutation,
|
||||
deleteSnapshotMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import { getCronExpression } from "~/utils/utils";
|
||||
@@ -40,7 +29,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Backup Job Details" },
|
||||
{ title: "Backup Job Details" },
|
||||
{
|
||||
name: "description",
|
||||
content: "View and manage backup job configuration, schedule, and snapshots.",
|
||||
@@ -61,8 +50,6 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const formId = useId();
|
||||
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string>();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [snapshotToDelete, setSnapshotToDelete] = useState<string | null>(null);
|
||||
|
||||
const { data: schedule } = useQuery({
|
||||
...getBackupScheduleOptions({ path: { scheduleId: params.id } }),
|
||||
@@ -123,17 +110,6 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
},
|
||||
});
|
||||
|
||||
const deleteSnapshot = useMutation({
|
||||
...deleteSnapshotMutation(),
|
||||
onSuccess: () => {
|
||||
setShowDeleteConfirm(false);
|
||||
setSnapshotToDelete(null);
|
||||
if (selectedSnapshotId === snapshotToDelete) {
|
||||
setSelectedSnapshotId(undefined);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (formValues: BackupScheduleFormValues) => {
|
||||
if (!schedule) return;
|
||||
|
||||
@@ -174,26 +150,6 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteSnapshot = (snapshotId: string) => {
|
||||
setSnapshotToDelete(snapshotId);
|
||||
setShowDeleteConfirm(true);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (snapshotToDelete) {
|
||||
toast.promise(
|
||||
deleteSnapshot.mutateAsync({
|
||||
path: { name: schedule.repository.name, snapshotId: snapshotToDelete },
|
||||
}),
|
||||
{
|
||||
loading: "Deleting snapshot...",
|
||||
success: "Snapshot deleted successfully",
|
||||
error: (error) => parseError(error)?.message || "Failed to delete snapshot",
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
return (
|
||||
<div>
|
||||
@@ -235,32 +191,8 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||
snapshot={selectedSnapshot}
|
||||
repositoryName={schedule.repository.name}
|
||||
volume={schedule.volume}
|
||||
onDeleteSnapshot={handleDeleteSnapshot}
|
||||
isDeletingSnapshot={deleteSnapshot.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete snapshot?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the snapshot and all its data from the
|
||||
repository.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmDelete}
|
||||
disabled={deleteSnapshot.isPending}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete snapshot
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Backup Jobs" },
|
||||
{ title: "Backup Jobs" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Automate volume backups with scheduled jobs and retention policies.",
|
||||
|
||||
@@ -24,7 +24,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Create Backup Job" },
|
||||
{ title: "Create Backup Job" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Create a new automated backup job for your volumes.",
|
||||
|
||||
@@ -17,7 +17,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Create Repository" },
|
||||
{ title: "Create Repository" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Create a new backup repository with encryption and compression.",
|
||||
|
||||
@@ -20,7 +20,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Repositories" },
|
||||
{ title: "Repositories" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Manage your backup repositories with encryption and compression.",
|
||||
|
||||
@@ -36,7 +36,7 @@ export const handle = {
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: `Ironmount - ${params.name}` },
|
||||
{ title: params.name },
|
||||
{
|
||||
name: "description",
|
||||
content: "View repository configuration, status, and snapshots.",
|
||||
|
||||
@@ -17,7 +17,7 @@ export const handle = {
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: `Ironmount - Snapshot ${params.snapshotId}` },
|
||||
{ title: `Snapshot ${params.snapshotId}` },
|
||||
{
|
||||
name: "description",
|
||||
content: "Browse and restore files from a backup snapshot.",
|
||||
|
||||
@@ -30,7 +30,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Settings" },
|
||||
{ title: "Settings" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Manage your account settings and preferences.",
|
||||
|
||||
@@ -6,13 +6,31 @@ import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { cn, slugify } from "~/client/lib/utils";
|
||||
import { deepClean } from "~/utils/object";
|
||||
import { DirectoryBrowser } from "./directory-browser";
|
||||
import { Button } from "./ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "./ui/form";
|
||||
import { Input } from "./ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
import { volumeConfigSchema } from "~/schemas/volumes";
|
||||
import { testConnectionMutation } from "../api-client/@tanstack/react-query.gen";
|
||||
import { testConnectionMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "~/client/components/ui/form";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "~/client/components/ui/select";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { DirectoryBrowser } from "~/client/components/directory-browser";
|
||||
|
||||
const SUPPORTS_CONNECTION_TEST = ["nfs", "smb", "webdav", "mariadb", "mysql", "postgres"];
|
||||
|
||||
export const formSchema = type({
|
||||
name: "2<=string<=32",
|
||||
@@ -35,6 +53,9 @@ const defaultValuesForType = {
|
||||
nfs: { backend: "nfs" as const, port: 2049, version: "4.1" as const },
|
||||
smb: { backend: "smb" as const, port: 445, vers: "3.0" as const },
|
||||
webdav: { backend: "webdav" as const, port: 80, ssl: false },
|
||||
mariadb: { backend: "mariadb" as const, port: 3306 },
|
||||
mysql: { backend: "mysql" as const, port: 3306 },
|
||||
postgres: { backend: "postgres" as const, port: 5432, dumpFormat: "custom" as const },
|
||||
};
|
||||
|
||||
export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, formId, loading, className }: Props) => {
|
||||
@@ -81,7 +102,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
const handleTestConnection = async () => {
|
||||
const formValues = getValues();
|
||||
|
||||
if (formValues.backend === "nfs" || formValues.backend === "smb" || formValues.backend === "webdav") {
|
||||
if (SUPPORTS_CONNECTION_TEST.includes(formValues.backend)) {
|
||||
testBackendConnection.mutate({
|
||||
body: { config: formValues },
|
||||
});
|
||||
@@ -121,15 +142,26 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
<FormLabel>Backend</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-[280px]">
|
||||
<SelectValue placeholder="Select a backend" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Network Storage</SelectLabel>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
<SelectItem value="webdav">WebDAV</SelectItem>
|
||||
</SelectGroup>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Databases</SelectLabel>
|
||||
<SelectItem value="mariadb">MariaDB</SelectItem>
|
||||
<SelectItem value="mysql">MySQL</SelectItem>
|
||||
<SelectItem value="postgres">PostgreSQL</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Choose the storage backend for this volume.</FormDescription>
|
||||
@@ -536,6 +568,258 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedBackend === "mariadb" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="localhost" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>MariaDB server hostname or IP address.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
defaultValue={3306}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="3306" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>MariaDB server port (default: 3306).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Database user with backup privileges.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Password for database authentication.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="database"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="myapp_production" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Name of the database to backup.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedBackend === "mysql" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="localhost" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>MySQL server hostname or IP address.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
defaultValue={3306}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="3306" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>MySQL server port (default: 3306).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="root" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Database user with backup privileges.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Password for database authentication.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="database"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="myapp_production" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Name of the database to backup.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedBackend === "postgres" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="host"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="localhost" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>PostgreSQL server hostname or IP address.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
defaultValue={5432}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="5432" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>PostgreSQL server port (default: 5432).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="postgres" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Database user with backup privileges.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="••••••••" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Password for database authentication.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="database"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Database</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="myapp_production" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Name of the database to backup.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dumpFormat"
|
||||
defaultValue="custom"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Dump Format</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value || "custom"}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select dump format" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="custom">Custom (Compressed)</SelectItem>
|
||||
<SelectItem value="plain">Plain SQL</SelectItem>
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>Format for database dumps (custom recommended).</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchedBackend !== "directory" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -4,12 +4,12 @@ import { useId } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { createVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||
import { parseError } from "~/client/lib/errors";
|
||||
import type { Route } from "./+types/create-volume";
|
||||
import { Alert, AlertDescription } from "~/client/components/ui/alert";
|
||||
import { CreateVolumeForm, type FormValues } from "../components/create-volume-form";
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: () => [{ label: "Volumes", href: "/volumes" }, { label: "Create" }],
|
||||
@@ -17,7 +17,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Create Volume" },
|
||||
{ title: "Create Volume" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Create a new storage volume with automatic mounting and health checks.",
|
||||
|
||||
@@ -37,7 +37,7 @@ export const handle = {
|
||||
|
||||
export function meta({ params }: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: `Ironmount - ${params.name}` },
|
||||
{ title: params.name },
|
||||
{
|
||||
name: "description",
|
||||
content: "View and manage volume details, configuration, and files.",
|
||||
@@ -119,6 +119,8 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
const { volume, statfs } = data;
|
||||
const dockerAvailable = capabilities.docker;
|
||||
|
||||
const isDatabaseVolume = ["mariadb", "mysql", "postgres"].includes(volume.config.backend);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start xs:items-center xs:flex-row xs:justify-between">
|
||||
@@ -152,7 +154,9 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
<Tabs value={activeTab} onValueChange={(value) => setSearchParams({ tab: value })} className="mt-4">
|
||||
<TabsList className="mb-2">
|
||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||
<TabsTrigger value="files">Files</TabsTrigger>
|
||||
<TabsTrigger disabled={isDatabaseVolume} value="files">
|
||||
Files
|
||||
</TabsTrigger>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<TabsTrigger disabled={!dockerAvailable} value="docker">
|
||||
@@ -167,9 +171,11 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {
|
||||
<TabsContent value="info">
|
||||
<VolumeInfoTabContent volume={volume} statfs={statfs} />
|
||||
</TabsContent>
|
||||
{!isDatabaseVolume && (
|
||||
<TabsContent value="files">
|
||||
<FilesTabContent volume={volume} />
|
||||
</TabsContent>
|
||||
)}
|
||||
{dockerAvailable && (
|
||||
<TabsContent value="docker">
|
||||
<DockerTabContent volume={volume} />
|
||||
|
||||
@@ -20,7 +20,7 @@ export const handle = {
|
||||
|
||||
export function meta(_: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Ironmount - Volumes" },
|
||||
{ title: "Volumes" },
|
||||
{
|
||||
name: "description",
|
||||
content: "Create, manage, monitor, and automate your Docker volumes with ease.",
|
||||
@@ -109,6 +109,10 @@ export default function Volumes({ loaderData }: Route.ComponentProps) {
|
||||
<SelectItem value="directory">Directory</SelectItem>
|
||||
<SelectItem value="nfs">NFS</SelectItem>
|
||||
<SelectItem value="smb">SMB</SelectItem>
|
||||
<SelectItem value="webdav">WebDAV</SelectItem>
|
||||
<SelectItem value="mariadb">MariaDB</SelectItem>
|
||||
<SelectItem value="mysql">MySQL</SelectItem>
|
||||
<SelectItem value="postgres">PostgreSQL</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(searchQuery || statusFilter || backendFilter) && (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CreateVolumeForm, type FormValues } from "~/client/components/create-volume-form";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -17,6 +16,7 @@ import type { StatFs, Volume } from "~/client/lib/types";
|
||||
import { HealthchecksCard } from "../components/healthchecks-card";
|
||||
import { StorageChart } from "../components/storage-chart";
|
||||
import { updateVolumeMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { CreateVolumeForm, type FormValues } from "../components/create-volume-form";
|
||||
|
||||
type Props = {
|
||||
volume: Volume;
|
||||
|
||||
@@ -5,6 +5,9 @@ export const BACKEND_TYPES = {
|
||||
smb: "smb",
|
||||
directory: "directory",
|
||||
webdav: "webdav",
|
||||
mariadb: "mariadb",
|
||||
mysql: "mysql",
|
||||
postgres: "postgres",
|
||||
} as const;
|
||||
|
||||
export type BackendType = keyof typeof BACKEND_TYPES;
|
||||
@@ -47,7 +50,47 @@ export const webdavConfigSchema = type({
|
||||
ssl: "boolean?",
|
||||
});
|
||||
|
||||
export const volumeConfigSchema = nfsConfigSchema.or(smbConfigSchema).or(webdavConfigSchema).or(directoryConfigSchema);
|
||||
export const mariadbConfigSchema = type({
|
||||
backend: "'mariadb'",
|
||||
host: "string",
|
||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(3306),
|
||||
username: "string",
|
||||
password: "string",
|
||||
database: "string",
|
||||
dumpOptions: "string[]?",
|
||||
readOnly: "false?",
|
||||
});
|
||||
|
||||
export const mysqlConfigSchema = type({
|
||||
backend: "'mysql'",
|
||||
host: "string",
|
||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(3306),
|
||||
username: "string",
|
||||
password: "string",
|
||||
database: "string",
|
||||
dumpOptions: "string[]?",
|
||||
readOnly: "false?",
|
||||
});
|
||||
|
||||
export const postgresConfigSchema = type({
|
||||
backend: "'postgres'",
|
||||
host: "string",
|
||||
port: type("string.integer").or(type("number")).to("1 <= number <= 65535").default(5432),
|
||||
username: "string",
|
||||
password: "string",
|
||||
database: "string",
|
||||
dumpFormat: type("'plain' | 'custom' | 'directory'").default("custom"),
|
||||
dumpOptions: "string[]?",
|
||||
readOnly: "false?",
|
||||
});
|
||||
|
||||
export const volumeConfigSchema = nfsConfigSchema
|
||||
.or(smbConfigSchema)
|
||||
.or(webdavConfigSchema)
|
||||
.or(directoryConfigSchema)
|
||||
.or(mariadbConfigSchema)
|
||||
.or(mysqlConfigSchema)
|
||||
.or(postgresConfigSchema);
|
||||
|
||||
export type BackendConfig = typeof volumeConfigSchema.infer;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import { volumeService } from "../modules/volumes/volume.service";
|
||||
import { readMountInfo } from "../utils/mountinfo";
|
||||
import { getVolumePath } from "../modules/volumes/helpers";
|
||||
import { createVolumeBackend } from "../modules/backends/backend";
|
||||
import { logger } from "../utils/logger";
|
||||
import { executeUnmount } from "../modules/backends/utils/backend-utils";
|
||||
import { toMessage } from "../utils/errors";
|
||||
@@ -16,7 +16,11 @@ export class CleanupDanglingMountsJob extends Job {
|
||||
|
||||
for (const mount of allSystemMounts) {
|
||||
if (mount.mountPoint.includes("ironmount") && mount.mountPoint.endsWith("_data")) {
|
||||
const matchingVolume = allVolumes.find((v) => getVolumePath(v) === mount.mountPoint);
|
||||
const matchingVolume = allVolumes.find((v) => {
|
||||
const backend = createVolumeBackend(v);
|
||||
return backend.getVolumePath() === mount.mountPoint;
|
||||
});
|
||||
|
||||
if (!matchingVolume) {
|
||||
logger.info(`Found dangling mount at ${mount.mountPoint}, attempting to unmount...`);
|
||||
await executeUnmount(mount.mountPoint).catch((err) => {
|
||||
@@ -36,7 +40,10 @@ export class CleanupDanglingMountsJob extends Job {
|
||||
|
||||
for (const dir of allIronmountDirs) {
|
||||
const volumePath = `${VOLUME_MOUNT_BASE}/${dir}/_data`;
|
||||
const matchingVolume = allVolumes.find((v) => getVolumePath(v) === volumePath);
|
||||
const matchingVolume = allVolumes.find((v) => {
|
||||
const backend = createVolumeBackend(v);
|
||||
return backend.getVolumePath() === volumePath;
|
||||
});
|
||||
if (!matchingVolume) {
|
||||
const fullPath = path.join(VOLUME_MOUNT_BASE, dir);
|
||||
logger.info(`Found dangling mount directory at ${fullPath}, attempting to remove...`);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { BackendStatus } from "~/schemas/volumes";
|
||||
import type { Volume } from "../../db/schema";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import { makeDirectoryBackend } from "./directory/directory-backend";
|
||||
import { makeNfsBackend } from "./nfs/nfs-backend";
|
||||
import { makeSmbBackend } from "./smb/smb-backend";
|
||||
import { makeWebdavBackend } from "./webdav/webdav-backend";
|
||||
import { makeMariaDBBackend } from "./mariadb/mariadb-backend";
|
||||
import { makeMySQLBackend } from "./mysql/mysql-backend";
|
||||
import { makePostgresBackend } from "./postgres/postgres-backend";
|
||||
|
||||
type OperationResult = {
|
||||
error?: string;
|
||||
@@ -15,23 +17,35 @@ export type VolumeBackend = {
|
||||
mount: () => Promise<OperationResult>;
|
||||
unmount: () => Promise<OperationResult>;
|
||||
checkHealth: () => Promise<OperationResult>;
|
||||
getVolumePath: () => string;
|
||||
getBackupPath: () => Promise<string>;
|
||||
};
|
||||
|
||||
export const createVolumeBackend = (volume: Volume): VolumeBackend => {
|
||||
const path = getVolumePath(volume);
|
||||
|
||||
switch (volume.config.backend) {
|
||||
case "nfs": {
|
||||
return makeNfsBackend(volume.config, path);
|
||||
return makeNfsBackend(volume.config, volume.name);
|
||||
}
|
||||
case "smb": {
|
||||
return makeSmbBackend(volume.config, path);
|
||||
return makeSmbBackend(volume.config, volume.name);
|
||||
}
|
||||
case "directory": {
|
||||
return makeDirectoryBackend(volume.config, path);
|
||||
return makeDirectoryBackend(volume.config, volume.name);
|
||||
}
|
||||
case "webdav": {
|
||||
return makeWebdavBackend(volume.config, path);
|
||||
return makeWebdavBackend(volume.config, volume.name);
|
||||
}
|
||||
case "mariadb": {
|
||||
return makeMariaDBBackend(volume.config);
|
||||
}
|
||||
case "mysql": {
|
||||
return makeMySQLBackend(volume.config);
|
||||
}
|
||||
case "postgres": {
|
||||
return makePostgresBackend(volume.config);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported backend type: ${(volume.config as any).backend}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +52,18 @@ const checkHealth = async (config: BackendConfig) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getVolumePath = (config: BackendConfig): string => {
|
||||
if (config.backend !== "directory") {
|
||||
throw new Error("Invalid backend type");
|
||||
}
|
||||
|
||||
return config.path;
|
||||
};
|
||||
|
||||
export const makeDirectoryBackend = (config: BackendConfig, volumePath: string): VolumeBackend => ({
|
||||
mount: () => mount(config, volumePath),
|
||||
unmount,
|
||||
checkHealth: () => checkHealth(config),
|
||||
getVolumePath: () => getVolumePath(config),
|
||||
getBackupPath: async () => getVolumePath(config),
|
||||
});
|
||||
|
||||
81
app/server/modules/backends/mariadb/mariadb-backend.ts
Normal file
81
app/server/modules/backends/mariadb/mariadb-backend.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
import { $ } from "bun";
|
||||
|
||||
const checkHealth = async (config: BackendConfig) => {
|
||||
if (config.backend !== "mariadb") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`Testing MariaDB connection to: ${config.host}:${config.port}`);
|
||||
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--user=${config.username}`,
|
||||
`--database=${config.database}`,
|
||||
"--skip-ssl",
|
||||
"--execute=SELECT 1",
|
||||
];
|
||||
|
||||
const env = {
|
||||
MYSQL_PWD: config.password,
|
||||
};
|
||||
|
||||
await $`mariadb ${args.join(" ")}`.env(env);
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
} catch (error) {
|
||||
logger.error("MariaDB health check failed:", error);
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const getBackupPath = async (config: BackendConfig) => {
|
||||
const dumpDir = await fs.mkdtemp(`/tmp/ironmount-mariadb-`);
|
||||
|
||||
if (config.backend !== "mariadb") {
|
||||
throw new Error("Invalid backend type for MariaDB dump");
|
||||
}
|
||||
|
||||
logger.info(`Starting MariaDB dump for database: ${config.database}`);
|
||||
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--user=${config.username}`,
|
||||
`--skip-ssl`,
|
||||
`--single-transaction`,
|
||||
`--quick`,
|
||||
`--lock-tables=false`,
|
||||
...(config.dumpOptions || []),
|
||||
config.database,
|
||||
];
|
||||
|
||||
const env = {
|
||||
MYSQL_PWD: config.password,
|
||||
};
|
||||
|
||||
const result = await $`mariadb-dump ${args}`.env(env).nothrow();
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`mariadb-dump failed with exit code ${result.exitCode}: ${result.stderr}`);
|
||||
}
|
||||
|
||||
await fs.writeFile(`${dumpDir}/dump.sql`, result.stdout);
|
||||
logger.info(`MariaDB dump completed: ${dumpDir}/dump.sql`);
|
||||
|
||||
return `${dumpDir}/dump.sql`;
|
||||
};
|
||||
|
||||
export const makeMariaDBBackend = (config: BackendConfig): VolumeBackend => ({
|
||||
mount: () => Promise.resolve({ status: BACKEND_STATUS.mounted }),
|
||||
unmount: () => Promise.resolve({ status: BACKEND_STATUS.unmounted }),
|
||||
checkHealth: () => checkHealth(config),
|
||||
getVolumePath: () => "/tmp",
|
||||
getBackupPath: () => getBackupPath(config),
|
||||
});
|
||||
76
app/server/modules/backends/mysql/mysql-backend.ts
Normal file
76
app/server/modules/backends/mysql/mysql-backend.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
import { $ } from "bun";
|
||||
|
||||
const checkHealth = async (config: BackendConfig) => {
|
||||
if (config.backend !== "mysql") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
logger.debug(`Testing MySQL connection to: ${config.host}:${config.port}`);
|
||||
try {
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--user=${config.username}`,
|
||||
`--database=${config.database}`,
|
||||
"--skip-ssl",
|
||||
"--execute=SELECT 1",
|
||||
];
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
MYSQL_PWD: config.password,
|
||||
};
|
||||
|
||||
await $`mysql ${args.join(" ")}`.env(env);
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
} catch (error) {
|
||||
logger.error("MySQL health check failed:", error);
|
||||
return { status: BACKEND_STATUS.error, error: toMessage(error) };
|
||||
}
|
||||
};
|
||||
|
||||
const getBackupPath = async (config: BackendConfig) => {
|
||||
if (config.backend !== "mysql") {
|
||||
throw new Error("Invalid backend type");
|
||||
}
|
||||
|
||||
logger.info(`Starting MySQL dump for database: ${config.database}`);
|
||||
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--user=${config.username}`,
|
||||
`--skip-ssl`,
|
||||
`--single-transaction`,
|
||||
`--quick`,
|
||||
`--lock-tables=false`,
|
||||
...(config.dumpOptions || []),
|
||||
config.database,
|
||||
];
|
||||
|
||||
const env = {
|
||||
MYSQL_PWD: config.password,
|
||||
};
|
||||
|
||||
const result = await $`mysql ${args}`.env(env).nothrow();
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`MySQL dump failed: ${result.stderr}`);
|
||||
}
|
||||
|
||||
console.log(result.stdout);
|
||||
|
||||
return "Nothing for now";
|
||||
};
|
||||
|
||||
export const makeMySQLBackend = (config: BackendConfig): VolumeBackend => ({
|
||||
mount: () => Promise.resolve({ status: BACKEND_STATUS.mounted }),
|
||||
unmount: () => Promise.resolve({ status: BACKEND_STATUS.unmounted }),
|
||||
checkHealth: () => checkHealth(config),
|
||||
getVolumePath: () => "/tmp",
|
||||
getBackupPath: () => getBackupPath(config),
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { OPERATION_TIMEOUT, VOLUME_MOUNT_BASE } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
@@ -9,7 +9,8 @@ import type { VolumeBackend } from "../backend";
|
||||
import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
const mount = async (config: BackendConfig, name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
logger.debug(`Mounting volume ${path}...`);
|
||||
|
||||
if (config.backend !== "nfs") {
|
||||
@@ -22,13 +23,13 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
return { status: BACKEND_STATUS.error, error: "NFS mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
const { status } = await checkHealth(name, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
await unmount(name);
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
@@ -57,7 +58,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
const unmount = async (name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("NFS unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "NFS unmounting is only supported on Linux hosts." };
|
||||
@@ -87,7 +90,9 @@ const unmount = async (path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const checkHealth = async (name: string, readOnly: boolean) => {
|
||||
const path = getVolumePath(name);
|
||||
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of NFS volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
@@ -114,8 +119,14 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const makeNfsBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
const getVolumePath = (name: string) => {
|
||||
return `${VOLUME_MOUNT_BASE}/${name}/_data`;
|
||||
};
|
||||
|
||||
export const makeNfsBackend = (config: BackendConfig, name: string): VolumeBackend => ({
|
||||
mount: () => mount(config, name),
|
||||
unmount: () => unmount(name),
|
||||
checkHealth: () => checkHealth(name, config.readOnly ?? false),
|
||||
getVolumePath: () => getVolumePath(name),
|
||||
getBackupPath: async () => getVolumePath(name),
|
||||
});
|
||||
|
||||
80
app/server/modules/backends/postgres/postgres-backend.ts
Normal file
80
app/server/modules/backends/postgres/postgres-backend.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import type { VolumeBackend } from "../backend";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
import { $ } from "bun";
|
||||
|
||||
const checkHealth = async (config: BackendConfig) => {
|
||||
if (config.backend !== "postgres") {
|
||||
return { status: BACKEND_STATUS.error, error: "Invalid backend type" };
|
||||
}
|
||||
|
||||
if (config.backend !== "postgres") {
|
||||
throw new Error("Invalid backend type for PostgreSQL connection test");
|
||||
}
|
||||
|
||||
logger.debug(`Testing PostgreSQL connection to: ${config.host}:${config.port}`);
|
||||
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--username=${config.username}`,
|
||||
`--dbname=${config.database}`,
|
||||
"--command=SELECT 1",
|
||||
"--no-password",
|
||||
];
|
||||
|
||||
const env = {
|
||||
PGPASSWORD: config.password,
|
||||
PGSSLMODE: "disable",
|
||||
};
|
||||
|
||||
logger.debug(`Running psql with args: ${args.join(" ")}`);
|
||||
const res = await $`psql ${args}`.env(env).nothrow();
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return { status: BACKEND_STATUS.error, error: res.stderr.toString() };
|
||||
}
|
||||
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
};
|
||||
|
||||
const getBackupPath = async (config: BackendConfig) => {
|
||||
if (config.backend !== "postgres") {
|
||||
throw new Error("Invalid backend type for PostgreSQL dump");
|
||||
}
|
||||
|
||||
const dumpDir = await fs.mkdtemp(`/tmp/ironmount-postgres-`);
|
||||
const outputPath = `${dumpDir}/${config.dumpFormat === "plain" ? "dump.sql" : "dump.dump"}`;
|
||||
|
||||
logger.info(`Starting PostgreSQL dump for database: ${config.database}`);
|
||||
|
||||
const args = [
|
||||
`--host=${config.host}`,
|
||||
`--port=${config.port}`,
|
||||
`--username=${config.username}`,
|
||||
`--dbname=${config.database}`,
|
||||
`--format=${config.dumpFormat}`,
|
||||
`--file=${outputPath}`,
|
||||
"--no-password",
|
||||
...(config.dumpOptions || []),
|
||||
];
|
||||
|
||||
const env = {
|
||||
PGPASSWORD: config.password,
|
||||
PGSSLMODE: "disable",
|
||||
};
|
||||
|
||||
await $`pg_dump ${args}`.env(env);
|
||||
|
||||
return outputPath;
|
||||
};
|
||||
|
||||
export const makePostgresBackend = (config: BackendConfig): VolumeBackend => ({
|
||||
mount: () => Promise.resolve({ status: "mounted" }),
|
||||
unmount: () => Promise.resolve({ status: "unmounted" }),
|
||||
checkHealth: () => checkHealth(config),
|
||||
getVolumePath: () => "/tmp",
|
||||
getBackupPath: () => getBackupPath(config),
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { OPERATION_TIMEOUT, VOLUME_MOUNT_BASE } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
@@ -9,7 +9,8 @@ import type { VolumeBackend } from "../backend";
|
||||
import { createTestFile, executeMount, executeUnmount } from "../utils/backend-utils";
|
||||
import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
const mount = async (config: BackendConfig, name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
logger.debug(`Mounting SMB volume ${path}...`);
|
||||
|
||||
if (config.backend !== "smb") {
|
||||
@@ -22,13 +23,13 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
return { status: BACKEND_STATUS.error, error: "SMB mounting is only supported on Linux hosts." };
|
||||
}
|
||||
|
||||
const { status } = await checkHealth(path, config.readOnly ?? false);
|
||||
const { status } = await checkHealth(name, config.readOnly ?? false);
|
||||
if (status === "mounted") {
|
||||
return { status: BACKEND_STATUS.mounted };
|
||||
}
|
||||
|
||||
logger.debug(`Trying to unmount any existing mounts at ${path} before mounting...`);
|
||||
await unmount(path);
|
||||
await unmount(name);
|
||||
|
||||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
@@ -70,7 +71,9 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
const unmount = async (name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("SMB unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "SMB unmounting is only supported on Linux hosts." };
|
||||
@@ -100,7 +103,9 @@ const unmount = async (path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const checkHealth = async (name: string, readOnly: boolean) => {
|
||||
const path = getVolumePath(name);
|
||||
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of SMB volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
@@ -127,8 +132,14 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const makeSmbBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
const getVolumePath = (name: string) => {
|
||||
return `${VOLUME_MOUNT_BASE}/${name}/_data`;
|
||||
};
|
||||
|
||||
export const makeSmbBackend = (config: BackendConfig, name: string): VolumeBackend => ({
|
||||
mount: () => mount(config, name),
|
||||
unmount: () => unmount(name),
|
||||
checkHealth: () => checkHealth(name, config.readOnly ?? false),
|
||||
getVolumePath: () => getVolumePath(name),
|
||||
getBackupPath: async () => getVolumePath(name),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { execFile as execFileCb } from "node:child_process";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { OPERATION_TIMEOUT, VOLUME_MOUNT_BASE } from "../../../core/constants";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
@@ -13,7 +13,8 @@ import { BACKEND_STATUS, type BackendConfig } from "~/schemas/volumes";
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
const mount = async (config: BackendConfig, path: string) => {
|
||||
const mount = async (config: BackendConfig, name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
logger.debug(`Mounting WebDAV volume ${path}...`);
|
||||
|
||||
if (config.backend !== "webdav") {
|
||||
@@ -104,7 +105,8 @@ const mount = async (config: BackendConfig, path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unmount = async (path: string) => {
|
||||
const unmount = async (name: string) => {
|
||||
const path = getVolumePath(name);
|
||||
if (os.platform() !== "linux") {
|
||||
logger.error("WebDAV unmounting is only supported on Linux hosts.");
|
||||
return { status: BACKEND_STATUS.error, error: "WebDAV unmounting is only supported on Linux hosts." };
|
||||
@@ -134,7 +136,9 @@ const unmount = async (path: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
const checkHealth = async (name: string, readOnly: boolean) => {
|
||||
const path = getVolumePath(name);
|
||||
|
||||
const run = async () => {
|
||||
logger.debug(`Checking health of WebDAV volume at ${path}...`);
|
||||
await fs.access(path);
|
||||
@@ -161,8 +165,14 @@ const checkHealth = async (path: string, readOnly: boolean) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const makeWebdavBackend = (config: BackendConfig, path: string): VolumeBackend => ({
|
||||
mount: () => mount(config, path),
|
||||
unmount: () => unmount(path),
|
||||
checkHealth: () => checkHealth(path, config.readOnly ?? false),
|
||||
const getVolumePath = (name: string) => {
|
||||
return `${VOLUME_MOUNT_BASE}/${name}/_data`;
|
||||
};
|
||||
|
||||
export const makeWebdavBackend = (config: BackendConfig, name: string): VolumeBackend => ({
|
||||
mount: () => mount(config, name),
|
||||
unmount: () => unmount(name),
|
||||
checkHealth: () => checkHealth(name, config.readOnly ?? false),
|
||||
getVolumePath: () => getVolumePath(name),
|
||||
getBackupPath: async () => getVolumePath(name),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { db } from "../../db/db";
|
||||
import { backupSchedulesTable, repositoriesTable, volumesTable } from "../../db/schema";
|
||||
import { restic } from "../../utils/restic";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
import type { CreateBackupScheduleBody, UpdateBackupScheduleBody } from "./backups.dto";
|
||||
import { toMessage } from "../../utils/errors";
|
||||
import { serverEvents } from "../../core/events";
|
||||
@@ -17,7 +17,7 @@ const calculateNextRun = (cronExpression: string): number => {
|
||||
try {
|
||||
const interval = CronExpressionParser.parse(cronExpression, {
|
||||
currentDate: new Date(),
|
||||
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
tz: "UTC",
|
||||
});
|
||||
|
||||
return interval.next().getTime();
|
||||
@@ -206,7 +206,8 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
||||
runningBackups.set(scheduleId, abortController);
|
||||
|
||||
try {
|
||||
const volumePath = getVolumePath(volume);
|
||||
const backend = createVolumeBackend(volume);
|
||||
const backupPath = await backend.getBackupPath();
|
||||
|
||||
const backupOptions: {
|
||||
exclude?: string[];
|
||||
@@ -226,7 +227,7 @@ const executeBackup = async (scheduleId: number, manual = false) => {
|
||||
backupOptions.include = schedule.includePatterns;
|
||||
}
|
||||
|
||||
await restic.backup(repository.config, volumePath, {
|
||||
await restic.backup(repository.config, backupPath, {
|
||||
...backupOptions,
|
||||
onProgress: (progress) => {
|
||||
serverEvents.emit("backup:progress", {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Hono } from "hono";
|
||||
import { volumeService } from "../volumes/volume.service";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
|
||||
export const driverController = new Hono()
|
||||
.post("/VolumeDriver.Capabilities", (c) => {
|
||||
@@ -31,9 +31,11 @@ export const driverController = new Hono()
|
||||
}
|
||||
|
||||
const volumeName = body.Name.replace(/^im-/, "");
|
||||
const { volume } = await volumeService.getVolume(volumeName);
|
||||
const backend = createVolumeBackend(volume);
|
||||
|
||||
return c.json({
|
||||
Mountpoint: getVolumePath(volumeName),
|
||||
Mountpoint: backend.getVolumePath(),
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Unmount", (c) => {
|
||||
@@ -49,9 +51,10 @@ export const driverController = new Hono()
|
||||
}
|
||||
|
||||
const { volume } = await volumeService.getVolume(body.Name.replace(/^im-/, ""));
|
||||
const backend = createVolumeBackend(volume);
|
||||
|
||||
return c.json({
|
||||
Mountpoint: getVolumePath(volume),
|
||||
Mountpoint: backend.getVolumePath(),
|
||||
});
|
||||
})
|
||||
.post("/VolumeDriver.Get", async (c) => {
|
||||
@@ -62,11 +65,12 @@ export const driverController = new Hono()
|
||||
}
|
||||
|
||||
const { volume } = await volumeService.getVolume(body.Name.replace(/^im-/, ""));
|
||||
const backend = createVolumeBackend(volume);
|
||||
|
||||
return c.json({
|
||||
Volume: {
|
||||
Name: `im-${volume.name}`,
|
||||
Mountpoint: getVolumePath(volume),
|
||||
Mountpoint: backend.getVolumePath(),
|
||||
Status: {},
|
||||
},
|
||||
Err: "",
|
||||
@@ -75,11 +79,16 @@ export const driverController = new Hono()
|
||||
.post("/VolumeDriver.List", async (c) => {
|
||||
const volumes = await volumeService.listVolumes();
|
||||
|
||||
const res = volumes.map((volume) => ({
|
||||
let res = [];
|
||||
for (const volume of volumes) {
|
||||
const backend = createVolumeBackend(volume);
|
||||
|
||||
res.push({
|
||||
Name: `im-${volume.name}`,
|
||||
Mountpoint: getVolumePath(volume),
|
||||
Mountpoint: backend.getVolumePath(),
|
||||
Status: {},
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
return c.json({
|
||||
Volumes: res,
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createRepositoryBody,
|
||||
createRepositoryDto,
|
||||
deleteRepositoryDto,
|
||||
deleteSnapshotDto,
|
||||
doctorRepositoryDto,
|
||||
getRepositoryDto,
|
||||
getSnapshotDetailsDto,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
restoreSnapshotBody,
|
||||
restoreSnapshotDto,
|
||||
type DeleteRepositoryDto,
|
||||
type DeleteSnapshotDto,
|
||||
type DoctorRepositoryDto,
|
||||
type GetRepositoryDto,
|
||||
type GetSnapshotDetailsDto,
|
||||
@@ -144,11 +142,4 @@ export const repositoriesController = new Hono()
|
||||
const result = await repositoriesService.doctorRepository(name);
|
||||
|
||||
return c.json<DoctorRepositoryDto>(result, 200);
|
||||
})
|
||||
.delete("/:name/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
||||
const { name, snapshotId } = c.req.param();
|
||||
|
||||
await repositoriesService.deleteSnapshot(name, snapshotId);
|
||||
|
||||
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
|
||||
});
|
||||
|
||||
@@ -326,28 +326,3 @@ export const listRcloneRemotesDto = describeRoute({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete a snapshot
|
||||
*/
|
||||
export const deleteSnapshotResponse = type({
|
||||
message: "string",
|
||||
});
|
||||
|
||||
export type DeleteSnapshotDto = typeof deleteSnapshotResponse.infer;
|
||||
|
||||
export const deleteSnapshotDto = describeRoute({
|
||||
description: "Delete a specific snapshot from a repository",
|
||||
tags: ["Repositories"],
|
||||
operationId: "deleteSnapshot",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Snapshot deleted successfully",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(deleteSnapshotResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -327,18 +327,6 @@ const doctorRepository = async (name: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const deleteSnapshot = async (name: string, snapshotId: string) => {
|
||||
const repository = await db.query.repositoriesTable.findFirst({
|
||||
where: eq(repositoriesTable.name, name),
|
||||
});
|
||||
|
||||
if (!repository) {
|
||||
throw new NotFoundError("Repository not found");
|
||||
}
|
||||
|
||||
await restic.deleteSnapshot(repository.config, snapshotId);
|
||||
};
|
||||
|
||||
export const repositoriesService = {
|
||||
listRepositories,
|
||||
createRepository,
|
||||
@@ -350,5 +338,4 @@ export const repositoriesService = {
|
||||
getSnapshotDetails,
|
||||
checkHealth,
|
||||
doctorRepository,
|
||||
deleteSnapshot,
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { VOLUME_MOUNT_BASE } from "../../core/constants";
|
||||
import type { Volume } from "../../db/schema";
|
||||
|
||||
export const getVolumePath = (volume: Volume) => {
|
||||
if (volume.config.backend === "directory") {
|
||||
return volume.config.path;
|
||||
}
|
||||
|
||||
return `${VOLUME_MOUNT_BASE}/${volume.name}/_data`;
|
||||
};
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
type BrowseFilesystemDto,
|
||||
} from "./volume.dto";
|
||||
import { volumeService } from "./volume.service";
|
||||
import { getVolumePath } from "./helpers";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
|
||||
export const volumeController = new Hono()
|
||||
.get("/", listVolumesDto, async (c) => {
|
||||
@@ -37,9 +37,10 @@ export const volumeController = new Hono()
|
||||
const body = c.req.valid("json");
|
||||
const res = await volumeService.createVolume(body.name, body.config);
|
||||
|
||||
const backend = createVolumeBackend(res.volume);
|
||||
const response = {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume),
|
||||
path: backend.getVolumePath(),
|
||||
};
|
||||
|
||||
return c.json<CreateVolumeDto>(response, 201);
|
||||
@@ -60,10 +61,11 @@ export const volumeController = new Hono()
|
||||
const { name } = c.req.param();
|
||||
const res = await volumeService.getVolume(name);
|
||||
|
||||
const backend = createVolumeBackend(res.volume);
|
||||
const response = {
|
||||
volume: {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume),
|
||||
path: backend.getVolumePath(),
|
||||
},
|
||||
statfs: {
|
||||
total: res.statfs.total ?? 0,
|
||||
@@ -85,9 +87,10 @@ export const volumeController = new Hono()
|
||||
const body = c.req.valid("json");
|
||||
const res = await volumeService.updateVolume(name, body);
|
||||
|
||||
const backend = createVolumeBackend(res.volume);
|
||||
const response = {
|
||||
...res.volume,
|
||||
path: getVolumePath(res.volume),
|
||||
path: backend.getVolumePath(),
|
||||
};
|
||||
|
||||
return c.json<UpdateVolumeDto>(response, 200);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { getStatFs, type StatFs } from "../../utils/mountinfo";
|
||||
import { withTimeout } from "../../utils/timeout";
|
||||
import { createVolumeBackend } from "../backends/backend";
|
||||
import type { UpdateVolumeBody } from "./volume.dto";
|
||||
import { getVolumePath } from "./helpers";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { serverEvents } from "../../core/events";
|
||||
import type { BackendConfig } from "~/schemas/volumes";
|
||||
@@ -129,7 +128,9 @@ const getVolume = async (name: string) => {
|
||||
|
||||
let statfs: Partial<StatFs> = {};
|
||||
if (volume.status === "mounted") {
|
||||
statfs = await withTimeout(getStatFs(getVolumePath(volume)), 1000, "getStatFs").catch((error) => {
|
||||
const backend = createVolumeBackend(volume);
|
||||
const volumePath = backend.getVolumePath();
|
||||
statfs = await withTimeout(getStatFs(volumePath), 1000, "getStatFs").catch((error) => {
|
||||
logger.warn(`Failed to get statfs for volume ${name}: ${toMessage(error)}`);
|
||||
return {};
|
||||
});
|
||||
@@ -203,7 +204,16 @@ const testConnection = async (backendConfig: BackendConfig) => {
|
||||
};
|
||||
|
||||
const backend = createVolumeBackend(mockVolume);
|
||||
const { error } = await backend.mount();
|
||||
let error: string | null = null;
|
||||
const mount = await backend.mount();
|
||||
if (mount.error) {
|
||||
error = mount.error;
|
||||
} else {
|
||||
const health = await backend.checkHealth();
|
||||
if (health.error) {
|
||||
error = health.error;
|
||||
}
|
||||
}
|
||||
|
||||
await backend.unmount();
|
||||
|
||||
@@ -295,8 +305,8 @@ const listFiles = async (name: string, subPath?: string) => {
|
||||
throw new InternalServerError("Volume is not mounted");
|
||||
}
|
||||
|
||||
// For directory volumes, use the configured path directly
|
||||
const volumePath = getVolumePath(volume);
|
||||
const backend = createVolumeBackend(volume);
|
||||
const volumePath = backend.getVolumePath();
|
||||
|
||||
const requestedPath = subPath ? path.join(volumePath, subPath) : volumePath;
|
||||
|
||||
|
||||
@@ -441,22 +441,6 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
const deleteSnapshot = async (config: RepositoryConfig, snapshotId: string) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "forget", snapshotId, "--prune"];
|
||||
|
||||
const res = await $`restic ${args}`.env(env).nothrow();
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
logger.error(`Restic snapshot deletion failed: ${res.stderr}`);
|
||||
throw new Error(`Failed to delete snapshot: ${res.stderr}`);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
};
|
||||
|
||||
const lsNodeSchema = type({
|
||||
name: "string",
|
||||
type: "string",
|
||||
@@ -617,7 +601,6 @@ export const restic = {
|
||||
restore,
|
||||
snapshots,
|
||||
forget,
|
||||
deleteSnapshot,
|
||||
unlock,
|
||||
ls,
|
||||
check,
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* This removes passwords and credentials from logs and error messages
|
||||
*/
|
||||
export const sanitizeSensitiveData = (text: string): string => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
return text;
|
||||
}
|
||||
|
||||
let sanitized = text.replace(/\b(pass|password)=([^\s,]+)/gi, "$1=***");
|
||||
|
||||
sanitized = sanitized.replace(/\/\/([^:@\s]+):([^@\s]+)@/g, "//$1:***@");
|
||||
|
||||
@@ -5,6 +5,8 @@ interface Params {
|
||||
args: string[];
|
||||
env?: NodeJS.ProcessEnv;
|
||||
signal?: AbortSignal;
|
||||
stdin?: string;
|
||||
timeout?: number;
|
||||
onStdout?: (data: string) => void;
|
||||
onStderr?: (error: string) => void;
|
||||
onError?: (error: Error) => Promise<void> | void;
|
||||
@@ -19,17 +21,26 @@ type SpawnResult = {
|
||||
};
|
||||
|
||||
export const safeSpawn = (params: Params) => {
|
||||
const { command, args, env = {}, signal, ...callbacks } = params;
|
||||
const { command, args, env = {}, signal, stdin, timeout, ...callbacks } = params;
|
||||
|
||||
return new Promise<SpawnResult>((resolve) => {
|
||||
return new Promise<SpawnResult>((resolve, reject) => {
|
||||
let stdoutData = "";
|
||||
let stderrData = "";
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
const child = spawn(command, args, {
|
||||
env: { ...process.env, ...env },
|
||||
signal: signal,
|
||||
});
|
||||
|
||||
// Handle timeout if specified
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
child.kill("SIGTERM");
|
||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
if (callbacks.onStdout) {
|
||||
callbacks.onStdout(data.toString());
|
||||
@@ -47,6 +58,7 @@ export const safeSpawn = (params: Params) => {
|
||||
});
|
||||
|
||||
child.on("error", async (error) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (callbacks.onError) {
|
||||
await callbacks.onError(error);
|
||||
}
|
||||
@@ -62,6 +74,7 @@ export const safeSpawn = (params: Params) => {
|
||||
});
|
||||
|
||||
child.on("close", async (code) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (callbacks.onClose) {
|
||||
await callbacks.onClose(code);
|
||||
}
|
||||
@@ -69,11 +82,15 @@ export const safeSpawn = (params: Params) => {
|
||||
await callbacks.finally();
|
||||
}
|
||||
|
||||
if (code !== 0 && code !== null) {
|
||||
reject(new Error(`Command failed with exit code ${code}: ${stderrData || stdoutData}`));
|
||||
} else {
|
||||
resolve({
|
||||
exitCode: code === null ? -1 : code,
|
||||
stdout: stdoutData,
|
||||
stderr: stderrData,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
1
bun.lock
1
bun.lock
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@ironmount/client",
|
||||
|
||||
@@ -15,7 +15,6 @@ services:
|
||||
ports:
|
||||
- "4096:4096"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/ironmount:/var/lib/ironmount
|
||||
|
||||
- ./app:/app/app
|
||||
@@ -38,7 +37,6 @@ services:
|
||||
ports:
|
||||
- "4096:4096"
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /var/lib/ironmount:/var/lib/ironmount:rshared
|
||||
- /run/docker/plugins:/run/docker/plugins
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
@@ -4,15 +4,8 @@ import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { reactRouterHonoServer } from "react-router-hono-server/dev";
|
||||
|
||||
const getVersion = () => {
|
||||
return process.env.VITE_APP_VERSION || "dev";
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [reactRouterHonoServer({ runtime: "bun" }), reactRouter(), tailwindcss(), tsconfigPaths()],
|
||||
define: {
|
||||
"import.meta.env.VITE_APP_VERSION": JSON.stringify(getVersion()),
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: true,
|
||||
|
||||
Reference in New Issue
Block a user