mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: exclude patterns
This commit is contained in:
18
apps/client/app/components/ui/textarea.tsx
Normal file
18
apps/client/app/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea };
|
||||||
@@ -9,13 +9,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/com
|
|||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
import { VolumeFileBrowser } from "~/components/volume-file-browser";
|
import { VolumeFileBrowser } from "~/components/volume-file-browser";
|
||||||
import type { BackupSchedule, Volume } from "~/lib/types";
|
import type { BackupSchedule, Volume } from "~/lib/types";
|
||||||
import { deepClean } from "~/utils/object";
|
import { deepClean } from "~/utils/object";
|
||||||
|
|
||||||
const formSchema = type({
|
const internalFormSchema = type({
|
||||||
repositoryId: "string",
|
repositoryId: "string",
|
||||||
excludePatterns: "string[]?",
|
excludePatternsText: "string?",
|
||||||
includePatterns: "string[]?",
|
includePatterns: "string[]?",
|
||||||
frequency: "string",
|
frequency: "string",
|
||||||
dailyTime: "string?",
|
dailyTime: "string?",
|
||||||
@@ -27,7 +28,7 @@ const formSchema = type({
|
|||||||
keepMonthly: "number?",
|
keepMonthly: "number?",
|
||||||
keepYearly: "number?",
|
keepYearly: "number?",
|
||||||
});
|
});
|
||||||
const cleanSchema = type.pipe((d) => formSchema(deepClean(d)));
|
const cleanSchema = type.pipe((d) => internalFormSchema(deepClean(d)));
|
||||||
|
|
||||||
export const weeklyDays = [
|
export const weeklyDays = [
|
||||||
{ label: "Monday", value: "1" },
|
{ label: "Monday", value: "1" },
|
||||||
@@ -39,7 +40,11 @@ export const weeklyDays = [
|
|||||||
{ label: "Sunday", value: "0" },
|
{ label: "Sunday", value: "0" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type BackupScheduleFormValues = typeof formSchema.infer;
|
type InternalFormValues = typeof internalFormSchema.infer;
|
||||||
|
|
||||||
|
export type BackupScheduleFormValues = Omit<InternalFormValues, "excludePatternsText"> & {
|
||||||
|
excludePatterns?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
volume: Volume;
|
volume: Volume;
|
||||||
@@ -50,7 +55,7 @@ type Props = {
|
|||||||
formId: string;
|
formId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const backupScheduleToFormValues = (schedule?: BackupSchedule): BackupScheduleFormValues | undefined => {
|
const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalFormValues | undefined => {
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -72,16 +77,36 @@ const backupScheduleToFormValues = (schedule?: BackupSchedule): BackupScheduleFo
|
|||||||
dailyTime,
|
dailyTime,
|
||||||
weeklyDay,
|
weeklyDay,
|
||||||
includePatterns: schedule.includePatterns || undefined,
|
includePatterns: schedule.includePatterns || undefined,
|
||||||
|
excludePatternsText: schedule.excludePatterns?.join("\n") || undefined,
|
||||||
...schedule.retentionPolicy,
|
...schedule.retentionPolicy,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Props) => {
|
export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: Props) => {
|
||||||
const form = useForm<BackupScheduleFormValues>({
|
const form = useForm<InternalFormValues>({
|
||||||
resolver: arktypeResolver(cleanSchema as unknown as typeof formSchema),
|
resolver: arktypeResolver(cleanSchema as unknown as typeof internalFormSchema),
|
||||||
defaultValues: backupScheduleToFormValues(initialValues),
|
defaultValues: backupScheduleToFormValues(initialValues),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(data: InternalFormValues) => {
|
||||||
|
// Convert excludePatternsText string to excludePatterns array
|
||||||
|
const { excludePatternsText, ...rest } = data;
|
||||||
|
const excludePatterns = excludePatternsText
|
||||||
|
? excludePatternsText
|
||||||
|
.split("\n")
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
onSubmit({
|
||||||
|
...rest,
|
||||||
|
excludePatterns,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
const { data: repositoriesData } = useQuery({
|
const { data: repositoriesData } = useQuery({
|
||||||
...listRepositoriesOptions(),
|
...listRepositoriesOptions(),
|
||||||
});
|
});
|
||||||
@@ -102,7 +127,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]"
|
className="grid gap-4 xl:grid-cols-[minmax(0,_2.3fr)_minmax(320px,_1fr)]"
|
||||||
id={formId}
|
id={formId}
|
||||||
>
|
>
|
||||||
@@ -249,6 +274,47 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Exclude patterns</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Optionally specify patterns to exclude from backups. Enter one pattern per line (e.g., *.tmp,
|
||||||
|
node_modules/**, .cache/).
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="excludePatternsText"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Exclusion patterns</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
{...field}
|
||||||
|
placeholder="*.tmp node_modules/** .cache/ *.log"
|
||||||
|
className="font-mono text-sm min-h-[120px]"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Patterns support glob syntax. See
|
||||||
|
<a
|
||||||
|
href="https://restic.readthedocs.io/en/stable/040_backup.html#excluding-files"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="underline hover:text-foreground"
|
||||||
|
>
|
||||||
|
Restic documentation
|
||||||
|
</a>
|
||||||
|
for more details.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Retention policy</CardTitle>
|
<CardTitle>Retention policy</CardTitle>
|
||||||
@@ -408,6 +474,33 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
|
|||||||
{repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}
|
{repositoriesData?.find((r) => r.id === formValues.repositoryId)?.name || "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{formValues.includePatterns && formValues.includePatterns.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase text-muted-foreground">Include paths</p>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{formValues.includePatterns.map((path) => (
|
||||||
|
<span key={path} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
||||||
|
{path}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{formValues.excludePatternsText && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase text-muted-foreground">Exclude patterns</p>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{formValues.excludePatternsText
|
||||||
|
.split("\n")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((pattern) => (
|
||||||
|
<span key={pattern} className="text-xs font-mono bg-accent px-1.5 py-0.5 rounded">
|
||||||
|
{pattern.trim()}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs uppercase text-muted-foreground">Retention</p>
|
<p className="text-xs uppercase text-muted-foreground">Retention</p>
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
|
|||||||
|
|
||||||
const buildEnv = async (config: RepositoryConfig) => {
|
const buildEnv = async (config: RepositoryConfig) => {
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string> = {
|
||||||
RESTIC_CACHE_DIR: "/tmp/restic-cache",
|
RESTIC_CACHE_DIR: "/var/lib/ironmount/restic/cache",
|
||||||
RESTIC_PASSWORD_FILE: RESTIC_PASS_FILE,
|
RESTIC_PASSWORD_FILE: RESTIC_PASS_FILE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user