86 lines
2.7 KiB
TypeScript
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"];
|
|
}
|