feat(config-import): support mirrors, oneFileSystem, and optional flags
- Add mirrors support for backup schedules (copy to secondary repos) - Add oneFileSystem flag to prevent crossing filesystem boundaries - Support autoRemount=false for volumes (defaults to true) - Support enabled=false for notification destinations (defaults to true) - Optimize: move early return before dynamic imports in importBackupSchedules - Update example config and README with new fields documentation
This commit is contained in:
parent
9e9edd73a1
commit
7476897f87
3 changed files with 109 additions and 5 deletions
|
|
@ -127,6 +127,12 @@ async function importVolumes(volumes: unknown[]): Promise<void> {
|
||||||
}
|
}
|
||||||
await volumeService.createVolume(v.name, v.config as BackendConfig);
|
await volumeService.createVolume(v.name, v.config as BackendConfig);
|
||||||
logger.info(`Initialized volume from config: ${v.name}`);
|
logger.info(`Initialized volume from config: ${v.name}`);
|
||||||
|
|
||||||
|
// If autoRemount is explicitly false, update the volume (default is true)
|
||||||
|
if (v.autoRemount === false) {
|
||||||
|
await volumeService.updateVolume(v.name, { autoRemount: false });
|
||||||
|
logger.info(`Set autoRemount=false for volume: ${v.name}`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e instanceof Error ? e : new Error(String(e));
|
const err = e instanceof Error ? e : new Error(String(e));
|
||||||
logger.warn(`Volume not created: ${err.message}`);
|
logger.warn(`Volume not created: ${err.message}`);
|
||||||
|
|
@ -165,8 +171,14 @@ async function importNotificationDestinations(notificationDestinations: unknown[
|
||||||
if (!isRecord(n) || typeof n.name !== "string" || !isRecord(n.config) || typeof n.config.type !== "string") {
|
if (!isRecord(n) || typeof n.name !== "string" || !isRecord(n.config) || typeof n.config.type !== "string") {
|
||||||
throw new Error("Invalid notification destination entry");
|
throw new Error("Invalid notification destination entry");
|
||||||
}
|
}
|
||||||
await notificationsServiceModule.notificationsService.createDestination(n.name, n.config as NotificationConfig);
|
const created = await notificationsServiceModule.notificationsService.createDestination(n.name, n.config as NotificationConfig);
|
||||||
logger.info(`Initialized notification destination from config: ${n.name}`);
|
logger.info(`Initialized notification destination from config: ${n.name}`);
|
||||||
|
|
||||||
|
// If enabled is explicitly false, update the destination (default is true)
|
||||||
|
if (n.enabled === false) {
|
||||||
|
await notificationsServiceModule.notificationsService.updateDestination(created.id, { enabled: false });
|
||||||
|
logger.info(`Set enabled=false for notification destination: ${n.name}`);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const err = e instanceof Error ? e : new Error(String(e));
|
const err = e instanceof Error ? e : new Error(String(e));
|
||||||
logger.warn(`Notification destination not created: ${err.message}`);
|
logger.warn(`Notification destination not created: ${err.message}`);
|
||||||
|
|
@ -243,9 +255,10 @@ async function attachScheduleNotifications(
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importBackupSchedules(backupSchedules: unknown[]): Promise<void> {
|
async function importBackupSchedules(backupSchedules: unknown[]): Promise<void> {
|
||||||
|
if (!Array.isArray(backupSchedules) || backupSchedules.length === 0) return;
|
||||||
|
|
||||||
const backupServiceModule = await import("../backups/backups.service");
|
const backupServiceModule = await import("../backups/backups.service");
|
||||||
const notificationsServiceModule = await import("../notifications/notifications.service");
|
const notificationsServiceModule = await import("../notifications/notifications.service");
|
||||||
if (!Array.isArray(backupSchedules) || backupSchedules.length === 0) return;
|
|
||||||
|
|
||||||
const volumes = await db.query.volumesTable.findMany();
|
const volumes = await db.query.volumesTable.findMany();
|
||||||
const repositories = await db.query.repositoriesTable.findMany();
|
const repositories = await db.query.repositoriesTable.findMany();
|
||||||
|
|
@ -312,6 +325,7 @@ async function importBackupSchedules(backupSchedules: unknown[]): Promise<void>
|
||||||
excludePatterns: asStringArray(s.excludePatterns),
|
excludePatterns: asStringArray(s.excludePatterns),
|
||||||
excludeIfPresent: asStringArray(s.excludeIfPresent),
|
excludeIfPresent: asStringArray(s.excludeIfPresent),
|
||||||
includePatterns: asStringArray(s.includePatterns),
|
includePatterns: asStringArray(s.includePatterns),
|
||||||
|
oneFileSystem: typeof s.oneFileSystem === "boolean" ? s.oneFileSystem : undefined,
|
||||||
});
|
});
|
||||||
logger.info(`Initialized backup schedule from config: ${scheduleName}`);
|
logger.info(`Initialized backup schedule from config: ${scheduleName}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -323,6 +337,57 @@ async function importBackupSchedules(backupSchedules: unknown[]): Promise<void>
|
||||||
if (createdSchedule && Array.isArray(s.notifications) && s.notifications.length > 0) {
|
if (createdSchedule && Array.isArray(s.notifications) && s.notifications.length > 0) {
|
||||||
await attachScheduleNotifications(createdSchedule.id, s.notifications, destinationBySlug, notificationsServiceModule);
|
await attachScheduleNotifications(createdSchedule.id, s.notifications, destinationBySlug, notificationsServiceModule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (createdSchedule && Array.isArray(s.mirrors) && s.mirrors.length > 0) {
|
||||||
|
await attachScheduleMirrors(createdSchedule.id, s.mirrors, repoByName, backupServiceModule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attachScheduleMirrors(
|
||||||
|
scheduleId: number,
|
||||||
|
mirrors: unknown[],
|
||||||
|
repoByName: Map<string, { id: string; name: string }>,
|
||||||
|
backupServiceModule: typeof import("../backups/backups.service"),
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const mirrorConfigs: Array<{ repositoryId: string; enabled: boolean }> = [];
|
||||||
|
|
||||||
|
for (const m of mirrors) {
|
||||||
|
if (!isRecord(m)) continue;
|
||||||
|
|
||||||
|
// Support both repository name (string) and repository object with name
|
||||||
|
const repoName =
|
||||||
|
typeof m.repository === "string"
|
||||||
|
? m.repository
|
||||||
|
: typeof m.repositoryName === "string"
|
||||||
|
? m.repositoryName
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!repoName) {
|
||||||
|
logger.warn("Mirror missing repository name; skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = repoByName.get(repoName);
|
||||||
|
if (!repo) {
|
||||||
|
logger.warn(`Mirror repository '${repoName}' not found; skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mirrorConfigs.push({
|
||||||
|
repositoryId: repo.id,
|
||||||
|
enabled: typeof m.enabled === "boolean" ? m.enabled : true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mirrorConfigs.length === 0) return;
|
||||||
|
|
||||||
|
await backupServiceModule.backupsService.updateMirrors(scheduleId, { mirrors: mirrorConfigs });
|
||||||
|
logger.info(`Assigned ${mirrorConfigs.length} mirror(s) to backup schedule`);
|
||||||
|
} catch (e) {
|
||||||
|
const err = e instanceof Error ? e : new Error(String(e));
|
||||||
|
logger.warn(`Failed to assign mirrors to schedule: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,12 +313,34 @@ Note for importing existing local repositories (migration):
|
||||||
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
|
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
|
||||||
"includePatterns": ["important-folder"],
|
"includePatterns": ["important-folder"],
|
||||||
"excludePatterns": ["*.tmp", "*.log"],
|
"excludePatterns": ["*.tmp", "*.log"],
|
||||||
|
"excludeIfPresent": [".nobackup"],
|
||||||
|
"oneFileSystem": true,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"notifications": ["slack-alerts", "email-admin"]
|
"notifications": ["slack-alerts", "email-admin"],
|
||||||
|
"mirrors": [
|
||||||
|
{ "repository": "s3-repo" },
|
||||||
|
{ "repository": "lo2" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`notifications` can also be an array of objects:
|
**Fields:**
|
||||||
|
|
||||||
|
- `name`: Unique schedule name
|
||||||
|
- `volume`: Name of the source volume
|
||||||
|
- `repository`: Name of the primary destination repository
|
||||||
|
- `cronExpression`: Cron string for schedule timing
|
||||||
|
- `retentionPolicy`: Object with retention rules (`keepLast`, `keepHourly`, `keepDaily`, `keepWeekly`, `keepMonthly`, `keepYearly`, `keepWithinDuration`)
|
||||||
|
- `includePatterns` / `excludePatterns`: Arrays of file patterns
|
||||||
|
- `excludeIfPresent`: Array of filenames; if any of these files exist in a directory, that directory is excluded (e.g., `[".nobackup"]`)
|
||||||
|
- `oneFileSystem`: Boolean; if `true`, restic won't cross filesystem boundaries (useful when backing up `/` to avoid traversing into mounted volumes)
|
||||||
|
- `enabled`: Boolean
|
||||||
|
- `notifications`: Array of notification destination names or detailed objects (see below)
|
||||||
|
- `mirrors`: Array of mirror repositories (see below)
|
||||||
|
|
||||||
|
#### Notifications (detailed)
|
||||||
|
|
||||||
|
`notifications` can be strings (destination names) or objects with fine-grained control:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
[
|
||||||
|
|
@ -332,6 +354,18 @@ Note for importing existing local repositories (migration):
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Mirrors
|
||||||
|
|
||||||
|
Mirrors let you automatically copy snapshots to additional repositories after each backup.
|
||||||
|
Each mirror references a repository by name:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"mirrors": [
|
||||||
|
{ "repository": "s3-repo" },
|
||||||
|
{ "repository": "lo2", "enabled": false }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### User setup (automated)
|
### User setup (automated)
|
||||||
|
|
||||||
Zerobyte currently supports a **single user**.
|
Zerobyte currently supports a **single user**.
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,13 @@
|
||||||
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
|
"retentionPolicy": { "keepLast": 7, "keepDaily": 7 },
|
||||||
"includePatterns": ["important-folder"],
|
"includePatterns": ["important-folder"],
|
||||||
"excludePatterns": ["*.tmp", "*.log"],
|
"excludePatterns": ["*.tmp", "*.log"],
|
||||||
|
"excludeIfPresent": [".nobackup"],
|
||||||
|
"oneFileSystem": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"notifications": ["slack-alerts"]
|
"notifications": ["slack-alerts"],
|
||||||
|
"mirrors": [
|
||||||
|
{ "repository": "s3-repo" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"notificationDestinations": [
|
"notificationDestinations": [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue