// API service for Unified Migration Tool with Environment Variables const API_URL = ''; // ============================================================================ // PostgreSQL API // ============================================================================ export const postgresApi = { // ==================== PostgreSQL Connection ==================== // Test PostgreSQL connection testConnection: async (options = {}) => { const { useEnvVars = false, uri, host, user, password, database, port = 5432 } = options; const body = { use_env_vars: useEnvVars }; if (uri) { body.uri = uri; } else if (host && user && password && database) { body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; } const response = await fetch(`${API_URL}/api/postgres/test-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Get PostgreSQL schemas getSchemas: async (uri) => { const response = await fetch(`${API_URL}/api/postgres/get-schemas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri }) }); return response.json(); }, // Get PostgreSQL tables getTables: async (uri, schema = '') => { const response = await fetch(`${API_URL}/api/postgres/get-tables`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri, schema }) }); return response.json(); }, // Get table counts getTableCounts: async (uri, schema = '') => { const response = await fetch(`${API_URL}/api/postgres/get-table-counts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri, schema }) }); return response.json(); }, // Parse PostgreSQL URI parseUri: async (uri) => { const response = await fetch(`${API_URL}/api/postgres/parse-uri`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri }) }); return response.json(); }, // Start PostgreSQL to PostgreSQL migration startMigration: async (sourceUri, destUri, schemas = null, tables = null) => { const response = await fetch(`${API_URL}/api/postgres/start-migration`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_uri: sourceUri, dest_uri: destUri, schemas, tables }) }); return response.json(); }, // Get migration status getMigrationStatus: async (migrationId) => { const response = await fetch(`${API_URL}/api/postgres/migration-status/${migrationId}`); return response.json(); }, // List all migrations listMigrations: async () => { const response = await fetch(`${API_URL}/api/postgres/list-migrations`); return response.json(); } }; // ============================================================================ // S3 to S3 API // ============================================================================ export const s3Api = { // ==================== Source S3 Connection ==================== // Test source S3 connection testSourceConnection: async (options = {}) => { const { useEnvVars = false, accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl, sessionToken } = options; const body = { use_env_vars: useEnvVars, access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }; const response = await fetch(`${API_URL}/api/s3-source/test-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Test destination S3 connection testDestinationConnection: async (options = {}) => { const { useEnvVars = false, accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl, sessionToken } = options; const body = { use_env_vars: useEnvVars, access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }; const response = await fetch(`${API_URL}/api/s3-destination/test-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // List source buckets listSourceBuckets: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) => { const response = await fetch(`${API_URL}/api/s3-source/list-buckets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }) }); return response.json(); }, // List destination buckets listDestinationBuckets: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) => { const response = await fetch(`${API_URL}/api/s3-destination/list-buckets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }) }); return response.json(); }, // List objects in a bucket listObjects: async (bucket, prefix = '', isSource = true, credentials = {}) => { const body = { bucket, prefix, is_source: isSource, access_key_id: credentials.accessKeyId, secret_access_key: credentials.secretAccessKey, region: credentials.region || 'us-east-1', endpoint_url: credentials.endpointUrl, session_token: credentials.sessionToken, max_keys: credentials.maxKeys || 1000 }; const response = await fetch(`${API_URL}/api/s3/list-objects`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Create bucket in destination createBucket: async (bucket, region = 'us-east-1', options = {}) => { const body = { bucket, region, access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, endpoint_url: options.endpointUrl, session_token: options.sessionToken }; const response = await fetch(`${API_URL}/api/s3-destination/create-bucket`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Migrate single object migrateObject: async (sourceBucket, sourceKey, destBucket, destKey = null, options = {}) => { const body = { source_bucket: sourceBucket, source_key: sourceKey, dest_bucket: destBucket, dest_key: destKey || sourceKey, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD' }; const response = await fetch(`${API_URL}/api/s3/migrate-object`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Migrate multiple objects in batch migrateBatch: async (objects, sourceBucket, destBucket, options = {}) => { const body = { objects, source_bucket: sourceBucket, dest_bucket: destBucket, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD', max_concurrent: options.maxConcurrent || 5 }; const response = await fetch(`${API_URL}/api/s3/migrate-batch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Start full S3 to S3 migration startMigration: async (sourceBucket, destBucket, prefix = '', options = {}) => { const body = { source_bucket: sourceBucket, dest_bucket: destBucket, prefix, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, include_patterns: options.includePatterns, exclude_patterns: options.excludePatterns, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD', create_dest_bucket: options.createDestBucket !== undefined ? options.createDestBucket : true, max_concurrent: options.maxConcurrent || 5 }; const response = await fetch(`${API_URL}/api/s3/start-migration`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Get migration status getMigrationStatus: async (migrationId) => { const response = await fetch(`${API_URL}/api/s3/migration-status/${migrationId}`); return response.json(); }, // List all migrations listMigrations: async () => { const response = await fetch(`${API_URL}/api/s3/list-migrations`); return response.json(); }, // Cancel migration cancelMigration: async (migrationId) => { const response = await fetch(`${API_URL}/api/s3/cancel-migration/${migrationId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); return response.json(); }, // Parse S3 URI parseUri: async (s3Uri) => { const response = await fetch(`${API_URL}/api/s3/parse-uri`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ s3_uri: s3Uri }) }); return response.json(); }, // Generate presigned URL generatePresignedUrl: async (bucket, key, isSource = true, expiration = 3600, credentials = {}) => { const body = { bucket, key, is_source: isSource, expiration, access_key_id: credentials.accessKeyId, secret_access_key: credentials.secretAccessKey, region: credentials.region || 'us-east-1', endpoint_url: credentials.endpointUrl, session_token: credentials.sessionToken }; const response = await fetch(`${API_URL}/api/s3/generate-presigned-url`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); } }; // ============================================================================ // PostgreSQL to S3 API // ============================================================================ export const postgresToS3Api = { // Test PostgreSQL connection testPostgresConnection: async (uri) => { const response = await fetch(`${API_URL}/api/postgres-s3/test-postgres-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri }) }); return response.json(); }, // Test S3 connection testS3Connection: async (accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null) => { const response = await fetch(`${API_URL}/api/postgres-s3/test-s3-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl }) }); return response.json(); }, // Get PostgreSQL schemas getSchemas: async (uri) => { const response = await fetch(`${API_URL}/api/postgres-s3/get-schemas`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri }) }); return response.json(); }, // Get PostgreSQL tables getTables: async (uri, schema = '') => { const response = await fetch(`${API_URL}/api/postgres-s3/get-tables`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uri, schema }) }); return response.json(); }, // Export single table to S3 exportTable: async (postgresUri, schema, table, s3Bucket, s3Key, options = {}) => { const body = { postgres_uri: postgresUri, schema, table, s3_bucket: s3Bucket, s3_key: s3Key, compress: options.compress !== undefined ? options.compress : true, format: options.format || 'csv', access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, region: options.region || 'us-east-1', endpoint_url: options.endpointUrl }; const response = await fetch(`${API_URL}/api/postgres-s3/export-table`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Start full PostgreSQL to S3 migration startMigration: async (postgresUri, s3Bucket, s3Prefix = '', options = {}) => { const body = { postgres_uri: postgresUri, s3_bucket: s3Bucket, s3_prefix: s3Prefix, schemas: options.schemas, tables: options.tables, compress: options.compress !== undefined ? options.compress : true, format: options.format || 'csv', access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, region: options.region || 'us-east-1', endpoint_url: options.endpointUrl }; const response = await fetch(`${API_URL}/api/postgres-s3/start-migration`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return response.json(); }, // Get migration status getMigrationStatus: async (migrationId) => { const response = await fetch(`${API_URL}/api/postgres-s3/migration-status/${migrationId}`); return response.json(); }, // List all migrations listMigrations: async () => { const response = await fetch(`${API_URL}/api/postgres-s3/list-migrations`); return response.json(); } }; // ============================================================================ // Common Environment and Utility Functions // ============================================================================ // Inject environment variables export const injectEnv = async (envVars) => { const response = await fetch(`${API_URL}/api/inject-env`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ environment_variables: envVars }) }); return response.json(); }; // Get current environment export const getCurrentEnv = async () => { const response = await fetch(`${API_URL}/api/get-current-env`); return response.json(); }; // Health check export const healthCheck = async () => { const response = await fetch(`${API_URL}/api/health`); return response.json(); }; // ============================================================================ // Helper Functions // ============================================================================ // Copy to clipboard export const copyToClipboard = async (text) => { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.error('Failed to copy:', err); return false; } }; // Format environment variables export const formatEnvVars = (envVars, format = 'dotenv') => { if (!envVars) return ''; switch(format) { case 'dotenv': return Object.entries(envVars) .map(([key, value]) => `${key}=${value}`) .join('\n'); case 'json': return JSON.stringify(envVars, null, 2); case 'shell_export': return Object.entries(envVars) .map(([key, value]) => `export ${key}="${value}"`) .join('\n'); case 'docker_env': return Object.entries(envVars) .map(([key, value]) => `-e ${key}="${value}"`) .join(' '); case 'docker_compose': return Object.entries(envVars) .map(([key, value]) => ` ${key}: ${value}`) .join('\n'); default: return Object.entries(envVars) .map(([key, value]) => `${key}: ${value}`) .join('\n'); } }; // Format file size export const formatFileSize = (bytes) => { if (bytes === 0 || !bytes) return '0 Bytes'; if (typeof bytes !== 'number') bytes = parseInt(bytes); const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; // Format PostgreSQL configuration for display export const formatPostgresConfig = (config) => { if (!config) return ''; const details = [ `📍 PostgreSQL Configuration:`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Host: ${config.host || 'Not set'}`, `Port: ${config.port || 5432}`, `User: ${config.user || 'Not set'}`, `Database: ${config.database || 'Not set'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━` ]; if (config.host && config.user && config.password) { details.push(`✅ Credentials: Configured`); } else { details.push(`❌ Credentials: Not configured`); } return details.join('\n'); }; // Format S3 configuration for display export const formatS3Config = (config, type = 'source') => { if (!config) return ''; const details = [ `📍 ${type.toUpperCase()} S3 Configuration:`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Endpoint: ${config.endpoint_url || 'AWS S3 (default)'}`, `Region: ${config.region || 'us-east-1'}`, `Bucket: ${config.bucket || 'Not set'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━` ]; if (config.access_key_id && config.secret_access_key) { details.push(`✅ Credentials: Configured`); } else { details.push(`❌ Credentials: Not configured`); } return details.join('\n'); }; // Format migration details for display export const formatMigrationDetails = (migrationData) => { if (!migrationData) return ''; const details = [ `🔄 Migration ID: ${migrationData.migration_id || migrationData.id || 'N/A'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Status: ${migrationData.status || 'N/A'}`, `Started: ${migrationData.started_at ? new Date(migrationData.started_at * 1000).toLocaleString() : 'N/A'}` ]; if (migrationData.stats) { details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); details.push(`📊 Statistics:`); Object.entries(migrationData.stats).forEach(([key, value]) => { details.push(` ${key}: ${value}`); }); } if (migrationData.message) { details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); details.push(`📝 Message: ${migrationData.message}`); } return details.join('\n'); }; // Extract PostgreSQL info from URI export const extractPostgresInfo = (uri) => { try { const match = uri.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); if (match) { return { user: match[1], password: match[2], host: match[3], port: parseInt(match[4]), database: match[5], isValid: true }; } return { isValid: false }; } catch (error) { console.error('Error extracting PostgreSQL info:', error); return { isValid: false, error: error.message }; } }; // ============================================================================ // Stream Progress API (Server-Sent Events) // ============================================================================ /** * Stream migration progress using Server-Sent Events * @param {string} migrationId - The migration ID to track * @param {string} type - Migration type: 's3', 'postgres', or 'postgres-s3' * @param {function} onProgress - Callback function for progress updates * @param {function} onComplete - Callback function when migration completes * @param {function} onError - Callback function for errors * @returns {EventSource} - The EventSource object for the stream */ export const streamProgress = (migrationId, type = 's3', onProgress, onComplete, onError) => { const url = new URL(`${API_URL}/api/stream-progress/${migrationId}`); url.searchParams.append('type', type); const eventSource = new EventSource(url.toString()); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); // التحقق من الإكمال بعدة طرق if (data.type === 'completion' || data.status === 'completed' || data.status === 'success' || data.success === true) { if (onComplete) onComplete(data); eventSource.close(); } // التحقق من الخطأ بعدة طرق else if (data.type === 'error' || data.status === 'failed' || data.status === 'error' || data.error) { if (onError) onError(data); eventSource.close(); } else { if (onProgress) onProgress(data); } } catch (error) { console.error('Error parsing stream data:', error); if (onError) onError({ error: error.message }); } }; eventSource.onerror = (error) => { console.error('EventSource error:', error); if (onError) onError({ error: 'Connection error', details: error }); eventSource.close(); }; return eventSource; }; /** * Stream progress with Promise-based wrapper (easier to use with async/await) * @param {string} migrationId - The migration ID to track * @param {string} type - Migration type * @param {function} onProgress - Optional progress callback * @returns {Promise} - Promise that resolves when migration completes */ export const streamProgressAsync = (migrationId, type = 's3', onProgress = null) => { return new Promise((resolve, reject) => { const eventSource = streamProgress( migrationId, type, (progress) => { // تمرير التقدم إلى callback إذا وجد if (onProgress) onProgress(progress); }, (completion) => { // إكمال الوعد عند الانتهاء resolve(completion); eventSource.close(); }, (error) => { // رفض الوعد عند الخطأ reject(error); eventSource.close(); } ); }); }; /** * Stream progress with automatic reconnection * @param {string} migrationId - The migration ID to track * @param {string} type - Migration type * @param {object} options - Options for the stream * @returns {object} - Control object with start, stop, and pause methods */ export const createProgressStream = (migrationId, type = 's3', options = {}) => { const { onProgress = () => {}, onComplete = () => {}, onError = () => {}, reconnectInterval = 3000, // إعادة الاتصال كل 3 ثواني maxReconnectAttempts = 10 // أقصى عدد محاولات إعادة الاتصال } = options; let eventSource = null; let reconnectAttempts = 0; let isActive = true; let reconnectTimer = null; const connect = () => { if (!isActive) return; // إغلاق الاتصال القديم إذا كان موجوداً if (eventSource) { eventSource.close(); } // إنشاء اتصال جديد eventSource = streamProgress( migrationId, type, (progress) => { // إعادة تعيين محاولات إعادة الاتصال عند استلام تقدم reconnectAttempts = 0; onProgress(progress); }, (completion) => { isActive = false; onComplete(completion); if (eventSource) eventSource.close(); }, (error) => { onError(error); // محاولة إعادة الاتصال إذا كان لا يزال نشطاً if (isActive && reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; console.log(`Reconnecting... Attempt ${reconnectAttempts}/${maxReconnectAttempts}`); if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, reconnectInterval); } else if (reconnectAttempts >= maxReconnectAttempts) { isActive = false; onError({ error: 'Max reconnection attempts reached' }); } } ); }; // بدء الاتصال connect(); // إرجاع كائن التحكم return { // إيقاف البث نهائياً stop: () => { isActive = false; if (reconnectTimer) clearTimeout(reconnectTimer); if (eventSource) { eventSource.close(); eventSource = null; } }, // إيقاف البث مؤقتاً pause: () => { isActive = false; if (reconnectTimer) clearTimeout(reconnectTimer); if (eventSource) { eventSource.close(); eventSource = null; } }, // استئناف البث resume: () => { if (!isActive) { isActive = true; reconnectAttempts = 0; connect(); } }, // التحقق من حالة الاتصال isConnected: () => eventSource !== null && eventSource.readyState === EventSource.OPEN, // الحصول على حالة الاتصال getState: () => { if (!eventSource) return 'disconnected'; switch (eventSource.readyState) { case EventSource.CONNECTING: return 'connecting'; case EventSource.OPEN: return 'connected'; case EventSource.CLOSED: return 'closed'; default: return 'unknown'; } } }; }; /** * Format progress data for display * @param {object} progress - Progress data from stream * @returns {string} - Formatted progress string */ export const formatProgressDisplay = (progress) => { if (!progress) return 'No progress data'; const lines = []; // إضافة عنوان if (progress.migration_id) { lines.push(`🚀 Migration: ${progress.migration_id}`); } // إضافة الحالة if (progress.status) { const statusEmoji = { 'running': '🟢', 'completed': '✅', 'failed': '❌', 'cancelled': '⏹️' }[progress.status] || '📊'; lines.push(`${statusEmoji} Status: ${progress.status}`); } // إضافة النسبة المئوية if (progress.percentage !== undefined) { lines.push(`📊 Progress: ${progress.percentage.toFixed(1)}%`); } else if (progress.percentages?.size !== undefined) { lines.push(`📊 Progress: ${progress.percentages.size.toFixed(1)}%`); } // إضافة إحصائيات S3 if (progress.processed_objects !== undefined && progress.total_objects !== undefined) { lines.push(`📦 Objects: ${progress.processed_objects}/${progress.total_objects}`); } // إضافة إحصائيات PostgreSQL if (progress.processed?.tables !== undefined && progress.total?.tables !== undefined) { lines.push(`📋 Tables: ${progress.processed.tables}/${progress.total.tables}`); } // إضافة حجم البيانات if (progress.processed_size_formatted && progress.total_size_formatted) { lines.push(`💾 Size: ${progress.processed_size_formatted}/${progress.total_size_formatted}`); } else if (progress.processed?.size_formatted && progress.total?.size_formatted) { lines.push(`💾 Size: ${progress.processed.size_formatted}/${progress.total.size_formatted}`); } // إضافة السرعة if (progress.current_speed_formatted) { lines.push(`⚡ Speed: ${progress.current_speed_formatted}`); } else if (progress.speed?.current_formatted) { lines.push(`⚡ Speed: ${progress.speed.current_formatted}`); } // إضافة الوقت if (progress.elapsed_time_formatted) { lines.push(`⏱️ Elapsed: ${progress.elapsed_time_formatted}`); } if (progress.eta_formatted) { lines.push(`⏳ ETA: ${progress.eta_formatted}`); } else if (progress.time?.eta_formatted) { lines.push(`⏳ ETA: ${progress.time.eta_formatted}`); } // إضافة العنصر الحالي if (progress.current_object) { lines.push(`📄 Current: ${progress.current_object}`); } else if (progress.current_table?.name) { const ct = progress.current_table; lines.push(`📄 Current Table: ${ct.name}`); if (ct.size_formatted) { lines.push(` Size: ${ct.size_formatted}`); } if (ct.rows_percentage !== undefined) { lines.push(` Rows: ${ct.rows_processed}/${ct.rows} (${ct.rows_percentage}%)`); } } return lines.join('\n'); }; /** * Create a progress bar string * @param {number} percentage - Progress percentage (0-100) * @param {number} width - Width of the progress bar * @returns {string} - Progress bar string */ export const createProgressBar = (percentage, width = 30) => { const filled = Math.floor(width * (percentage / 100)); const empty = width - filled; const bar = '█'.repeat(filled) + '░'.repeat(empty); return `[${bar}] ${percentage.toFixed(1)}%`; }; /** * Estimate remaining time based on progress and speed * @param {object} progress - Progress data * @returns {string} - Estimated remaining time */ export const estimateRemainingTime = (progress) => { if (!progress) return 'Unknown'; // استخدام ETA من البيانات إذا كان موجوداً if (progress.eta_formatted) return progress.eta_formatted; if (progress.time?.eta_formatted) return progress.time.eta_formatted; // حساب يدوي إذا لم يكن موجوداً const remaining = progress.remaining_size || progress.remaining?.size; const speed = progress.current_speed || progress.speed?.current; if (remaining && speed && speed > 0) { const seconds = remaining / speed; return formatTimeEstimate(seconds); } return 'Calculating...'; }; /** * Format time estimate * @param {number} seconds - Time in seconds * @returns {string} - Formatted time string */ export const formatTimeEstimate = (seconds) => { if (seconds < 60) { return `${Math.round(seconds)}s`; } else if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.round(seconds % 60); return `${minutes}m ${remainingSeconds}s`; } else if (seconds < 86400) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours}h ${minutes}m`; } else { const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); return `${days}d ${hours}h`; } }; // ============================================================================ // إضافة الدوال إلى التصدير // ============================================================================ // Extract S3 info from URI export const extractS3Info = (s3Uri) => { try { if (s3Uri.startsWith('s3://')) { const uri = s3Uri.substring(5); const parts = uri.split('/', 1); const bucket = parts[0]; const key = uri.substring(bucket.length + 1); return { bucket, key: key || '', fullUri: s3Uri, isValid: true }; } return { isValid: false }; } catch (error) { console.error('Error parsing S3 URI:', error); return { isValid: false, error: error.message }; } }; // Build PostgreSQL connection string export const buildPostgresConnectionString = (host, database, user, port = 5432) => { if (!host || !database) return ''; return `postgresql://${user ? user + '@' : ''}${host}:${port}/${database}`; }; // Build S3 URL export const buildS3Url = (bucket, key, endpointUrl = null) => { if (!bucket) return ''; if (!key) return `s3://${bucket}`; if (endpointUrl) { return `${endpointUrl}/${bucket}/${key}`; } return `s3://${bucket}/${key}`; }; // Group tables by schema export const groupTablesBySchema = (tables) => { const groups = {}; tables.forEach(table => { const schema = table.schema || 'public'; if (!groups[schema]) { groups[schema] = { count: 0, tables: [] }; } groups[schema].count++; groups[schema].tables.push(table); }); return groups; }; // Group objects by prefix export const groupObjectsByPrefix = (objects, depth = 1) => { const groups = {}; objects.forEach(obj => { const parts = obj.key.split('/'); let prefix = ''; for (let i = 0; i < Math.min(depth, parts.length - 1); i++) { prefix += parts[i] + '/'; } if (!prefix) prefix = '/'; if (!groups[prefix]) { groups[prefix] = { count: 0, totalSize: 0, objects: [] }; } groups[prefix].count++; groups[prefix].totalSize += obj.size; groups[prefix].objects.push(obj); }); return groups; }; // ============================================================================ // Export all APIs // ============================================================================ export default { postgresApi, s3Api, postgresToS3Api, injectEnv, getCurrentEnv, healthCheck, copyToClipboard, formatEnvVars, formatFileSize, formatPostgresConfig, formatS3Config, formatMigrationDetails, extractPostgresInfo, extractS3Info, buildPostgresConnectionString, buildS3Url, groupTablesBySchema, groupObjectsByPrefix, streamProgress, streamProgressAsync, createProgressStream, formatProgressDisplay, createProgressBar, estimateRemainingTime, formatTimeEstimate };