- Add ImportResult type to track succeeded/warnings/errors counts across all import functions - Implement multi-layer repository duplicate detection: - URL-based check (same path/bucket/endpoint already registered) - Restic repo check (path is already a restic repository) - Name-based fallback check - Standardize logging between CLI and env var import methods with logImportSummary() - CLI exits with code 1 when import has errors - Remove shortId override for local repo migrations (use full path with isExistingRepository instead) - Update example JSON and README: - Document that local repo path is optional (defaults to /var/lib/zerobyte/repositories) - Add existing-local-repo example with isExistingRepository: true - Add S3 endpoint field to example - Expand config behavior docs to explain all repository skip conditions - Improve .gitignore to exclude all JSON except example template
112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { Command } from "commander";
|
|
import path from "node:path";
|
|
import fs from "node:fs/promises";
|
|
|
|
async function readStdin(): Promise<string> {
|
|
const chunks: Buffer[] = [];
|
|
for await (const chunk of process.stdin) {
|
|
chunks.push(chunk);
|
|
}
|
|
return Buffer.concat(chunks).toString("utf-8");
|
|
}
|
|
|
|
export const importConfigCommand = new Command("import-config")
|
|
.description("Import configuration from a JSON file or stdin")
|
|
.option("-c, --config <path>", "Path to the configuration file")
|
|
.option("--stdin", "Read configuration from stdin")
|
|
.option("--dry-run", "Validate the config without importing")
|
|
.action(async (options) => {
|
|
console.log("\n📦 Zerobyte Config Import\n");
|
|
|
|
if (!options.config && !options.stdin) {
|
|
console.error("❌ Either --config <path> or --stdin is required");
|
|
console.log("\nUsage:");
|
|
console.log(" zerobyte import-config --config /path/to/config.json");
|
|
console.log(" cat config.json | zerobyte import-config --stdin");
|
|
process.exit(1);
|
|
}
|
|
|
|
if (options.config && options.stdin) {
|
|
console.error("❌ Cannot use both --config and --stdin");
|
|
process.exit(1);
|
|
}
|
|
|
|
let configJson: string;
|
|
|
|
if (options.stdin) {
|
|
console.log("📄 Reading config from stdin...");
|
|
try {
|
|
configJson = await readStdin();
|
|
if (!configJson.trim()) {
|
|
console.error("❌ No input received from stdin");
|
|
process.exit(1);
|
|
}
|
|
} catch (e) {
|
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
console.error(`❌ Failed to read stdin: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
const configPath = path.resolve(process.cwd(), options.config);
|
|
|
|
// Check if file exists
|
|
try {
|
|
await fs.access(configPath);
|
|
} catch {
|
|
console.error(`❌ Config file not found: ${configPath}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`📄 Config file: ${configPath}`);
|
|
configJson = await fs.readFile(configPath, "utf-8");
|
|
}
|
|
|
|
// Parse and validate JSON
|
|
let config: unknown;
|
|
try {
|
|
config = JSON.parse(configJson);
|
|
} catch (e) {
|
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
console.error(`❌ Invalid JSON: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (options.dryRun) {
|
|
console.log("🔍 Dry run mode - validating config only\n");
|
|
|
|
const root = typeof config === "object" && config !== null ? config : {};
|
|
const configObj =
|
|
"config" in root && typeof root.config === "object" && root.config !== null ? root.config : root;
|
|
|
|
const sections = ["volumes", "repositories", "backupSchedules", "notificationDestinations", "users"];
|
|
for (const section of sections) {
|
|
const items = (configObj as Record<string, unknown>)[section] || [];
|
|
const count = Array.isArray(items) ? items.length : 0;
|
|
console.log(` ${section}: ${count} item(s)`);
|
|
}
|
|
|
|
const hasRecoveryKey = !!(configObj as Record<string, unknown>).recoveryKey;
|
|
console.log(` recoveryKey: ${hasRecoveryKey ? "provided" : "not provided"}`);
|
|
|
|
console.log("\n✅ Config is valid JSON");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Ensure database is initialized with migrations
|
|
const { runDbMigrations } = await import("../../db/db");
|
|
runDbMigrations();
|
|
|
|
const { applyConfigImport } = await import("../../modules/lifecycle/config-import");
|
|
const result = await applyConfigImport(config);
|
|
|
|
// Exit with error code if there were errors
|
|
if (result.errors > 0) {
|
|
process.exit(1);
|
|
}
|
|
} catch (e) {
|
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
console.error(`❌ Import failed: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
});
|