/** * Storage Add S3 Command * Configure an S3-compatible backup storage destination. * Works with AWS S3, MinIO, Backblaze B2, Wasabi, and GCS (via S3 interface). */ import { addBackupStorage, setDefaultBackupStorage, verifyBackupStorage, } from '../../services/backup-storage'; import { askConfirm, askText, withInterviewSession } from '../../services/bus-interview'; import { celiloIntro, celiloOutro } from '../prompts'; import { resolveServiceCredential } from '../service-credential'; import type { CommandResult } from '../types'; export async function handleStorageAddS3( _args: string[], flags: Record = {}, ): Promise { // Non-secret prompts route through the bus interview (ISS-0127); the S3 // secret access key is a credential that travels by flag/env only (D7). const scope = 'storage-add:s3'; return withInterviewSession(async () => { try { celiloIntro('Add S3 Backup Storage'); // Support non-interactive mode via flags (scriptable from docker-exec / // automation). region + endpoint have sensible defaults; name, bucket, and // the two credential flags are required for a fully non-interactive run. const flagName = typeof flags.name === 'string' ? flags.name : undefined; const flagBucket = typeof flags.bucket === 'string' ? flags.bucket : undefined; const flagRegion = typeof flags.region === 'string' ? flags.region : undefined; const flagEndpoint = typeof flags.endpoint === 'string' ? flags.endpoint : undefined; const flagAccessKeyId = typeof flags['access-key-id'] === 'string' ? flags['access-key-id'] : undefined; const flagSecretAccessKey = typeof flags['secret-access-key'] === 'string' ? flags['secret-access-key'] : undefined; const nonInteractive = Boolean( flagName && flagBucket && flagAccessKeyId && flagSecretAccessKey, ); const name = flagName ?? (await askText({ scope, key: 'name', message: 'Human-readable name', placeholder: 'e.g., Backblaze B2 Backups', required: true, })); const bucket = flagBucket ?? (await askText({ scope, key: 'bucket', message: 'Bucket name', placeholder: 'e.g., homelab-backups', required: true, })); const region = flagRegion ?? (await askText({ scope, key: 'region', message: 'Region', defaultValue: 'us-east-1', placeholder: 'us-east-1', required: true, })); const endpoint = flagEndpoint ?? (await askText({ scope, key: 'endpoint', message: 'Endpoint URL', defaultValue: 'https://s3.amazonaws.com', placeholder: 'https://s3.amazonaws.com', required: true, pattern: '^https?://', })); if ( flagEndpoint && !flagEndpoint.startsWith('https://') && !flagEndpoint.startsWith('http://') ) { return { success: false, error: `Invalid --endpoint '${flagEndpoint}': must start with https:// or http://`, }; } const accessKeyId = flagAccessKeyId ?? (await askText({ scope, key: 'access_key_id', message: 'Access Key ID', required: true, })); // Secret access key is a credential — flag/env only (D7). const secretAccessKey = await resolveServiceCredential({ field: 'Secret Access Key', flag: 'secret-access-key', envVar: 'S3_SECRET_ACCESS_KEY', flagValue: flagSecretAccessKey, }); const storage = await addBackupStorage({ name, providerName: 's3', credentials: { bucket, region, endpoint, accessKeyId, secretAccessKey, }, }); console.log(`\nStorage '${storage.storageId}' saved`); console.log('Testing connection...'); const { result } = await verifyBackupStorage(storage.id); if (!result.success) { console.log(`\n✗ Verification failed: ${result.message}`); celiloOutro( `Storage '${storage.storageId}' added but not verified.\n\nCheck credentials and re-verify: celilo storage verify ${storage.storageId}`, ); return { success: true, message: `Added storage: ${storage.storageId} (not verified)` }; } console.log(`✓ ${result.message}`); if (nonInteractive) { // Non-interactive: auto-set as default (matches storage-add-local). setDefaultBackupStorage(storage.id); console.log('✓ Set as default'); } else { const makeDefault = await askConfirm({ scope, key: 'make_default', message: 'Set as default backup destination?', defaultValue: true, }); if (makeDefault) { setDefaultBackupStorage(storage.id); console.log('✓ Set as default'); } } celiloOutro( `Storage '${storage.storageId}' (${name}) added and verified!\n\nStorage ID: ${storage.storageId}\nBucket: ${bucket}\nEndpoint: ${endpoint}\n\nNext steps:\n celilo module backup Create a backup\n celilo storage list List storage destinations`, ); return { success: true, message: `Added S3 storage: ${storage.storageId}` }; } catch (error) { return { success: false, error: `Failed to add S3 storage: ${error instanceof Error ? error.message : String(error)}`, }; } }); }