الملفات
psqlmigrator/api.js

1153 أسطر
39 KiB
JavaScript

// 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
};