mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: custom local repository path
This commit is contained in:
@@ -741,12 +741,21 @@ export type ListRepositoriesResponses = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -754,7 +763,7 @@ export type ListRepositoriesResponses = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'error' | 'healthy' | 'unknown' | null;
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -799,12 +808,21 @@ export type CreateRepositoryData = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off';
|
compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off';
|
||||||
@@ -919,12 +937,21 @@ export type GetRepositoryResponses = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -932,7 +959,7 @@ export type GetRepositoryResponses = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'error' | 'healthy' | 'unknown' | null;
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1166,12 +1193,21 @@ export type ListBackupSchedulesResponses = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1179,7 +1215,7 @@ export type ListBackupSchedulesResponses = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'error' | 'healthy' | 'unknown' | null;
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
@@ -1379,12 +1415,21 @@ export type GetBackupScheduleResponses = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1392,7 +1437,7 @@ export type GetBackupScheduleResponses = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'error' | 'healthy' | 'unknown' | null;
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
@@ -1573,12 +1618,21 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||||||
name: string;
|
name: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
path?: string;
|
||||||
} | {
|
} | {
|
||||||
backend: 'rclone';
|
backend: 'rclone';
|
||||||
path: string;
|
path: string;
|
||||||
remote: string;
|
remote: string;
|
||||||
customPassword?: string;
|
customPassword?: string;
|
||||||
isExistingRepository?: boolean;
|
isExistingRepository?: boolean;
|
||||||
|
} | {
|
||||||
|
backend: 'rest';
|
||||||
|
url: string;
|
||||||
|
customPassword?: string;
|
||||||
|
isExistingRepository?: boolean;
|
||||||
|
password?: string;
|
||||||
|
path?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
createdAt: number;
|
createdAt: number;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -1586,7 +1640,7 @@ export type GetBackupScheduleForVolumeResponses = {
|
|||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: 'error' | 'healthy' | 'unknown' | null;
|
status: 'error' | 'healthy' | 'unknown' | null;
|
||||||
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
|
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
|
||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
repositoryId: string;
|
repositoryId: string;
|
||||||
|
|||||||
@@ -10,12 +10,23 @@ import { Input } from "./ui/input";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Alert, AlertDescription } from "./ui/alert";
|
import { Alert, AlertDescription } from "./ui/alert";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink, AlertTriangle } from "lucide-react";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||||
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
import { useSystemInfo } from "~/client/hooks/use-system-info";
|
||||||
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
|
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
|
||||||
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
|
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
|
||||||
import { Checkbox } from "./ui/checkbox";
|
import { Checkbox } from "./ui/checkbox";
|
||||||
|
import { DirectoryBrowser } from "./directory-browser";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "./ui/alert-dialog";
|
||||||
|
|
||||||
export const formSchema = type({
|
export const formSchema = type({
|
||||||
name: "2<=string<=32",
|
name: "2<=string<=32",
|
||||||
@@ -67,6 +78,8 @@ export const CreateRepositoryForm = ({
|
|||||||
const watchedIsExistingRepository = watch("isExistingRepository");
|
const watchedIsExistingRepository = watch("isExistingRepository");
|
||||||
|
|
||||||
const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
|
const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
|
||||||
|
const [showPathBrowser, setShowPathBrowser] = useState(false);
|
||||||
|
const [showPathWarning, setShowPathWarning] = useState(false);
|
||||||
|
|
||||||
const { capabilities } = useSystemInfo();
|
const { capabilities } = useSystemInfo();
|
||||||
|
|
||||||
@@ -247,6 +260,87 @@ export const CreateRepositoryForm = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{watchedBackend === "local" && (
|
||||||
|
<>
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Repository Directory</FormLabel>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 text-sm font-mono bg-muted px-3 py-2 rounded-md border">
|
||||||
|
{form.watch("path") || "/var/lib/ironmount/repositories"}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowPathWarning(true)}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
The directory where the repository will be stored.
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<AlertDialog open={showPathWarning} onOpenChange={setShowPathWarning}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||||
|
Important: Host Mount Required
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="space-y-3">
|
||||||
|
<p>
|
||||||
|
When selecting a custom path, ensure it is mounted from the host machine into the
|
||||||
|
container.
|
||||||
|
</p>
|
||||||
|
<p className="font-medium">
|
||||||
|
If the path is not a host mount, you will lose your repository data when the container
|
||||||
|
restarts.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
The default path <code className="bg-muted px-1 rounded">/var/lib/ironmount/repositories</code> is
|
||||||
|
already mounted from the host and is safe to use.
|
||||||
|
</p>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => {
|
||||||
|
setShowPathBrowser(true);
|
||||||
|
setShowPathWarning(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
I Understand, Continue
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
|
||||||
|
<AlertDialog open={showPathBrowser} onOpenChange={setShowPathBrowser}>
|
||||||
|
<AlertDialogContent className="max-w-2xl">
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Select Repository Directory</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Choose a directory from the filesystem to store the repository.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<DirectoryBrowser
|
||||||
|
onSelectPath={(path) => form.setValue("path", path)}
|
||||||
|
selectedPath={form.watch("path") || "/var/lib/ironmount/repositories"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>Done</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{watchedBackend === "s3" && (
|
{watchedBackend === "s3" && (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const r2RepositoryConfigSchema = type({
|
|||||||
export const localRepositoryConfigSchema = type({
|
export const localRepositoryConfigSchema = type({
|
||||||
backend: "'local'",
|
backend: "'local'",
|
||||||
name: "string",
|
name: "string",
|
||||||
|
path: "string?",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
export const gcsRepositoryConfigSchema = type({
|
export const gcsRepositoryConfigSchema = type({
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ const ensurePassfile = async () => {
|
|||||||
const buildRepoUrl = (config: RepositoryConfig): string => {
|
const buildRepoUrl = (config: RepositoryConfig): string => {
|
||||||
switch (config.backend) {
|
switch (config.backend) {
|
||||||
case "local":
|
case "local":
|
||||||
return `${REPOSITORY_BASE}/${config.name}`;
|
return `${config.path}/${config.name}` || `${REPOSITORY_BASE}/${config.name}`;
|
||||||
case "s3":
|
case "s3":
|
||||||
return `s3:${config.endpoint}/${config.bucket}`;
|
return `s3:${config.endpoint}/${config.bucket}`;
|
||||||
case "r2": {
|
case "r2": {
|
||||||
|
|||||||
Reference in New Issue
Block a user