Files
HiveGrid-Wireframes/hivegrid-cloud/packages/cli/src/commands/backup/manifest.ts
T

86 lines
2.7 KiB
TypeScript

import { createHash } from "node:crypto";
import { z } from "zod";
export const BACKUP_MANIFEST_VERSION = 1;
export const ComponentChecksumSchema = z
.object({
algorithm: z.literal("sha256"),
value: z.string().regex(/^[a-f0-9]{64}$/u, "expected lowercase hex sha256"),
})
.strict();
export const ComponentEntrySchema = z
.object({
path: z.string().min(1),
sizeBytes: z.number().int().nonnegative(),
checksum: ComponentChecksumSchema,
metadata: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
})
.strict();
export const EncryptionMetadataSchema = z
.object({
method: z.enum(["age", "gpg", "none"]),
recipient: z.string().optional(),
keyFingerprint: z.string().optional(),
})
.strict();
export const BackupManifestSchema = z
.object({
schemaVersion: z.literal(BACKUP_MANIFEST_VERSION),
backupId: z.string().min(1),
createdAt: z.string().datetime({ offset: false }),
creator: z
.object({
cliVersion: z.string().min(1),
host: z.string().optional(),
})
.strict(),
classification: z.enum(["daily", "weekly", "monthly"]).array().nonempty(),
components: z
.object({
postgres: ComponentEntrySchema.optional(),
clickhouse: ComponentEntrySchema.optional(),
storage: ComponentEntrySchema.extend({ objectCount: z.number().int().nonnegative() }).optional(),
metadata: ComponentEntrySchema.optional(),
})
.strict(),
encryption: EncryptionMetadataSchema,
notes: z.string().optional(),
})
.strict();
export type ComponentChecksum = z.infer<typeof ComponentChecksumSchema>;
export type ComponentEntry = z.infer<typeof ComponentEntrySchema>;
export type EncryptionMetadata = z.infer<typeof EncryptionMetadataSchema>;
export type BackupManifest = z.infer<typeof BackupManifestSchema>;
export function serializeManifest(manifest: BackupManifest): string {
const parsed = BackupManifestSchema.parse(manifest);
return `${JSON.stringify(parsed, null, 2)}\n`;
}
export function deserializeManifest(raw: string | Buffer): BackupManifest {
const text = typeof raw === "string" ? raw : raw.toString("utf8");
const data = JSON.parse(text) as unknown;
return BackupManifestSchema.parse(data);
}
export function sha256Hex(input: Buffer | string): string {
return createHash("sha256").update(input).digest("hex");
}
export function classifyBackup(createdAtIso: string): BackupManifest["classification"] {
const date = new Date(createdAtIso);
const out: BackupManifest["classification"][number][] = ["daily"];
if (date.getUTCDay() === 0) {
out.push("weekly");
}
if (date.getUTCDate() === 1) {
out.push("monthly");
}
return out as BackupManifest["classification"];
}