mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
Compare commits
5 Commits
v0.10.1-be
...
v0.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e4c782ff | ||
|
|
c726c6fc72 | ||
|
|
4d48d7be58 | ||
|
|
df6b70c96f | ||
|
|
94423bd0a5 |
@@ -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;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function AppSidebar() {
|
|||||||
"opacity-0 w-0 overflow-hidden": state === "collapsed",
|
"opacity-0 w-0 overflow-hidden": state === "collapsed",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Version {APP_VERSION}
|
{APP_VERSION}
|
||||||
</div>
|
</div>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -41,6 +52,7 @@ const defaultValuesForType = {
|
|||||||
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
|
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
|
||||||
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
azure: { backend: "azure" as const, compressionMode: "auto" as const },
|
||||||
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
|
||||||
|
rest: { backend: "rest" as const, compressionMode: "auto" as const },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateRepositoryForm = ({
|
export const CreateRepositoryForm = ({
|
||||||
@@ -66,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();
|
||||||
|
|
||||||
@@ -126,6 +140,7 @@ export const CreateRepositoryForm = ({
|
|||||||
<SelectItem value="r2">Cloudflare R2</SelectItem>
|
<SelectItem value="r2">Cloudflare R2</SelectItem>
|
||||||
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
|
||||||
<SelectItem value="azure">Azure Blob Storage</SelectItem>
|
<SelectItem value="azure">Azure Blob Storage</SelectItem>
|
||||||
|
<SelectItem value="rest">REST Server</SelectItem>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<SelectItem disabled={!capabilities.rclone} value="rclone">
|
<SelectItem disabled={!capabilities.rclone} value="rclone">
|
||||||
@@ -245,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
|
||||||
@@ -546,6 +642,67 @@ export const CreateRepositoryForm = ({
|
|||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{watchedBackend === "rest" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="url"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>REST Server URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="http://192.168.1.30:8000" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>URL of the REST server.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Repository Path (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my-backup-repo" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Path to the repository on the REST server (leave empty for root).</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="username" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Username for REST server authentication.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="••••••••" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Password for REST server authentication.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{mode === "update" && (
|
{mode === "update" && (
|
||||||
<Button type="submit" className="w-full" loading={loading}>
|
<Button type="submit" className="w-full" loading={loading}>
|
||||||
Save Changes
|
Save Changes
|
||||||
|
|||||||
@@ -207,7 +207,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="2049" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="2049"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>NFS server port (default: 2049).</FormDescription>
|
<FormDescription>NFS server port (default: 2049).</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -332,7 +337,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="80" {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="80"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>WebDAV server port (default: 80 for HTTP, 443 for HTTPS).</FormDescription>
|
<FormDescription>WebDAV server port (default: 80 for HTTP, 443 for HTTPS).</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Database, HardDrive, Cloud } from "lucide-react";
|
import { Database, HardDrive, Cloud, Server } from "lucide-react";
|
||||||
import type { RepositoryBackend } from "~/schemas/restic";
|
import type { RepositoryBackend } from "~/schemas/restic";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -14,6 +14,8 @@ export const RepositoryIcon = ({ backend, className = "h-4 w-4" }: Props) => {
|
|||||||
return <Cloud className={className} />;
|
return <Cloud className={className} />;
|
||||||
case "gcs":
|
case "gcs":
|
||||||
return <Cloud className={className} />;
|
return <Cloud className={className} />;
|
||||||
|
case "rest":
|
||||||
|
return <Server className={className} />;
|
||||||
default:
|
default:
|
||||||
return <Database className={className} />;
|
return <Database className={className} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const REPOSITORY_BACKENDS = {
|
|||||||
gcs: "gcs",
|
gcs: "gcs",
|
||||||
azure: "azure",
|
azure: "azure",
|
||||||
rclone: "rclone",
|
rclone: "rclone",
|
||||||
|
rest: "rest",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
|
||||||
@@ -36,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({
|
||||||
@@ -59,12 +61,21 @@ export const rcloneRepositoryConfigSchema = type({
|
|||||||
path: "string",
|
path: "string",
|
||||||
}).and(baseRepositoryConfigSchema);
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
|
export const restRepositoryConfigSchema = type({
|
||||||
|
backend: "'rest'",
|
||||||
|
url: "string",
|
||||||
|
username: "string?",
|
||||||
|
password: "string?",
|
||||||
|
path: "string?",
|
||||||
|
}).and(baseRepositoryConfigSchema);
|
||||||
|
|
||||||
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
export const repositoryConfigSchema = s3RepositoryConfigSchema
|
||||||
.or(r2RepositoryConfigSchema)
|
.or(r2RepositoryConfigSchema)
|
||||||
.or(localRepositoryConfigSchema)
|
.or(localRepositoryConfigSchema)
|
||||||
.or(gcsRepositoryConfigSchema)
|
.or(gcsRepositoryConfigSchema)
|
||||||
.or(azureRepositoryConfigSchema)
|
.or(azureRepositoryConfigSchema)
|
||||||
.or(rcloneRepositoryConfigSchema);
|
.or(rcloneRepositoryConfigSchema)
|
||||||
|
.or(restRepositoryConfigSchema);
|
||||||
|
|
||||||
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
export type RepositoryConfig = typeof repositoryConfigSchema.infer;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
|
|||||||
case "azure":
|
case "azure":
|
||||||
encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
|
encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
|
||||||
break;
|
break;
|
||||||
|
case "rest":
|
||||||
|
if (config.username) {
|
||||||
|
encryptedConfig.username = await cryptoUtils.encrypt(config.username);
|
||||||
|
}
|
||||||
|
if (config.password) {
|
||||||
|
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptedConfig as RepositoryConfig;
|
return encryptedConfig as RepositoryConfig;
|
||||||
|
|||||||
@@ -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.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": {
|
||||||
@@ -84,6 +84,10 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||||||
return `azure:${config.container}:/`;
|
return `azure:${config.container}:/`;
|
||||||
case "rclone":
|
case "rclone":
|
||||||
return `rclone:${config.remote}:${config.path}`;
|
return `rclone:${config.remote}:${config.path}`;
|
||||||
|
case "rest": {
|
||||||
|
const path = config.path ? `/${config.path}` : "";
|
||||||
|
return `rest:${config.url}${path}`;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
|
||||||
}
|
}
|
||||||
@@ -133,6 +137,15 @@ const buildEnv = async (config: RepositoryConfig) => {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "rest": {
|
||||||
|
if (config.username) {
|
||||||
|
env.RESTIC_REST_USERNAME = await cryptoUtils.decrypt(config.username);
|
||||||
|
}
|
||||||
|
if (config.password) {
|
||||||
|
env.RESTIC_REST_PASSWORD = await cryptoUtils.decrypt(config.password);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
@@ -142,6 +155,9 @@ const init = async (config: RepositoryConfig) => {
|
|||||||
await ensurePassfile();
|
await ensurePassfile();
|
||||||
|
|
||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
|
|
||||||
|
logger.info(`Initializing restic repository at ${repoUrl}...`);
|
||||||
|
|
||||||
const env = await buildEnv(config);
|
const env = await buildEnv(config);
|
||||||
|
|
||||||
const res = await $`restic init --repo ${repoUrl} --json`.env(env).nothrow();
|
const res = await $`restic init --repo ${repoUrl} --json`.env(env).nothrow();
|
||||||
|
|||||||
9
bun.lock
9
bun.lock
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 0,
|
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@ironmount/client",
|
"name": "@ironmount/client",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.6",
|
|
||||||
"@hono/standard-validator": "^0.1.5",
|
"@hono/standard-validator": "^0.1.5",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -24,8 +22,6 @@
|
|||||||
"@react-router/serve": "^7.9.3",
|
"@react-router/serve": "^7.9.3",
|
||||||
"@scalar/hono-api-reference": "^0.9.24",
|
"@scalar/hono-api-reference": "^0.9.24",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"arktype": "^2.1.26",
|
"arktype": "^2.1.26",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -61,6 +57,7 @@
|
|||||||
"@hey-api/openapi-ts": "^0.87.4",
|
"@hey-api/openapi-ts": "^0.87.4",
|
||||||
"@react-router/dev": "^7.9.3",
|
"@react-router/dev": "^7.9.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@types/bun": "^1.3.2",
|
"@types/bun": "^1.3.2",
|
||||||
"@types/dockerode": "^3.3.45",
|
"@types/dockerode": "^3.3.45",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
@@ -483,10 +480,6 @@
|
|||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
|
||||||
|
|
||||||
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
|
|
||||||
|
|
||||||
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||||
|
|
||||||
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"studio": "drizzle-kit studio"
|
"studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hono/node-server": "^1.19.6",
|
|
||||||
"@hono/standard-validator": "^0.1.5",
|
"@hono/standard-validator": "^0.1.5",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -36,8 +35,6 @@
|
|||||||
"@react-router/serve": "^7.9.3",
|
"@react-router/serve": "^7.9.3",
|
||||||
"@scalar/hono-api-reference": "^0.9.24",
|
"@scalar/hono-api-reference": "^0.9.24",
|
||||||
"@tanstack/react-query": "^5.90.2",
|
"@tanstack/react-query": "^5.90.2",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
|
||||||
"@tanstack/react-table": "^8.21.3",
|
|
||||||
"arktype": "^2.1.26",
|
"arktype": "^2.1.26",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -73,6 +70,7 @@
|
|||||||
"@hey-api/openapi-ts": "^0.87.4",
|
"@hey-api/openapi-ts": "^0.87.4",
|
||||||
"@react-router/dev": "^7.9.3",
|
"@react-router/dev": "^7.9.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@tanstack/react-query-devtools": "^5.90.2",
|
||||||
"@types/bun": "^1.3.2",
|
"@types/bun": "^1.3.2",
|
||||||
"@types/dockerode": "^3.3.45",
|
"@types/dockerode": "^3.3.45",
|
||||||
"@types/node": "^24.6.2",
|
"@types/node": "^24.6.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user