diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts
index 216a4740..8fa02f2d 100644
--- a/app/client/api-client/types.gen.ts
+++ b/app/client/api-client/types.gen.ts
@@ -2430,6 +2430,7 @@ export type ListBackupSchedulesResponses = {
} | null;
excludePatterns: Array | null;
excludeIfPresent: Array | null;
+ includePaths: Array | null;
includePatterns: Array | null;
oneFileSystem: boolean;
customResticParams: Array | null;
@@ -2704,6 +2705,7 @@ export type CreateBackupScheduleData = {
};
excludePatterns?: Array;
excludeIfPresent?: Array;
+ includePaths?: Array;
includePatterns?: Array;
oneFileSystem?: boolean;
tags?: Array;
@@ -2737,6 +2739,7 @@ export type CreateBackupScheduleResponses = {
} | null;
excludePatterns: Array | null;
excludeIfPresent: Array | null;
+ includePaths: Array | null;
includePatterns: Array | null;
oneFileSystem: boolean;
customResticParams: Array | null;
@@ -2803,6 +2806,7 @@ export type GetBackupScheduleResponses = {
} | null;
excludePatterns: Array | null;
excludeIfPresent: Array | null;
+ includePaths: Array | null;
includePatterns: Array | null;
oneFileSystem: boolean;
customResticParams: Array | null;
@@ -3076,6 +3080,7 @@ export type UpdateBackupScheduleData = {
};
excludePatterns?: Array;
excludeIfPresent?: Array;
+ includePaths?: Array;
includePatterns?: Array;
oneFileSystem?: boolean;
tags?: Array;
@@ -3111,6 +3116,7 @@ export type UpdateBackupScheduleResponses = {
} | null;
excludePatterns: Array | null;
excludeIfPresent: Array | null;
+ includePaths: Array | null;
includePatterns: Array | null;
oneFileSystem: boolean;
customResticParams: Array | null;
@@ -3157,6 +3163,7 @@ export type GetBackupScheduleForVolumeResponses = {
} | null;
excludePatterns: Array | null;
excludeIfPresent: Array | null;
+ includePaths: Array | null;
includePatterns: Array | null;
oneFileSystem: boolean;
customResticParams: Array | null;
@@ -4154,13 +4161,13 @@ export type GetBackupProgressResponses = {
scheduleId: string;
volumeName: string;
repositoryName: string;
- seconds_elapsed: number;
+ seconds_elapsed?: number;
seconds_remaining?: number;
percent_done: number;
total_files: number;
- files_done: number;
+ files_done?: number;
total_bytes: number;
- bytes_done: number;
+ bytes_done?: number;
current_files?: Array;
} | null;
};
diff --git a/app/client/modules/backups/components/create-schedule-form/index.tsx b/app/client/modules/backups/components/create-schedule-form/index.tsx
index beb040b7..949d2d2f 100644
--- a/app/client/modules/backups/components/create-schedule-form/index.tsx
+++ b/app/client/modules/backups/components/create-schedule-form/index.tsx
@@ -40,22 +40,22 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
const {
excludePatternsText,
excludeIfPresentText,
- includePatternsText,
+ includePatterns,
customResticParamsText,
- includePatterns: fileBrowserPatterns,
+ includePaths,
cronExpression,
...rest
} = data;
const excludePatterns = parseMultilineEntries(excludePatternsText);
const excludeIfPresent = parseMultilineEntries(excludeIfPresentText);
- const textPatterns = parseMultilineEntries(includePatternsText);
- const includePatterns = [...(fileBrowserPatterns || []), ...textPatterns];
+ const parsedIncludePatterns = parseMultilineEntries(includePatterns);
const customResticParams = parseMultilineEntries(customResticParamsText);
onSubmit({
...rest,
cronExpression,
- includePatterns: includePatterns.length > 0 ? includePatterns : [],
+ includePaths: includePaths?.length ? includePaths : [],
+ includePatterns: parsedIncludePatterns.length > 0 ? parsedIncludePatterns : [],
excludePatterns,
excludeIfPresent,
customResticParams,
@@ -67,13 +67,13 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
const frequency = form.watch("frequency");
const formValues = form.watch();
- const [selectedPaths, setSelectedPaths] = useState>(new Set(initialValues?.includePatterns || []));
+ const [selectedPaths, setSelectedPaths] = useState>(new Set(initialValues?.includePaths || []));
const [showAllSelectedPaths, setShowAllSelectedPaths] = useState(false);
const handleSelectionChange = useCallback(
(paths: Set) => {
setSelectedPaths(paths);
- form.setValue("includePatterns", Array.from(paths));
+ form.setValue("includePaths", Array.from(paths));
},
[form],
);
@@ -83,7 +83,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
const newPaths = new Set(selectedPaths);
newPaths.delete(pathToRemove);
setSelectedPaths(newPaths);
- form.setValue("includePatterns", Array.from(newPaths));
+ form.setValue("includePaths", Array.from(newPaths));
},
[selectedPaths, form],
);
diff --git a/app/client/modules/backups/components/create-schedule-form/paths-section.tsx b/app/client/modules/backups/components/create-schedule-form/paths-section.tsx
index a84b6254..d97e4206 100644
--- a/app/client/modules/backups/components/create-schedule-form/paths-section.tsx
+++ b/app/client/modules/backups/components/create-schedule-form/paths-section.tsx
@@ -68,7 +68,7 @@ export const PathsSection = ({
)}
(
Additional include patterns
diff --git a/app/client/modules/backups/components/create-schedule-form/summary-section.tsx b/app/client/modules/backups/components/create-schedule-form/summary-section.tsx
index 69872f3c..4b98846e 100644
--- a/app/client/modules/backups/components/create-schedule-form/summary-section.tsx
+++ b/app/client/modules/backups/components/create-schedule-form/summary-section.tsx
@@ -38,22 +38,22 @@ export const SummarySection = ({ volume, frequency, formValues }: SummarySection
{repositoriesData?.find((r) => r.shortId === formValues.repositoryId)?.name || "—"}
- {(formValues.includePatterns && formValues.includePatterns.length > 0) || formValues.includePatternsText ? (
+ {(formValues.includePaths && formValues.includePaths.length > 0) || formValues.includePatterns ? (
Include paths/patterns
- {formValues.includePatterns?.slice(0, 20).map((path) => (
+ {formValues.includePaths?.slice(0, 20).map((path) => (
{path}
))}
- {formValues.includePatterns && formValues.includePatterns.length > 20 && (
- + {formValues.includePatterns.length - 20} more
+ {formValues.includePaths && formValues.includePaths.length > 20 && (
+ + {formValues.includePaths.length - 20} more
)}
- {formValues.includePatternsText
+ {formValues.includePatterns
?.split("\n")
.filter(Boolean)
- .slice(0, 20 - (formValues.includePatterns?.length || 0))
+ .slice(0, 20 - (formValues.includePaths?.length || 0))
.map((pattern) => (
{pattern.trim()}
diff --git a/app/client/modules/backups/components/create-schedule-form/types.ts b/app/client/modules/backups/components/create-schedule-form/types.ts
index bad646ff..a1d2a4fa 100644
--- a/app/client/modules/backups/components/create-schedule-form/types.ts
+++ b/app/client/modules/backups/components/create-schedule-form/types.ts
@@ -5,8 +5,8 @@ export const internalFormSchema = z.object({
repositoryId: z.string(),
excludePatternsText: z.string().optional(),
excludeIfPresentText: z.string().optional(),
- includePatternsText: z.string().optional(),
- includePatterns: z.array(z.string()).optional(),
+ includePatterns: z.string().optional(),
+ includePaths: z.array(z.string()).optional(),
frequency: z.string(),
dailyTime: z.string().optional(),
weeklyDay: z.string().optional(),
@@ -36,9 +36,10 @@ export type InternalFormValues = z.infer;
export type BackupScheduleFormValues = Omit<
InternalFormValues,
- "excludePatternsText" | "excludeIfPresentText" | "includePatternsText" | "customResticParamsText"
+ "excludePatternsText" | "excludeIfPresentText" | "includePatterns" | "customResticParamsText"
> & {
excludePatterns?: string[];
excludeIfPresent?: string[];
+ includePatterns?: string[];
customResticParams?: string[];
};
diff --git a/app/client/modules/backups/components/create-schedule-form/utils.ts b/app/client/modules/backups/components/create-schedule-form/utils.ts
index a2d59ccc..6584ea25 100644
--- a/app/client/modules/backups/components/create-schedule-form/utils.ts
+++ b/app/client/modules/backups/components/create-schedule-form/utils.ts
@@ -17,16 +17,11 @@ export const backupScheduleToFormValues = (schedule?: BackupSchedule): InternalF
const cronValues = cronToFormValues(schedule.cronExpression ?? "0 * * * *");
- const patterns = schedule.includePatterns || [];
- const isGlobPattern = (p: string) => /[*?[\]]/.test(p);
- const fileBrowserPaths = patterns.filter((p) => !isGlobPattern(p));
- const textPatterns = patterns.filter(isGlobPattern);
-
return {
name: schedule.name,
repositoryId: schedule.repository.shortId,
- includePatterns: fileBrowserPaths.length > 0 ? fileBrowserPaths : undefined,
- includePatternsText: textPatterns.length > 0 ? textPatterns.join("\n") : undefined,
+ includePaths: schedule.includePaths?.length ? schedule.includePaths : undefined,
+ includePatterns: schedule.includePatterns?.length ? schedule.includePatterns.join("\n") : undefined,
excludePatternsText: schedule.excludePatterns?.join("\n") || undefined,
excludeIfPresentText: schedule.excludeIfPresent?.join("\n") || undefined,
oneFileSystem: schedule.oneFileSystem ?? false,
diff --git a/app/client/modules/backups/routes/backup-details.tsx b/app/client/modules/backups/routes/backup-details.tsx
index 7a521f90..a581a7b3 100644
--- a/app/client/modules/backups/routes/backup-details.tsx
+++ b/app/client/modules/backups/routes/backup-details.tsx
@@ -185,6 +185,7 @@ export function ScheduleDetailsPage(props: Props) {
enabled: schedule.enabled,
cronExpression,
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
+ includePaths: formValues.includePaths,
includePatterns: formValues.includePatterns,
excludePatterns: formValues.excludePatterns,
excludeIfPresent: formValues.excludeIfPresent,
@@ -203,6 +204,7 @@ export function ScheduleDetailsPage(props: Props) {
enabled,
cronExpression: schedule.cronExpression,
retentionPolicy: schedule.retentionPolicy || undefined,
+ includePaths: schedule.includePaths || [],
includePatterns: schedule.includePatterns || [],
excludePatterns: schedule.excludePatterns || [],
excludeIfPresent: schedule.excludeIfPresent || [],
diff --git a/app/client/modules/backups/routes/create-backup.tsx b/app/client/modules/backups/routes/create-backup.tsx
index 4fb4cf1f..80a34e91 100644
--- a/app/client/modules/backups/routes/create-backup.tsx
+++ b/app/client/modules/backups/routes/create-backup.tsx
@@ -69,6 +69,7 @@ export function CreateBackupPage() {
enabled: true,
cronExpression,
retentionPolicy: Object.keys(retentionPolicy).length > 0 ? retentionPolicy : undefined,
+ includePaths: formValues.includePaths,
includePatterns: formValues.includePatterns,
excludePatterns: formValues.excludePatterns,
excludeIfPresent: formValues.excludeIfPresent,
diff --git a/app/drizzle/20260320172926_hesitant_naoko/migration.sql b/app/drizzle/20260320172926_hesitant_naoko/migration.sql
new file mode 100644
index 00000000..4200ead6
--- /dev/null
+++ b/app/drizzle/20260320172926_hesitant_naoko/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE `backup_schedules_table` ADD `include_paths` text DEFAULT '[]';
\ No newline at end of file
diff --git a/app/drizzle/20260320172926_hesitant_naoko/snapshot.json b/app/drizzle/20260320172926_hesitant_naoko/snapshot.json
new file mode 100644
index 00000000..e8814169
--- /dev/null
+++ b/app/drizzle/20260320172926_hesitant_naoko/snapshot.json
@@ -0,0 +1,2281 @@
+{
+ "version": "7",
+ "dialect": "sqlite",
+ "id": "ab5feb4c-5609-4ba7-be8c-ccde709abccc",
+ "prevIds": ["7cbce296-a3ec-4b25-8930-32d5796a8ced"],
+ "ddl": [
+ {
+ "name": "account",
+ "entityType": "tables"
+ },
+ {
+ "name": "app_metadata",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedule_mirrors_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedule_notifications_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "backup_schedules_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "invitation",
+ "entityType": "tables"
+ },
+ {
+ "name": "member",
+ "entityType": "tables"
+ },
+ {
+ "name": "notification_destinations_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "organization",
+ "entityType": "tables"
+ },
+ {
+ "name": "repositories_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "sessions_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "sso_provider",
+ "entityType": "tables"
+ },
+ {
+ "name": "two_factor",
+ "entityType": "tables"
+ },
+ {
+ "name": "users_table",
+ "entityType": "tables"
+ },
+ {
+ "name": "verification",
+ "entityType": "tables"
+ },
+ {
+ "name": "volumes_table",
+ "entityType": "tables"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "account_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id_token",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "access_token_expires_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "refresh_token_expires_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "scope",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "account"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "key",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "app_metadata"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "schedule_id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_at",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_status",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_copy_error",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "schedule_id",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "destination_id",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "notify_on_start",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "notify_on_success",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "notify_on_warning",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "notify_on_failure",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "volume_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "repository_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "cron_expression",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "retention_policy",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "exclude_patterns",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "exclude_if_present",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "include_paths",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "include_patterns",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_status",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_backup_error",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "next_backup_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "one_file_system",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'[]'",
+ "generated": null,
+ "name": "custom_restic_params",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "0",
+ "generated": null,
+ "name": "sort_order",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "backup_schedules_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "inviter_id",
+ "entityType": "columns",
+ "table": "invitation"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'member'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "member"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "enabled",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "notification_destinations_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "slug",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "logo",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "metadata",
+ "entityType": "columns",
+ "table": "organization"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provisioning_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'auto'",
+ "generated": null,
+ "name": "compression_mode",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": "'unknown'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_checked",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_error",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "doctor_result",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stats",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "stats_updated_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "upload_limit_enabled",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "real",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "1",
+ "generated": null,
+ "name": "upload_limit_value",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Mbps'",
+ "generated": null,
+ "name": "upload_limit_unit",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "download_limit_enabled",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "real",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "1",
+ "generated": null,
+ "name": "download_limit_value",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'Mbps'",
+ "generated": null,
+ "name": "download_limit_unit",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "repositories_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "token",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ip_address",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_agent",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "impersonated_by",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "active_organization_id",
+ "entityType": "columns",
+ "table": "sessions_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provider_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "issuer",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "domain",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "auto_link_matching_emails",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "oidc_config",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "saml_config",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "sso_provider"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "secret",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "backup_codes",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "table": "two_factor"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "username",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "password_hash",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "has_downloaded_restic_password",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "email",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "email_verified",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "image",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "display_username",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "two_factor_enabled",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'",
+ "generated": null,
+ "name": "role",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "false",
+ "generated": null,
+ "name": "banned",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ban_reason",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "ban_expires",
+ "entityType": "columns",
+ "table": "users_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "identifier",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "value",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "expires_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "verification"
+ },
+ {
+ "type": "integer",
+ "notNull": false,
+ "autoincrement": true,
+ "default": null,
+ "generated": null,
+ "name": "id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "short_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "provisioning_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "name",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "type",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'unmounted'",
+ "generated": null,
+ "name": "status",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": false,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "last_error",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "last_health_check",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "created_at",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(unixepoch() * 1000)",
+ "generated": null,
+ "name": "updated_at",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "config",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "integer",
+ "notNull": true,
+ "autoincrement": false,
+ "default": "true",
+ "generated": null,
+ "name": "auto_remount",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "type": "text",
+ "notNull": true,
+ "autoincrement": false,
+ "default": null,
+ "generated": null,
+ "name": "organization_id",
+ "entityType": "columns",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "account_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "account"
+ },
+ {
+ "columns": ["schedule_id"],
+ "tableTo": "backup_schedules_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["repository_id"],
+ "tableTo": "repositories_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["schedule_id"],
+ "tableTo": "backup_schedules_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["destination_id"],
+ "tableTo": "notification_destinations_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["volume_id"],
+ "tableTo": "volumes_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_volume_id_volumes_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["repository_id"],
+ "tableTo": "repositories_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_repository_id_repositories_table_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "backup_schedules_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "invitation_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "invitation"
+ },
+ {
+ "columns": ["inviter_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "invitation_inviter_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "invitation"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "member_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "member"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "member_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "member"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "notification_destinations_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "notification_destinations_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "repositories_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "repositories_table"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "sessions_table_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "sessions_table"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "fk_sso_provider_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "SET NULL",
+ "nameExplicit": false,
+ "name": "fk_sso_provider_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["user_id"],
+ "tableTo": "users_table",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "two_factor_user_id_users_table_id_fk",
+ "entityType": "fks",
+ "table": "two_factor"
+ },
+ {
+ "columns": ["organization_id"],
+ "tableTo": "organization",
+ "columnsTo": ["id"],
+ "onUpdate": "NO ACTION",
+ "onDelete": "CASCADE",
+ "nameExplicit": false,
+ "name": "volumes_table_organization_id_organization_id_fk",
+ "entityType": "fks",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["schedule_id", "destination_id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk",
+ "entityType": "pks",
+ "table": "backup_schedule_notifications_table"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "account_pk",
+ "table": "account",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["key"],
+ "nameExplicit": false,
+ "name": "app_metadata_pk",
+ "table": "app_metadata",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_pk",
+ "table": "backup_schedule_mirrors_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "backup_schedules_table_pk",
+ "table": "backup_schedules_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "invitation_pk",
+ "table": "invitation",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "member_pk",
+ "table": "member",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "notification_destinations_table_pk",
+ "table": "notification_destinations_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "organization_pk",
+ "table": "organization",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "repositories_table_pk",
+ "table": "repositories_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sessions_table_pk",
+ "table": "sessions_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "sso_provider_pk",
+ "table": "sso_provider",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "two_factor_pk",
+ "table": "two_factor",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "users_table_pk",
+ "table": "users_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "verification_pk",
+ "table": "verification",
+ "entityType": "pks"
+ },
+ {
+ "columns": ["id"],
+ "nameExplicit": false,
+ "name": "volumes_table_pk",
+ "table": "volumes_table",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "account_userId_idx",
+ "entityType": "indexes",
+ "table": "account"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "invitation_organizationId_idx",
+ "entityType": "indexes",
+ "table": "invitation"
+ },
+ {
+ "columns": [
+ {
+ "value": "email",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "invitation_email_idx",
+ "entityType": "indexes",
+ "table": "invitation"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "member_organizationId_idx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "member_userId_idx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "member_org_user_uidx",
+ "entityType": "indexes",
+ "table": "member"
+ },
+ {
+ "columns": [
+ {
+ "value": "slug",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "organization_slug_uidx",
+ "entityType": "indexes",
+ "table": "organization"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "provisioning_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "repositories_table_org_provisioning_id_uidx",
+ "entityType": "indexes",
+ "table": "repositories_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "sessionsTable_userId_idx",
+ "entityType": "indexes",
+ "table": "sessions_table"
+ },
+ {
+ "columns": [
+ {
+ "value": "secret",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "twoFactor_secret_idx",
+ "entityType": "indexes",
+ "table": "two_factor"
+ },
+ {
+ "columns": [
+ {
+ "value": "user_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "twoFactor_userId_idx",
+ "entityType": "indexes",
+ "table": "two_factor"
+ },
+ {
+ "columns": [
+ {
+ "value": "identifier",
+ "isExpression": false
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "origin": "manual",
+ "name": "verification_identifier_idx",
+ "entityType": "indexes",
+ "table": "verification"
+ },
+ {
+ "columns": [
+ {
+ "value": "organization_id",
+ "isExpression": false
+ },
+ {
+ "value": "provisioning_id",
+ "isExpression": false
+ }
+ ],
+ "isUnique": true,
+ "where": null,
+ "origin": "manual",
+ "name": "volumes_table_org_provisioning_id_uidx",
+ "entityType": "indexes",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["schedule_id", "repository_id"],
+ "nameExplicit": false,
+ "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique",
+ "entityType": "uniques",
+ "table": "backup_schedule_mirrors_table"
+ },
+ {
+ "columns": ["name", "organization_id"],
+ "nameExplicit": false,
+ "name": "volumes_table_name_organization_id_unique",
+ "entityType": "uniques",
+ "table": "volumes_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "backup_schedules_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "backup_schedules_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "repositories_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "repositories_table"
+ },
+ {
+ "columns": ["token"],
+ "nameExplicit": false,
+ "name": "sessions_table_token_unique",
+ "entityType": "uniques",
+ "table": "sessions_table"
+ },
+ {
+ "columns": ["provider_id"],
+ "nameExplicit": false,
+ "name": "sso_provider_provider_id_unique",
+ "entityType": "uniques",
+ "table": "sso_provider"
+ },
+ {
+ "columns": ["username"],
+ "nameExplicit": false,
+ "name": "users_table_username_unique",
+ "entityType": "uniques",
+ "table": "users_table"
+ },
+ {
+ "columns": ["email"],
+ "nameExplicit": false,
+ "name": "users_table_email_unique",
+ "entityType": "uniques",
+ "table": "users_table"
+ },
+ {
+ "columns": ["short_id"],
+ "nameExplicit": false,
+ "name": "volumes_table_short_id_unique",
+ "entityType": "uniques",
+ "table": "volumes_table"
+ }
+ ],
+ "renames": []
+}
diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts
index 6b7f72db..70b62ab5 100644
--- a/app/server/db/schema.ts
+++ b/app/server/db/schema.ts
@@ -300,6 +300,7 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
}>(),
excludePatterns: text("exclude_patterns", { mode: "json" }).$type().default([]),
excludeIfPresent: text("exclude_if_present", { mode: "json" }).$type().default([]),
+ includePaths: text("include_paths", { mode: "json" }).$type().default([]),
includePatterns: text("include_patterns", { mode: "json" }).$type().default([]),
lastBackupAt: int("last_backup_at", { mode: "number" }),
lastBackupStatus: text("last_backup_status").$type<"success" | "error" | "in_progress" | "warning" | null>(),
diff --git a/app/server/modules/backups/__tests__/backups.patterns.test.ts b/app/server/modules/backups/__tests__/backups.patterns.test.ts
index a276a1bb..2c7f886d 100644
--- a/app/server/modules/backups/__tests__/backups.patterns.test.ts
+++ b/app/server/modules/backups/__tests__/backups.patterns.test.ts
@@ -9,6 +9,7 @@ const createSchedule = (overrides: Partial = {}): BackupSch
fromAny({
shortId: "sched-1234",
oneFileSystem: false,
+ includePaths: [],
includePatterns: [],
excludePatterns: [],
excludeIfPresent: [],
@@ -20,7 +21,8 @@ describe("executeBackup - include / exclude patterns", () => {
// arrange
const volumePath = "/var/lib/zerobyte/volumes/vol123/_data";
const schedule = createSchedule({
- includePatterns: ["*.zip", "/Photos", "!/Temp", "!*.log"],
+ includePaths: ["/Photos"],
+ includePatterns: ["*.zip", "!/Temp", "!*.log"],
excludePatterns: [".DS_Store", "/Config", "!/Important", "!*.tmp"],
excludeIfPresent: [".nobackup"],
});
@@ -31,9 +33,9 @@ describe("executeBackup - include / exclude patterns", () => {
// assert
expect(options).toMatchObject({
- include: [
+ includePaths: [path.join(volumePath, "Photos")],
+ includePatterns: [
path.join(volumePath, "*.zip"),
- path.join(volumePath, "Photos"),
`!${path.join(volumePath, "Temp")}`,
`!${path.join(volumePath, "*.log")}`,
],
@@ -48,7 +50,7 @@ describe("executeBackup - include / exclude patterns", () => {
const volumePath = `/${volumeName}`;
const selectedPath = `/${volumeName}`;
const schedule = createSchedule({
- includePatterns: [selectedPath],
+ includePaths: [selectedPath],
});
const signal = new AbortController().signal;
@@ -56,7 +58,7 @@ describe("executeBackup - include / exclude patterns", () => {
const options = createBackupOptions(schedule, volumePath, signal);
// assert
- expect(options.include).toEqual([path.join(volumePath, volumeName)]);
+ expect(options.includePaths).toEqual([path.join(volumePath, volumeName)]);
});
test("should correctly mix relative and absolute patterns", () => {
@@ -73,7 +75,7 @@ describe("executeBackup - include / exclude patterns", () => {
const options = createBackupOptions(schedule, volumePath, signal);
// assert
- expect(options.include).toEqual([
+ expect(options.includePatterns).toEqual([
path.join(volumePath, relativeInclude),
path.join(volumePath, "anchored/include"),
]);
@@ -91,7 +93,8 @@ describe("executeBackup - include / exclude patterns", () => {
const options = createBackupOptions(schedule, "/var/lib/zerobyte/volumes/vol999/_data", signal);
// assert
- expect(options.include).toEqual([]);
+ expect(options.includePaths).toEqual([]);
+ expect(options.includePatterns).toEqual([]);
expect(options.exclude).toEqual([]);
});
@@ -124,10 +127,24 @@ describe("executeBackup - include / exclude patterns", () => {
const options = createBackupOptions(schedule, volumePath, signal);
- expect(options.include).toEqual([
+ expect(options.includePatterns).toEqual([
path.join(volumePath, "**/*.xyz"),
path.join(volumePath, "*.zip"),
`!${path.join(volumePath, "**/*.tmp")}`,
]);
});
+
+ test("keeps selected include paths separate from include patterns", () => {
+ const volumePath = "/var/lib/zerobyte/volumes/vol123/_data";
+ const schedule = createSchedule({
+ includePaths: ["/movies [1]"],
+ includePatterns: ["**/*.txt"],
+ });
+ const signal = new AbortController().signal;
+
+ const options = createBackupOptions(schedule, volumePath, signal);
+
+ expect(options.includePaths).toEqual([path.join(volumePath, "movies [1]")]);
+ expect(options.includePatterns).toEqual([path.join(volumePath, "**/*.txt")]);
+ });
});
diff --git a/app/server/modules/backups/backup.helpers.ts b/app/server/modules/backups/backup.helpers.ts
index f3b05ee8..cc99dfbb 100644
--- a/app/server/modules/backups/backup.helpers.ts
+++ b/app/server/modules/backups/backup.helpers.ts
@@ -53,7 +53,10 @@ export const createBackupOptions = (schedule: BackupSchedule, volumePath: string
signal,
exclude: schedule.excludePatterns ? schedule.excludePatterns.map((p) => processPattern(p, volumePath)) : undefined,
excludeIfPresent: schedule.excludeIfPresent ?? undefined,
- include: schedule.includePatterns
+ includePaths: schedule.includePaths
+ ? schedule.includePaths.map((p) => processPattern(p, volumePath, true))
+ : undefined,
+ includePatterns: schedule.includePatterns
? schedule.includePatterns.map((p) => processPattern(p, volumePath, true))
: undefined,
customResticParams: schedule.customResticParams ?? undefined,
diff --git a/app/server/modules/backups/backups.dto.ts b/app/server/modules/backups/backups.dto.ts
index 5cd08fa6..bfb352b8 100644
--- a/app/server/modules/backups/backups.dto.ts
+++ b/app/server/modules/backups/backups.dto.ts
@@ -27,6 +27,7 @@ const backupScheduleSchema = z.object({
retentionPolicy: retentionPolicySchema.nullable(),
excludePatterns: z.array(z.string()).nullable(),
excludeIfPresent: z.array(z.string()).nullable(),
+ includePaths: z.array(z.string()).nullable(),
includePatterns: z.array(z.string()).nullable(),
oneFileSystem: z.boolean(),
customResticParams: z.array(z.string()).nullable(),
@@ -122,6 +123,7 @@ export const createBackupScheduleBody = z.object({
retentionPolicy: retentionPolicySchema.optional(),
excludePatterns: z.array(z.string()).optional(),
excludeIfPresent: z.array(z.string()).optional(),
+ includePaths: z.array(z.string()).optional(),
includePatterns: z.array(z.string()).optional(),
oneFileSystem: z.boolean().optional(),
tags: z.array(z.string()).optional(),
@@ -158,6 +160,7 @@ export const updateBackupScheduleBody = z.object({
retentionPolicy: retentionPolicySchema.optional(),
excludePatterns: z.array(z.string()).optional(),
excludeIfPresent: z.array(z.string()).optional(),
+ includePaths: z.array(z.string()).optional(),
includePatterns: z.array(z.string()).optional(),
oneFileSystem: z.boolean().optional(),
tags: z.array(z.string()).optional(),
diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts
index 1c14080f..ff2571eb 100644
--- a/app/server/modules/backups/backups.service.ts
+++ b/app/server/modules/backups/backups.service.ts
@@ -147,6 +147,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
retentionPolicy: data.retentionPolicy ?? null,
excludePatterns: data.excludePatterns ?? [],
excludeIfPresent: data.excludeIfPresent ?? [],
+ includePaths: data.includePaths ?? [],
includePatterns: data.includePatterns ?? [],
oneFileSystem: data.oneFileSystem,
customResticParams: data.customResticParams ?? [],
diff --git a/app/server/modules/lifecycle/migrations.ts b/app/server/modules/lifecycle/migrations.ts
index 9064b8ab..5dfde9c4 100644
--- a/app/server/modules/lifecycle/migrations.ts
+++ b/app/server/modules/lifecycle/migrations.ts
@@ -3,6 +3,7 @@ import { v00001 } from "./migrations/00001-retag-snapshots";
import { v00002 } from "./migrations/00002-isolate-restic-passwords";
import { v00003 } from "./migrations/00003-assign-organization";
import { v00004 } from "./migrations/00004-concat-path-name";
+import { v00005 } from "./migrations/00005-split-backup-include-paths";
import { sql } from "drizzle-orm";
import { appMetadataTable, usersTable } from "../../db/schema";
import { db } from "../../db/db";
@@ -37,7 +38,7 @@ type MigrationEntity = {
dependsOn?: string[];
};
-const registry: MigrationEntity[] = [v00001, v00002, v00003, v00004];
+const registry: MigrationEntity[] = [v00001, v00002, v00003, v00004, v00005];
export const runMigrations = async () => {
const userCount = await db.select({ count: sql`count(*)` }).from(usersTable);
@@ -77,7 +78,7 @@ export const runMigrations = async () => {
"Seek support by opening an issue on the Zerobyte GitHub repository if you need assistance.",
"================================================================================",
];
- err.forEach((line) => logger.error(line));
+ err.forEach(logger.error);
process.exit(1);
}
}
@@ -105,7 +106,7 @@ export const runMigrations = async () => {
"on the Zerobyte GitHub repository if you need assistance.",
"================================================================================",
];
- err.forEach((line) => logger.error(line));
+ err.forEach(logger.error);
process.exit(1);
}
}
diff --git a/app/server/modules/lifecycle/migrations/00005-split-backup-include-paths.ts b/app/server/modules/lifecycle/migrations/00005-split-backup-include-paths.ts
new file mode 100644
index 00000000..6d543e7b
--- /dev/null
+++ b/app/server/modules/lifecycle/migrations/00005-split-backup-include-paths.ts
@@ -0,0 +1,55 @@
+import { eq } from "drizzle-orm";
+import { logger } from "@zerobyte/core/node";
+import { db } from "../../../db/db";
+import { backupSchedulesTable } from "../../../db/schema";
+import { toMessage } from "~/server/utils/errors";
+
+const isGlobPattern = (value: string) => /[*?[\]]/.test(value);
+
+const execute = async () => {
+ const errors: Array<{ name: string; error: string }> = [];
+ const schedules = await db.query.backupSchedulesTable.findMany();
+ let migratedCount = 0;
+
+ for (const schedule of schedules) {
+ if (schedule.includePaths?.length || !schedule.includePatterns?.length) {
+ continue;
+ }
+
+ try {
+ const existingIncludePatterns = schedule.includePatterns ?? [];
+ const includePaths = existingIncludePatterns.filter((value) => !isGlobPattern(value));
+ if (includePaths.length === 0) {
+ continue;
+ }
+
+ const includePatterns = existingIncludePatterns.filter(isGlobPattern);
+
+ await db
+ .update(backupSchedulesTable)
+ .set({
+ includePaths,
+ includePatterns,
+ updatedAt: Date.now(),
+ })
+ .where(eq(backupSchedulesTable.id, schedule.id));
+
+ migratedCount += 1;
+ } catch (error) {
+ errors.push({
+ name: `backup-schedule:${schedule.id}`,
+ error: toMessage(error),
+ });
+ }
+ }
+
+ logger.info(`Migration 00005-split-backup-include-paths updated ${migratedCount} backup schedules.`);
+
+ return { success: errors.length === 0, errors };
+};
+
+export const v00005 = {
+ execute,
+ id: "00005-split-backup-include-paths",
+ type: "maintenance" as const,
+};
diff --git a/e2e/0002-backup-restore.spec.ts b/e2e/0002-backup-restore.spec.ts
index 20ab7555..817c4061 100644
--- a/e2e/0002-backup-restore.spec.ts
+++ b/e2e/0002-backup-restore.spec.ts
@@ -16,6 +16,7 @@ type ScenarioOptions = {
includePatterns?: string;
excludePatterns?: string;
excludeIfPresent?: string;
+ selectedPaths?: string[];
};
type BackupJobOptions = ScenarioOptions & {
@@ -99,6 +100,15 @@ async function createBackupJob(page: Page, options: BackupJobOptions) {
if (options.includePatterns) {
await page.getByLabel("Additional include patterns").fill(options.includePatterns);
}
+ if (options.selectedPaths) {
+ for (const selectedPath of options.selectedPaths) {
+ const escapedPath = path.posix.basename(selectedPath).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ await page
+ .getByRole("button", { name: new RegExp(escapedPath) })
+ .getByRole("checkbox")
+ .click();
+ }
+ }
if (options.excludePatterns) {
await page.getByLabel("Exclusion patterns").fill(options.excludePatterns);
}
@@ -462,3 +472,38 @@ test("backup respects include globs, exclusion patterns, and exclude-if-present"
await expect(page.getByRole("button", { name: /\.nobackup/ })).toBeVisible();
await expect(page.getByRole("button", { name: /blocked\.xyz/ })).toHaveCount(0);
});
+
+test("backup can include a selected folder whose name contains brackets", async ({ page }, testInfo) => {
+ const runId = getRunId(testInfo);
+ const names = getScenarioNames(runId);
+ const bracketDir = `movies [${runId}]`;
+ const bracketPath = path.join(testDataPath, bracketDir);
+ const fileName = `inside-${runId}.txt`;
+
+ fs.mkdirSync(bracketPath, { recursive: true });
+ fs.writeFileSync(path.join(bracketPath, fileName), "bracket path content");
+
+ await gotoAndWaitForAppReady(page, "/");
+ await expect(page).toHaveURL("/volumes");
+
+ await createBackupScenario(page, names, {
+ selectedPaths: [`/${bracketDir}`],
+ });
+
+ await page.getByRole("button", { name: "Backup now" }).click();
+ await expect(page.getByText("Backup started successfully")).toBeVisible();
+ await expect(page.getByText("✓ Success")).toBeVisible({ timeout: 30000 });
+
+ await page
+ .getByRole("button", { name: /\d+ B$/ })
+ .first()
+ .click();
+ const bracketFolderRow = page.getByRole("button", {
+ name: new RegExp(bracketDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
+ });
+ await expect(bracketFolderRow).toBeVisible();
+ await bracketFolderRow.locator("svg").first().click();
+ await expect(page.getByRole("button", { name: new RegExp(fileName.replace(".", "\\.")) })).toBeVisible({
+ timeout: 15000,
+ });
+});
diff --git a/packages/core/src/restic/commands/__tests__/backup.test.ts b/packages/core/src/restic/commands/__tests__/backup.test.ts
index fdd0e14b..476c253f 100644
--- a/packages/core/src/restic/commands/__tests__/backup.test.ts
+++ b/packages/core/src/restic/commands/__tests__/backup.test.ts
@@ -51,7 +51,7 @@ const config = {
type SetupOptions = {
spawnResult?: Partial;
- onSpawnCall?: (params: SafeSpawnParams) => void;
+ onSpawnCall?: (params: SafeSpawnParams) => void | Promise;
};
/**
@@ -64,8 +64,12 @@ const setup = ({ spawnResult = {}, onSpawnCall }: SetupOptions = {}) => {
const cleanupSpy = spyOn(cleanupModule, "cleanupTemporaryKeys").mockImplementation(() => Promise.resolve());
spyOn(spawnModule, "safeSpawn").mockImplementation((params) => {
capturedArgs = params.args;
- onSpawnCall?.(params);
- return Promise.resolve({ exitCode: 0, summary: VALID_SUMMARY, error: "", ...spawnResult });
+ return Promise.resolve(onSpawnCall?.(params)).then(() => ({
+ exitCode: 0,
+ summary: VALID_SUMMARY,
+ error: "",
+ ...spawnResult,
+ }));
});
return {
@@ -117,7 +121,7 @@ describe("backup command", () => {
"/mnt/data",
{
organizationId: "org-1",
- include: ["/mnt/data/docs", "/mnt/data/photos"],
+ includePatterns: ["/mnt/data/docs", "/mnt/data/photos"],
},
mockDeps,
);
@@ -126,6 +130,42 @@ describe("backup command", () => {
expect(getArgs()).not.toContain("/mnt/data");
});
+ test("passes include paths via --files-from-raw and include patterns via --files-from", async () => {
+ const literalDir = "/mnt/data/movies [1]";
+ let rawIncludeContent = "";
+ let patternIncludeContent = "";
+ const { hasFlag } = setup({
+ onSpawnCall: async (params) => {
+ const rawIncludeIndex = params.args.indexOf("--files-from-raw");
+ const patternIncludeIndex = params.args.indexOf("--files-from");
+
+ if (rawIncludeIndex > -1) {
+ rawIncludeContent = await Bun.file(params.args[rawIncludeIndex + 1]!).text();
+ }
+
+ if (patternIncludeIndex > -1) {
+ patternIncludeContent = await Bun.file(params.args[patternIncludeIndex + 1]!).text();
+ }
+ },
+ });
+
+ await backup(
+ config,
+ "/mnt/data",
+ {
+ organizationId: "org-1",
+ includePaths: [literalDir],
+ includePatterns: ["/mnt/data/**/*.zip"],
+ },
+ mockDeps,
+ );
+
+ expect(hasFlag("--files-from-raw")).toBe(true);
+ expect(hasFlag("--files-from")).toBe(true);
+ expect(rawIncludeContent).toBe(`${literalDir}\0`);
+ expect(patternIncludeContent).toBe("/mnt/data/**/*.zip");
+ });
+
test("adds --tag for each entry in options.tags", async () => {
const { getOptionValues } = setup();
await backup(
diff --git a/packages/core/src/restic/commands/backup.ts b/packages/core/src/restic/commands/backup.ts
index b0022db8..1a9078e1 100644
--- a/packages/core/src/restic/commands/backup.ts
+++ b/packages/core/src/restic/commands/backup.ts
@@ -20,7 +20,8 @@ export const backup = async (
organizationId: string;
exclude?: string[];
excludeIfPresent?: string[];
- include?: string[];
+ includePaths?: string[];
+ includePatterns?: string[];
tags?: string[];
oneFileSystem?: boolean;
compressionMode?: CompressionMode;
@@ -50,23 +51,35 @@ export const backup = async (
}
let includeFile: string | null = null;
- const usesSourceArg = !options.include || options.include.length === 0;
+ let rawIncludeFile: string | null = null;
+ const usesSourceArg =
+ (!options.includePaths || options.includePaths.length === 0) &&
+ (!options.includePatterns || options.includePatterns.length === 0);
- if (options.include && options.include.length > 0) {
+ if (options.includePatterns?.length) {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-"));
includeFile = path.join(tmp, "include.txt");
- await fs.writeFile(includeFile, options.include.join("\n"), "utf-8");
+ await fs.writeFile(includeFile, options.includePatterns.join("\n"), "utf-8");
args.push("--files-from", includeFile);
}
+ if (options.includePaths?.length) {
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-include-raw-"));
+ rawIncludeFile = path.join(tmp, "include.raw");
+
+ await fs.writeFile(rawIncludeFile, Buffer.from(`${options.includePaths.join("\0")}\0`, "utf-8"));
+
+ args.push("--files-from-raw", rawIncludeFile);
+ }
+
for (const exclude of deps.defaultExcludes) {
args.push("--exclude", exclude);
}
let excludeFile: string | null = null;
- if (options.exclude && options.exclude.length > 0) {
+ if (options.exclude?.length) {
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "zerobyte-restic-exclude-"));
excludeFile = path.join(tmp, "exclude.txt");
@@ -75,13 +88,13 @@ export const backup = async (
args.push("--exclude-file", excludeFile);
}
- if (options.excludeIfPresent && options.excludeIfPresent.length > 0) {
+ if (options.excludeIfPresent?.length) {
for (const filename of options.excludeIfPresent) {
args.push("--exclude-if-present", filename);
}
}
- if (options.customResticParams && options.customResticParams.length > 0) {
+ if (options.customResticParams?.length) {
const validationError = validateCustomResticParams(options.customResticParams);
if (validationError) {
throw new Error(`Invalid customResticParams: ${validationError}`);
@@ -147,6 +160,9 @@ export const backup = async (
if (includeFile) {
await fs.unlink(includeFile).catch(() => {});
}
+ if (rawIncludeFile) {
+ await fs.unlink(rawIncludeFile).catch(() => {});
+ }
if (excludeFile) {
await fs.unlink(excludeFile).catch(() => {});
}