UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

508 lines (507 loc) 22.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } }; Object.defineProperty(exports, "__esModule", { value: true }); const client_s3_1 = require("@aws-sdk/client-s3"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const util_1 = require("util"); const execAsync = (0, util_1.promisify)(require('child_process').exec); /** * Restore MongoDB database from S3 backup */ const command = { alias: ['s3r'], description: 'Restore database from S3', name: 's3-restore', run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () { var _a, e_1, _b, _c; const { helper, parameters, print: { error, info, spin, success, warning }, prompt, system, } = toolbox; // Start timer const timer = system.startTimer(); info('MongoDB Restore from S3'); info(''); // ============================================================================ // Step 1: S3 Configuration // ============================================================================ info('Step 1: S3 Configuration'); info(''); const s3Bucket = yield helper.getInput(parameters.options.bucket || process.env.S3_BUCKET, { name: 'S3 Bucket Name', showError: true, }); if (!s3Bucket) { error('S3 Bucket is required'); return; } const s3Key = yield helper.getInput(parameters.options.key || process.env.S3_KEY, { name: 'S3 Access Key ID', showError: true, }); if (!s3Key) { error('S3 Access Key ID is required'); return; } const s3Secret = yield helper.getInput(parameters.options.secret || process.env.S3_SECRET, { name: 'S3 Secret Access Key', showError: true, }); if (!s3Secret) { error('S3 Secret Access Key is required'); return; } const s3Url = yield helper.getInput(parameters.options.url || process.env.S3_URL, { name: 'S3 Endpoint URL', showError: true, }); if (!s3Url) { error('S3 Endpoint URL is required'); return; } const s3Region = yield helper.getInput(parameters.options.region || process.env.S3_REGION || 'de', { initial: 'de', name: 'S3 Region (optional)', showError: false, }); const s3Folder = yield helper.getInput(parameters.options.folder || process.env.S3_FOLDER || '', { initial: '', name: 'S3 Folder/Prefix (optional)', showError: false, }); // ============================================================================ // Step 2: Initialize S3 Client and List Backups // ============================================================================ info(''); info('Step 2: Fetching available backups from S3...'); info(''); let s3Client; let backupFiles = []; try { s3Client = new client_s3_1.S3({ credentials: { accessKeyId: s3Key, secretAccessKey: s3Secret, }, endpoint: s3Url, forcePathStyle: true, region: s3Region, }); const listSpin = spin('Listing backup files from S3'); // List all backup files from S3 const paginator = (0, client_s3_1.paginateListObjectsV2)({ client: s3Client }, { Bucket: s3Bucket, Prefix: s3Folder, }); try { for (var _d = true, paginator_1 = __asyncValues(paginator), paginator_1_1; paginator_1_1 = yield paginator_1.next(), _a = paginator_1_1.done, !_a; _d = true) { _c = paginator_1_1.value; _d = false; const page = _c; const objects = page.Contents; if (!(objects === null || objects === void 0 ? void 0 : objects.length)) { continue; } backupFiles = [ ...backupFiles, ...objects .filter((object) => object.Key && (object.Key.endsWith('.tar.gz') || object.Key.endsWith('.archive'))) .map((object) => ({ key: object.Key, label: object.Key.split('/').pop() || object.Key, lastModified: object.LastModified, size: object.Size, })), ]; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (!_d && !_a && (_b = paginator_1.return)) yield _b.call(paginator_1); } finally { if (e_1) throw e_1.error; } } // Sort by last modified date (newest first) backupFiles.sort((a, b) => { if (!a.lastModified || !b.lastModified) { return 0; } return b.lastModified.getTime() - a.lastModified.getTime(); }); listSpin.succeed(`Found ${backupFiles.length} backup file(s)`); } catch (err) { error(`Failed to connect to S3 or list backups: ${err.message}`); return; } if (backupFiles.length === 0) { warning('No backup files found in the specified S3 bucket/folder'); return; } // ============================================================================ // Step 3: Select Backup File // ============================================================================ info(''); info('Step 3: Select backup file'); info(''); // Format backup files for selection with additional info const backupChoices = backupFiles.map((file, index) => { const sizeStr = file.size ? `${(file.size / 1024 / 1024).toFixed(2)} MB` : 'Unknown size'; const dateStr = file.lastModified ? file.lastModified.toLocaleString('de-DE', { day: '2-digit', hour: '2-digit', minute: '2-digit', month: '2-digit', year: 'numeric', }) : 'Unknown date'; const marker = index === 0 ? ' (newest)' : ''; return { message: `${file.label}${marker} - ${dateStr} - ${sizeStr}`, name: file.key, }; }); const { selectedBackupKey } = yield prompt.ask({ choices: backupChoices, initial: 0, // Pre-select the newest (first) backup message: 'Select backup file to restore:', name: 'selectedBackupKey', type: 'select', }); if (!selectedBackupKey) { error('No backup file selected'); return; } const selectedBackup = backupFiles.find((f) => f.key === selectedBackupKey); if (!selectedBackup) { error('Selected backup not found'); return; } success(`Selected: ${selectedBackup.label}`); // ============================================================================ // Step 4: Download Backup // ============================================================================ info(''); info('Step 4: Downloading backup...'); info(''); const tempDir = path.join('/tmp', `backup-${Date.now()}`); const backupFile = path.join(tempDir, 'backup.archive'); try { yield fs.promises.mkdir(tempDir, { recursive: true }); const downloadSpin = spin(`Downloading ${selectedBackup.label}`); const command = { Bucket: s3Bucket, Key: selectedBackup.key, }; const data = yield s3Client.getObject(command); const bodyStream = data.Body; const bodyArray = yield bodyStream.transformToByteArray(); yield fs.promises.writeFile(backupFile, Buffer.from(bodyArray)); downloadSpin.succeed(`Downloaded ${selectedBackup.label} (${(bodyArray.length / 1024 / 1024).toFixed(2)} MB)`); } catch (err) { error(`Failed to download backup: ${err.message}`); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 5: Extract Backup // ============================================================================ info(''); info('Step 5: Extracting backup...'); info(''); const extractDir = path.join(tempDir, 'extracted'); try { yield fs.promises.mkdir(extractDir, { recursive: true }); const extractSpin = spin('Extracting backup archive'); const extractCommand = `tar -xzf "${backupFile}" -C "${extractDir}"`; yield execAsync(extractCommand); extractSpin.succeed('Backup extracted'); } catch (err) { error(`Failed to extract backup: ${err.message}`); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 6: Find Databases in Backup // ============================================================================ info(''); info('Step 6: Detecting databases from backup...'); info(''); let backupRootDir = ''; let sourceDbName = ''; const systemDatabases = ['admin', 'local', 'config']; try { const findDbSpin = spin('Searching for database files'); // Find all directories containing BSON files const findBsonCommand = `find "${extractDir}" -name "*.bson" -exec dirname {} \\; | sort -u`; const { stdout } = yield execAsync(findBsonCommand); const dbPaths = stdout .trim() .split('\n') .filter((p) => p); if (dbPaths.length === 0) { throw new Error('No database files found in backup'); } // Find the common parent directory (backup root) // Example: /tmp/backup-xxx/extracted/tmp/backup-name/admin -> parent is /tmp/backup-xxx/extracted/tmp/backup-name const firstPath = dbPaths[0]; const pathParts = firstPath.split('/'); // The parent is everything except the last part (database name) backupRootDir = pathParts.slice(0, -1).join('/'); // Get all database names from their directories const dbNames = dbPaths .map((p) => { const parts = p.split('/'); return parts[parts.length - 1]; }) .filter((name, index, self) => self.indexOf(name) === index); // unique // Filter out system databases const userDatabases = dbNames.filter((name) => !systemDatabases.includes(name)); if (userDatabases.length === 0) { warning('Only system databases (admin, local, config) found in backup'); warning('Will proceed, but you may want to check the backup file'); sourceDbName = dbNames[0]; // Use first available database } else if (userDatabases.length === 1) { sourceDbName = userDatabases[0]; findDbSpin.succeed(`Found database: ${sourceDbName}`); } else { findDbSpin.succeed(`Found ${userDatabases.length} databases`); // Let user select which database to restore info(''); info('Multiple databases found in backup:'); userDatabases.forEach((db) => info(` - ${db}`)); info(''); const { selectedDb } = yield prompt.ask({ choices: userDatabases, initial: 0, message: 'Select source database to restore:', name: 'selectedDb', type: 'select', }); if (!selectedDb) { error('No database selected'); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } sourceDbName = selectedDb; success(`Selected source database: ${sourceDbName}`); } info(`Backup root directory: ${backupRootDir}`); } catch (err) { error(`Could not detect databases: ${err.message}`); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 7: MongoDB Configuration // ============================================================================ info(''); info('Step 7: MongoDB Configuration'); info(''); const mongoUri = yield helper.getInput(parameters.options.mongoUri || process.env.MONGO_URI || 'mongodb://127.0.0.1:27017', { initial: 'mongodb://127.0.0.1:27017', name: 'MongoDB Connection URI (without database name)', showError: true, }); if (!mongoUri) { error('MongoDB URI is required'); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } const targetDbName = yield helper.getInput(parameters.options.database || sourceDbName, { initial: sourceDbName, name: 'Target Database Name', showError: true, }); if (!targetDbName) { error('Target database name is required'); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 8: Confirmation // ============================================================================ info(''); warning('IMPORTANT: This operation will restore the backup to the target database.'); warning(`Target: ${mongoUri}/${targetDbName}`); warning('If the database already exists, it may be overwritten or merged.'); info(''); const { confirmRestore } = yield prompt.ask({ initial: false, message: `Proceed with restore to ${targetDbName}?`, name: 'confirmRestore', type: 'confirm', }); if (!confirmRestore) { info('Restore cancelled'); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 9: Restore Database // ============================================================================ info(''); info('Step 9: Restoring database...'); info(''); try { const restoreSpin = spin(`Restoring ${sourceDbName} to ${targetDbName}`); // Build mongorestore command // If source and target names are the same, use simpler command let restoreCommand; if (sourceDbName === targetDbName) { // Restore without renaming const fullMongoUri = `${mongoUri}/${targetDbName}`; restoreCommand = `mongorestore --uri="${fullMongoUri}" --nsInclude="${sourceDbName}.*" "${backupRootDir}"`; } else { // Restore with renaming using --nsFrom and --nsTo restoreCommand = `mongorestore --uri="${mongoUri}" --nsFrom="${sourceDbName}.*" --nsTo="${targetDbName}.*" --nsInclude="${sourceDbName}.*" "${backupRootDir}"`; } info('Running: mongorestore ...'); const { stderr } = yield execAsync(restoreCommand, { maxBuffer: 1024 * 1024 * 50, // 50MB buffer for large outputs }); // Check for actual failures (mongorestore outputs warnings to stderr) if (stderr && stderr.includes('Failed:') && !stderr.includes('0 document(s) failed')) { throw new Error(stderr); } restoreSpin.succeed(`Database restored successfully (${sourceDbName}${targetDbName})`); } catch (err) { error(`Failed to restore database: ${err.message}`); info(''); info('Please ensure:'); info('- MongoDB is running and accessible'); info('- mongorestore tool is installed (part of MongoDB Database Tools)'); info('- The MongoDB URI is correct'); info('- The source database exists in the backup'); // Cleanup try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); } catch (e) { // Ignore cleanup errors } return; } // ============================================================================ // Step 10: Cleanup // ============================================================================ info(''); const cleanupSpin = spin('Cleaning up temporary files'); try { yield fs.promises.rm(tempDir, { force: true, recursive: true }); cleanupSpin.succeed('Temporary files cleaned up'); } catch (err) { cleanupSpin.fail(`Failed to cleanup temporary files: ${err.message}`); warning(`You may need to manually delete: ${tempDir}`); } // ============================================================================ // Done // ============================================================================ info(''); success(`Database restored successfully to ${targetDbName} in ${helper.msToMinutesAndSeconds(timer())}m`); info(''); if (!parameters.options.fromGluegunMenu) { process.exit(0); } return `mongodb restored ${targetDbName}`; }), }; exports.default = command;