refactor: put export endpoint in system controller
This commit is contained in:
parent
081a10afeb
commit
3f550678db
9 changed files with 198 additions and 247 deletions
20
README.md
20
README.md
|
|
@ -238,26 +238,6 @@ Zerobyte allows you to easily restore your data from backups. To restore data, n
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Exporting configuration
|
|
||||||
|
|
||||||
Zerobyte allows you to export your configuration for backup, migration, or documentation purposes.
|
|
||||||
|
|
||||||
To export, click the "Export" button in Settings. A dialog will appear with options to:
|
|
||||||
|
|
||||||
- **Include metadata** - Include IDs, timestamps, and runtime state of entities
|
|
||||||
- **Secrets handling**:
|
|
||||||
- **Exclude** - Remove sensitive fields like passwords and API keys
|
|
||||||
- **Keep encrypted** - Export secrets in encrypted form (requires the same recovery key to decrypt on import)
|
|
||||||
- **Decrypt** - Export secrets as plaintext (use with caution)
|
|
||||||
- **Include recovery key** - Include the master encryption key for all repositories
|
|
||||||
- **Include password hash** - Include the hashed user passwords (enables future import workflows)
|
|
||||||
|
|
||||||
Export requires password verification for security. You must enter your password to confirm your identity before any configuration can be exported.
|
|
||||||
|
|
||||||
Export is downloaded as JSON file that can be used for reference or future import functionality.
|
|
||||||
|
|
||||||
> **Sensitive data handling**: Some sensitive data from earlier versions may not be encrypted in the database. Additionally, nested configuration objects within config fields are exported as-is and not processed separately. Review exported data carefully before sharing, especially when using the "Decrypt" secrets option.
|
|
||||||
|
|
||||||
## Third-Party Software
|
## Third-Party Software
|
||||||
|
|
||||||
This project includes the following third-party software components:
|
This project includes the following third-party software components:
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,7 @@ export const downloadResticPassword = <ThrowOnError extends boolean = false>(opt
|
||||||
* Export full configuration including all volumes, repositories, backup schedules, and notifications
|
* Export full configuration including all volumes, repositories, backup schedules, and notifications
|
||||||
*/
|
*/
|
||||||
export const exportFullConfig = <ThrowOnError extends boolean = false>(options?: Options<ExportFullConfigData, ThrowOnError>) => (options?.client ?? client).post<ExportFullConfigResponses, ExportFullConfigErrors, ThrowOnError>({
|
export const exportFullConfig = <ThrowOnError extends boolean = false>(options?: Options<ExportFullConfigData, ThrowOnError>) => (options?.client ?? client).post<ExportFullConfigResponses, ExportFullConfigErrors, ThrowOnError>({
|
||||||
url: '/api/v1/config/export',
|
url: '/api/v1/system/export',
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -3345,7 +3345,7 @@ export type ExportFullConfigData = {
|
||||||
};
|
};
|
||||||
path?: never;
|
path?: never;
|
||||||
query?: never;
|
query?: never;
|
||||||
url: '/api/v1/config/export';
|
url: '/api/v1/system/export';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExportFullConfigErrors = {
|
export type ExportFullConfigErrors = {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import { volumeController } from "./modules/volumes/volume.controller";
|
||||||
import { backupScheduleController } from "./modules/backups/backups.controller";
|
import { backupScheduleController } from "./modules/backups/backups.controller";
|
||||||
import { eventsController } from "./modules/events/events.controller";
|
import { eventsController } from "./modules/events/events.controller";
|
||||||
import { notificationsController } from "./modules/notifications/notifications.controller";
|
import { notificationsController } from "./modules/notifications/notifications.controller";
|
||||||
import { configExportController } from "./modules/lifecycle/config-export.controller";
|
|
||||||
import { handleServiceError } from "./utils/errors";
|
import { handleServiceError } from "./utils/errors";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
import { config } from "./core/config";
|
import { config } from "./core/config";
|
||||||
|
|
@ -61,8 +60,7 @@ export const createApp = () => {
|
||||||
.route("/api/v1/backups", backupScheduleController)
|
.route("/api/v1/backups", backupScheduleController)
|
||||||
.route("/api/v1/notifications", notificationsController)
|
.route("/api/v1/notifications", notificationsController)
|
||||||
.route("/api/v1/system", systemController)
|
.route("/api/v1/system", systemController)
|
||||||
.route("/api/v1/events", eventsController)
|
.route("/api/v1/events", eventsController);
|
||||||
.route("/api/v1/config", configExportController);
|
|
||||||
|
|
||||||
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
app.get("/api/v1/openapi.json", generalDescriptor(app));
|
||||||
app.get("/api/v1/docs", requireAuth, scalarDescriptor);
|
app.get("/api/v1/docs", requireAuth, scalarDescriptor);
|
||||||
|
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { validator } from "hono-openapi";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import type { Context } from "hono";
|
|
||||||
import {
|
|
||||||
backupScheduleNotificationsTable,
|
|
||||||
backupScheduleMirrorsTable,
|
|
||||||
usersTable,
|
|
||||||
type BackupSchedule,
|
|
||||||
type BackupScheduleNotification,
|
|
||||||
type BackupScheduleMirror,
|
|
||||||
volumesTable,
|
|
||||||
repositoriesTable,
|
|
||||||
backupSchedulesTable,
|
|
||||||
notificationDestinationsTable,
|
|
||||||
} from "../../db/schema";
|
|
||||||
import { db } from "../../db/db";
|
|
||||||
import { logger } from "../../utils/logger";
|
|
||||||
import { authService } from "../auth/auth.service";
|
|
||||||
import { fullExportBodySchema, fullExportDto, type FullExportBody } from "./config-export.dto";
|
|
||||||
import { requireAuth } from "../auth/auth.middleware";
|
|
||||||
import { toMessage } from "~/server/utils/errors";
|
|
||||||
|
|
||||||
const METADATA_KEYS = {
|
|
||||||
timestamps: [
|
|
||||||
"createdAt",
|
|
||||||
"updatedAt",
|
|
||||||
"lastBackupAt",
|
|
||||||
"nextBackupAt",
|
|
||||||
"lastHealthCheck",
|
|
||||||
"lastChecked",
|
|
||||||
"lastCopyAt",
|
|
||||||
],
|
|
||||||
runtimeState: [
|
|
||||||
"status",
|
|
||||||
"lastError",
|
|
||||||
"lastBackupStatus",
|
|
||||||
"lastBackupError",
|
|
||||||
"hasDownloadedResticPassword",
|
|
||||||
"lastCopyStatus",
|
|
||||||
"lastCopyError",
|
|
||||||
"sortOrder",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALL_METADATA_KEYS = [...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
|
|
||||||
|
|
||||||
function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMetadata: boolean): Partial<T> {
|
|
||||||
if (includeMetadata) {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
const result = { ...obj };
|
|
||||||
for (const key of ALL_METADATA_KEYS) {
|
|
||||||
delete result[key as keyof T];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyExportPassword(
|
|
||||||
c: Context,
|
|
||||||
password: string,
|
|
||||||
): Promise<{ valid: true; userId: number } | { valid: false; error: string }> {
|
|
||||||
const user = c.get("user");
|
|
||||||
if (!user) {
|
|
||||||
return { valid: false, error: "Not authenticated" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await authService.verifyPassword(user.id, password);
|
|
||||||
if (!isValid) {
|
|
||||||
return { valid: false, error: "Incorrect password" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true, userId: user.id };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportEntity(entity: Record<string, unknown>, params: FullExportBody) {
|
|
||||||
return filterMetadataOut(entity, params.includeMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportEntities<T extends Record<string, unknown>>(entities: T[], params: FullExportBody) {
|
|
||||||
return Promise.all(entities.map((e) => exportEntity(e, params)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const transformBackupSchedules = (
|
|
||||||
schedules: BackupSchedule[],
|
|
||||||
scheduleNotifications: BackupScheduleNotification[],
|
|
||||||
scheduleMirrors: BackupScheduleMirror[],
|
|
||||||
params: FullExportBody,
|
|
||||||
) => {
|
|
||||||
return schedules.map((schedule) => {
|
|
||||||
const assignments = scheduleNotifications
|
|
||||||
.filter((sn) => sn.scheduleId === schedule.id)
|
|
||||||
.map((sn) => filterMetadataOut(sn, params.includeMetadata));
|
|
||||||
|
|
||||||
const mirrors = scheduleMirrors
|
|
||||||
.filter((sm) => sm.scheduleId === schedule.id)
|
|
||||||
.map((sm) => filterMetadataOut(sm, params.includeMetadata));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...filterMetadataOut(schedule, params.includeMetadata),
|
|
||||||
notifications: assignments,
|
|
||||||
mirrors,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const configExportController = new Hono()
|
|
||||||
.use(requireAuth)
|
|
||||||
.post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
|
|
||||||
try {
|
|
||||||
const params = c.req.valid("json");
|
|
||||||
|
|
||||||
const verification = await verifyExportPassword(c, params.password);
|
|
||||||
if (!verification.valid) {
|
|
||||||
return c.json({ error: verification.error }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, scheduleMirrors, users] =
|
|
||||||
await Promise.all([
|
|
||||||
db.select().from(volumesTable),
|
|
||||||
db.select().from(repositoriesTable),
|
|
||||||
db.select().from(backupSchedulesTable),
|
|
||||||
db.select().from(notificationDestinationsTable),
|
|
||||||
db.select().from(backupScheduleNotificationsTable),
|
|
||||||
db.select().from(backupScheduleMirrorsTable),
|
|
||||||
db.select().from(usersTable),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const backupSchedules = transformBackupSchedules(
|
|
||||||
backupSchedulesRaw,
|
|
||||||
scheduleNotifications,
|
|
||||||
scheduleMirrors,
|
|
||||||
params,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [exportVolumes, exportRepositories, exportNotifications, exportUsers] = await Promise.all([
|
|
||||||
exportEntities(volumes, params),
|
|
||||||
exportEntities(repositories, params),
|
|
||||||
exportEntities(notifications, params),
|
|
||||||
exportEntities(users, params),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return c.json({
|
|
||||||
version: 1,
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
volumes: exportVolumes,
|
|
||||||
repositories: exportRepositories,
|
|
||||||
backupSchedules,
|
|
||||||
notificationDestinations: exportNotifications,
|
|
||||||
users: exportUsers,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`Config export failed: ${toMessage(err)}`);
|
|
||||||
return c.json({ error: toMessage(err) }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
import { type } from "arktype";
|
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
|
||||||
|
|
||||||
export const fullExportBodySchema = type({
|
|
||||||
includeMetadata: "boolean = false",
|
|
||||||
password: "string",
|
|
||||||
});
|
|
||||||
|
|
||||||
export type FullExportBody = typeof fullExportBodySchema.infer;
|
|
||||||
|
|
||||||
const exportResponseSchema = type({
|
|
||||||
version: "number",
|
|
||||||
exportedAt: "string?",
|
|
||||||
recoveryKey: "string?",
|
|
||||||
volumes: "unknown[]?",
|
|
||||||
repositories: "unknown[]?",
|
|
||||||
backupSchedules: "unknown[]?",
|
|
||||||
notificationDestinations: "unknown[]?",
|
|
||||||
users: type({
|
|
||||||
id: "number?",
|
|
||||||
username: "string",
|
|
||||||
passwordHash: "string?",
|
|
||||||
createdAt: "number?",
|
|
||||||
updatedAt: "number?",
|
|
||||||
hasDownloadedResticPassword: "boolean?",
|
|
||||||
})
|
|
||||||
.array()
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorResponseSchema = type({
|
|
||||||
error: "string",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fullExportDto = describeRoute({
|
|
||||||
description: "Export full configuration including all volumes, repositories, backup schedules, and notifications",
|
|
||||||
operationId: "exportFullConfig",
|
|
||||||
tags: ["Config Export"],
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Full configuration export",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(exportResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
401: {
|
|
||||||
description: "Password required for export or authentication failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Export failed",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: resolver(errorResponseSchema),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { validator } from "hono-openapi";
|
||||||
import {
|
import {
|
||||||
downloadResticPasswordBodySchema,
|
downloadResticPasswordBodySchema,
|
||||||
downloadResticPasswordDto,
|
downloadResticPasswordDto,
|
||||||
|
fullExportBodySchema,
|
||||||
|
fullExportDto,
|
||||||
getUpdatesDto,
|
getUpdatesDto,
|
||||||
systemInfoDto,
|
systemInfoDto,
|
||||||
type SystemInfoDto,
|
type SystemInfoDto,
|
||||||
|
|
@ -61,4 +63,20 @@ export const systemController = new Hono()
|
||||||
return c.json({ message: "Failed to read Restic password file" }, 500);
|
return c.json({ message: "Failed to read Restic password file" }, 500);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.post("/export", fullExportDto, validator("json", fullExportBodySchema), async (c) => {
|
||||||
|
const user = c.get("user");
|
||||||
|
const body = c.req.valid("json");
|
||||||
|
|
||||||
|
const [dbUser] = await db.select().from(usersTable).where(eq(usersTable.id, user.id));
|
||||||
|
|
||||||
|
if (!dbUser) {
|
||||||
|
return c.json({ message: "User not found" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await Bun.password.verify(body.password, dbUser.passwordHash);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return c.json({ message: "Incorrect password" }, 401);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,66 @@ export const downloadResticPasswordDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fullExportBodySchema = type({
|
||||||
|
includeMetadata: "boolean = false",
|
||||||
|
password: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FullExportBody = typeof fullExportBodySchema.infer;
|
||||||
|
|
||||||
|
const exportResponseSchema = type({
|
||||||
|
version: "number",
|
||||||
|
exportedAt: "string?",
|
||||||
|
recoveryKey: "string?",
|
||||||
|
volumes: "unknown[]?",
|
||||||
|
repositories: "unknown[]?",
|
||||||
|
backupSchedules: "unknown[]?",
|
||||||
|
notificationDestinations: "unknown[]?",
|
||||||
|
users: type({
|
||||||
|
id: "number?",
|
||||||
|
username: "string",
|
||||||
|
passwordHash: "string?",
|
||||||
|
createdAt: "number?",
|
||||||
|
updatedAt: "number?",
|
||||||
|
hasDownloadedResticPassword: "boolean?",
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorResponseSchema = type({
|
||||||
|
error: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fullExportDto = describeRoute({
|
||||||
|
description: "Export full configuration including all volumes, repositories, backup schedules, and notifications",
|
||||||
|
operationId: "exportFullConfig",
|
||||||
|
tags: ["Config Export"],
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Full configuration export",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(exportResponseSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Password required for export or authentication failed",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(errorResponseSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: "Export failed",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(errorResponseSchema),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
import { getCapabilities } from "../../core/capabilities";
|
import { getCapabilities } from "../../core/capabilities";
|
||||||
import { config } from "../../core/config";
|
import { config } from "../../core/config";
|
||||||
import type { UpdateInfoDto } from "./system.dto";
|
import type { FullExportBody, UpdateInfoDto } from "./system.dto";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { cache } from "../../utils/cache";
|
import { cache } from "../../utils/cache";
|
||||||
import { logger } from "~/server/utils/logger";
|
import { logger } from "~/server/utils/logger";
|
||||||
|
import { db } from "~/server/db/db";
|
||||||
|
import {
|
||||||
|
backupScheduleMirrorsTable,
|
||||||
|
backupScheduleNotificationsTable,
|
||||||
|
backupSchedulesTable,
|
||||||
|
notificationDestinationsTable,
|
||||||
|
repositoriesTable,
|
||||||
|
usersTable,
|
||||||
|
volumesTable,
|
||||||
|
type BackupScheduleMirror,
|
||||||
|
type BackupScheduleNotification,
|
||||||
|
type BackupSchedule,
|
||||||
|
} from "~/server/db/schema";
|
||||||
|
|
||||||
const CACHE_TTL = 60 * 60;
|
const CACHE_TTL = 60 * 60;
|
||||||
|
|
||||||
|
|
@ -90,7 +103,106 @@ const getUpdates = async (): Promise<UpdateInfoDto> => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const METADATA_KEYS = {
|
||||||
|
timestamps: [
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt",
|
||||||
|
"lastBackupAt",
|
||||||
|
"nextBackupAt",
|
||||||
|
"lastHealthCheck",
|
||||||
|
"lastChecked",
|
||||||
|
"lastCopyAt",
|
||||||
|
],
|
||||||
|
runtimeState: [
|
||||||
|
"status",
|
||||||
|
"lastError",
|
||||||
|
"lastBackupStatus",
|
||||||
|
"lastBackupError",
|
||||||
|
"hasDownloadedResticPassword",
|
||||||
|
"lastCopyStatus",
|
||||||
|
"lastCopyError",
|
||||||
|
"sortOrder",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALL_METADATA_KEYS = [...METADATA_KEYS.timestamps, ...METADATA_KEYS.runtimeState];
|
||||||
|
|
||||||
|
function filterMetadataOut<T extends Record<string, unknown>>(obj: T, includeMetadata: boolean): Partial<T> {
|
||||||
|
if (includeMetadata) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
const result = { ...obj };
|
||||||
|
for (const key of ALL_METADATA_KEYS) {
|
||||||
|
delete result[key as keyof T];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportEntity(entity: Record<string, unknown>, params: FullExportBody) {
|
||||||
|
return filterMetadataOut(entity, params.includeMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportEntities<T extends Record<string, unknown>>(entities: T[], params: FullExportBody) {
|
||||||
|
return Promise.all(entities.map((e) => exportEntity(e, params)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformBackupSchedules = (
|
||||||
|
schedules: BackupSchedule[],
|
||||||
|
scheduleNotifications: BackupScheduleNotification[],
|
||||||
|
scheduleMirrors: BackupScheduleMirror[],
|
||||||
|
params: FullExportBody,
|
||||||
|
) => {
|
||||||
|
return schedules.map((schedule) => {
|
||||||
|
const assignments = scheduleNotifications
|
||||||
|
.filter((sn) => sn.scheduleId === schedule.id)
|
||||||
|
.map((sn) => filterMetadataOut(sn, params.includeMetadata));
|
||||||
|
|
||||||
|
const mirrors = scheduleMirrors
|
||||||
|
.filter((sm) => sm.scheduleId === schedule.id)
|
||||||
|
.map((sm) => filterMetadataOut(sm, params.includeMetadata));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...filterMetadataOut(schedule, params.includeMetadata),
|
||||||
|
notifications: assignments,
|
||||||
|
mirrors,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportConfig = async (params: FullExportBody) => {
|
||||||
|
const [volumes, repositories, backupSchedulesRaw, notifications, scheduleNotifications, scheduleMirrors, users] =
|
||||||
|
await Promise.all([
|
||||||
|
db.select().from(volumesTable),
|
||||||
|
db.select().from(repositoriesTable),
|
||||||
|
db.select().from(backupSchedulesTable),
|
||||||
|
db.select().from(notificationDestinationsTable),
|
||||||
|
db.select().from(backupScheduleNotificationsTable),
|
||||||
|
db.select().from(backupScheduleMirrorsTable),
|
||||||
|
db.select().from(usersTable),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const backupSchedules = transformBackupSchedules(backupSchedulesRaw, scheduleNotifications, scheduleMirrors, params);
|
||||||
|
|
||||||
|
const [exportVolumes, exportRepositories, exportNotifications, exportUsers] = await Promise.all([
|
||||||
|
exportEntities(volumes, params),
|
||||||
|
exportEntities(repositories, params),
|
||||||
|
exportEntities(notifications, params),
|
||||||
|
exportEntities(users, params),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
volumes: exportVolumes,
|
||||||
|
repositories: exportRepositories,
|
||||||
|
backupSchedules,
|
||||||
|
notificationDestinations: exportNotifications,
|
||||||
|
users: exportUsers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const systemService = {
|
export const systemService = {
|
||||||
getSystemInfo,
|
getSystemInfo,
|
||||||
getUpdates,
|
getUpdates,
|
||||||
|
exportConfig,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue