1153 أسطر
39 KiB
JavaScript
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
|
|
}; |