123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- import { Meteor } from 'meteor/meteor';
- import { ReactiveCache } from '/imports/reactiveCache';
- import { Attachments, fileStoreStrategyFactory } from '/models/attachments';
- import { moveToStorage } from '/models/lib/fileStoreStrategy';
- import os from 'os';
- import { createHash } from 'crypto';
- // Migration state management
- const migrationState = {
- isRunning: false,
- isPaused: false,
- targetStorage: null,
- batchSize: 10,
- delayMs: 1000,
- cpuThreshold: 70,
- progress: 0,
- totalAttachments: 0,
- migratedAttachments: 0,
- currentBatch: [],
- migrationQueue: [],
- log: [],
- startTime: null,
- lastCpuCheck: 0
- };
- // CPU monitoring
- function getCpuUsage() {
- const cpus = os.cpus();
- let totalIdle = 0;
- let totalTick = 0;
-
- cpus.forEach(cpu => {
- for (const type in cpu.times) {
- totalTick += cpu.times[type];
- }
- totalIdle += cpu.times.idle;
- });
-
- const idle = totalIdle / cpus.length;
- const total = totalTick / cpus.length;
- const usage = 100 - Math.floor(100 * idle / total);
-
- return usage;
- }
- // Logging function
- function addToLog(message) {
- const timestamp = new Date().toISOString();
- const logEntry = `[${timestamp}] ${message}`;
- migrationState.log.unshift(logEntry);
-
- // Keep only last 100 log entries
- if (migrationState.log.length > 100) {
- migrationState.log = migrationState.log.slice(0, 100);
- }
-
- console.log(logEntry);
- }
- // Get migration status
- function getMigrationStatus() {
- return {
- isRunning: migrationState.isRunning,
- isPaused: migrationState.isPaused,
- targetStorage: migrationState.targetStorage,
- progress: migrationState.progress,
- totalAttachments: migrationState.totalAttachments,
- migratedAttachments: migrationState.migratedAttachments,
- remainingAttachments: migrationState.totalAttachments - migrationState.migratedAttachments,
- status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
- log: migrationState.log.slice(0, 10).join('\n'), // Return last 10 log entries
- startTime: migrationState.startTime,
- estimatedTimeRemaining: calculateEstimatedTimeRemaining()
- };
- }
- // Calculate estimated time remaining
- function calculateEstimatedTimeRemaining() {
- if (!migrationState.isRunning || migrationState.migratedAttachments === 0) {
- return null;
- }
-
- const elapsed = Date.now() - migrationState.startTime;
- const rate = migrationState.migratedAttachments / elapsed; // attachments per ms
- const remaining = migrationState.totalAttachments - migrationState.migratedAttachments;
-
- return Math.round(remaining / rate);
- }
- // Process a single attachment migration
- function migrateAttachment(attachmentId) {
- try {
- const attachment = ReactiveCache.getAttachment(attachmentId);
- if (!attachment) {
- addToLog(`Warning: Attachment ${attachmentId} not found`);
- return false;
- }
- // Check if already in target storage
- const currentStorage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
- if (currentStorage === migrationState.targetStorage) {
- addToLog(`Attachment ${attachmentId} already in target storage ${migrationState.targetStorage}`);
- return true;
- }
- // Perform migration
- moveToStorage(attachment, migrationState.targetStorage, fileStoreStrategyFactory);
- addToLog(`Migrated attachment ${attachmentId} from ${currentStorage} to ${migrationState.targetStorage}`);
-
- return true;
- } catch (error) {
- addToLog(`Error migrating attachment ${attachmentId}: ${error.message}`);
- return false;
- }
- }
- // Process a batch of attachments
- function processBatch() {
- if (!migrationState.isRunning || migrationState.isPaused) {
- return;
- }
- const batch = migrationState.migrationQueue.splice(0, migrationState.batchSize);
- if (batch.length === 0) {
- // Migration complete
- migrationState.isRunning = false;
- migrationState.progress = 100;
- addToLog(`Migration completed. Migrated ${migrationState.migratedAttachments} attachments.`);
- return;
- }
- let successCount = 0;
- batch.forEach(attachmentId => {
- if (migrateAttachment(attachmentId)) {
- successCount++;
- migrationState.migratedAttachments++;
- }
- });
- // Update progress
- migrationState.progress = Math.round((migrationState.migratedAttachments / migrationState.totalAttachments) * 100);
-
- addToLog(`Processed batch: ${successCount}/${batch.length} successful. Progress: ${migrationState.progress}%`);
- // Check CPU usage
- const currentTime = Date.now();
- if (currentTime - migrationState.lastCpuCheck > 5000) { // Check every 5 seconds
- const cpuUsage = getCpuUsage();
- migrationState.lastCpuCheck = currentTime;
-
- if (cpuUsage > migrationState.cpuThreshold) {
- addToLog(`CPU usage ${cpuUsage}% exceeds threshold ${migrationState.cpuThreshold}%. Pausing migration.`);
- migrationState.isPaused = true;
- return;
- }
- }
- // Schedule next batch
- if (migrationState.isRunning && !migrationState.isPaused) {
- Meteor.setTimeout(() => {
- processBatch();
- }, migrationState.delayMs);
- }
- }
- // Initialize migration queue
- function initializeMigrationQueue() {
- const allAttachments = ReactiveCache.getAttachments();
- migrationState.totalAttachments = allAttachments.length;
- migrationState.migrationQueue = allAttachments.map(attachment => attachment._id);
- migrationState.migratedAttachments = 0;
- migrationState.progress = 0;
-
- addToLog(`Initialized migration queue with ${migrationState.totalAttachments} attachments`);
- }
- // Start migration
- function startMigration(targetStorage, batchSize, delayMs, cpuThreshold) {
- if (migrationState.isRunning) {
- throw new Meteor.Error('migration-already-running', 'Migration is already running');
- }
- migrationState.isRunning = true;
- migrationState.isPaused = false;
- migrationState.targetStorage = targetStorage;
- migrationState.batchSize = batchSize;
- migrationState.delayMs = delayMs;
- migrationState.cpuThreshold = cpuThreshold;
- migrationState.startTime = Date.now();
- migrationState.lastCpuCheck = 0;
- initializeMigrationQueue();
- addToLog(`Started migration to ${targetStorage} with batch size ${batchSize}, delay ${delayMs}ms, CPU threshold ${cpuThreshold}%`);
- // Start processing
- processBatch();
- }
- // Pause migration
- function pauseMigration() {
- if (!migrationState.isRunning) {
- throw new Meteor.Error('migration-not-running', 'No migration is currently running');
- }
- migrationState.isPaused = true;
- addToLog('Migration paused');
- }
- // Resume migration
- function resumeMigration() {
- if (!migrationState.isRunning) {
- throw new Meteor.Error('migration-not-running', 'No migration is currently running');
- }
- if (!migrationState.isPaused) {
- throw new Meteor.Error('migration-not-paused', 'Migration is not paused');
- }
- migrationState.isPaused = false;
- addToLog('Migration resumed');
-
- // Continue processing
- processBatch();
- }
- // Stop migration
- function stopMigration() {
- if (!migrationState.isRunning) {
- throw new Meteor.Error('migration-not-running', 'No migration is currently running');
- }
- migrationState.isRunning = false;
- migrationState.isPaused = false;
- migrationState.migrationQueue = [];
- addToLog('Migration stopped');
- }
- // Get attachment storage configuration
- function getAttachmentStorageConfiguration() {
- const config = {
- filesystemPath: process.env.WRITABLE_PATH || '/data',
- attachmentsPath: `${process.env.WRITABLE_PATH || '/data'}/attachments`,
- avatarsPath: `${process.env.WRITABLE_PATH || '/data'}/avatars`,
- gridfsEnabled: true, // Always available
- s3Enabled: false,
- s3Endpoint: '',
- s3Bucket: '',
- s3Region: '',
- s3SslEnabled: false,
- s3Port: 443
- };
- // Check S3 configuration
- if (process.env.S3) {
- try {
- const s3Config = JSON.parse(process.env.S3).s3;
- if (s3Config && s3Config.key && s3Config.secret && s3Config.bucket) {
- config.s3Enabled = true;
- config.s3Endpoint = s3Config.endPoint || '';
- config.s3Bucket = s3Config.bucket || '';
- config.s3Region = s3Config.region || '';
- config.s3SslEnabled = s3Config.sslEnabled || false;
- config.s3Port = s3Config.port || 443;
- }
- } catch (error) {
- console.error('Error parsing S3 configuration:', error);
- }
- }
- return config;
- }
- // Get attachment monitoring data
- function getAttachmentMonitoringData() {
- const attachments = ReactiveCache.getAttachments();
- const stats = {
- totalAttachments: attachments.length,
- filesystemAttachments: 0,
- gridfsAttachments: 0,
- s3Attachments: 0,
- totalSize: 0,
- filesystemSize: 0,
- gridfsSize: 0,
- s3Size: 0
- };
- attachments.forEach(attachment => {
- const storage = fileStoreStrategyFactory.getFileStrategy(attachment, 'original').getStorageName();
- const size = attachment.size || 0;
-
- stats.totalSize += size;
-
- switch (storage) {
- case 'fs':
- stats.filesystemAttachments++;
- stats.filesystemSize += size;
- break;
- case 'gridfs':
- stats.gridfsAttachments++;
- stats.gridfsSize += size;
- break;
- case 's3':
- stats.s3Attachments++;
- stats.s3Size += size;
- break;
- }
- });
- return stats;
- }
- // Test S3 connection
- function testS3Connection(s3Config) {
- // This would implement actual S3 connection testing
- // For now, we'll just validate the configuration
- if (!s3Config.secretKey) {
- throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
- }
- // In a real implementation, you would test the connection here
- // For now, we'll just return success
- return { success: true, message: 'S3 connection test successful' };
- }
- // Save S3 settings
- function saveS3Settings(s3Config) {
- if (!s3Config.secretKey) {
- throw new Meteor.Error('s3-secret-key-required', 'S3 secret key is required');
- }
- // In a real implementation, you would save the S3 configuration
- // For now, we'll just return success
- return { success: true, message: 'S3 settings saved successfully' };
- }
- // Meteor methods
- if (Meteor.isServer) {
- Meteor.methods({
- // Migration methods
- 'startAttachmentMigration'(config) {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- startMigration(config.targetStorage, config.batchSize, config.delayMs, config.cpuThreshold);
- return { success: true, message: 'Migration started' };
- },
- 'pauseAttachmentMigration'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- pauseMigration();
- return { success: true, message: 'Migration paused' };
- },
- 'resumeAttachmentMigration'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- resumeMigration();
- return { success: true, message: 'Migration resumed' };
- },
- 'stopAttachmentMigration'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- stopMigration();
- return { success: true, message: 'Migration stopped' };
- },
- 'getAttachmentMigrationSettings'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return {
- batchSize: migrationState.batchSize,
- delayMs: migrationState.delayMs,
- cpuThreshold: migrationState.cpuThreshold,
- status: migrationState.isRunning ? (migrationState.isPaused ? 'paused' : 'running') : 'idle',
- progress: migrationState.progress
- };
- },
- // Configuration methods
- 'getAttachmentStorageConfiguration'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return getAttachmentStorageConfiguration();
- },
- 'testS3Connection'(s3Config) {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return testS3Connection(s3Config);
- },
- 'saveS3Settings'(s3Config) {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return saveS3Settings(s3Config);
- },
- // Monitoring methods
- 'getAttachmentMonitoringData'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return getAttachmentMonitoringData();
- },
- 'refreshAttachmentMonitoringData'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- return getAttachmentMonitoringData();
- },
- 'exportAttachmentMonitoringData'() {
- if (!this.userId) {
- throw new Meteor.Error('not-authorized', 'Must be logged in');
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- throw new Meteor.Error('not-authorized', 'Admin access required');
- }
- const monitoringData = getAttachmentMonitoringData();
- const migrationStatus = getMigrationStatus();
-
- return {
- timestamp: new Date().toISOString(),
- monitoring: monitoringData,
- migration: migrationStatus,
- system: {
- cpuUsage: getCpuUsage(),
- memoryUsage: process.memoryUsage(),
- uptime: process.uptime()
- }
- };
- }
- });
- // Publications
- Meteor.publish('attachmentMigrationStatus', function() {
- if (!this.userId) {
- return this.ready();
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- return this.ready();
- }
- const self = this;
- let handle;
- function updateStatus() {
- const status = getMigrationStatus();
- self.changed('attachmentMigrationStatus', 'status', status);
- }
- self.added('attachmentMigrationStatus', 'status', getMigrationStatus());
- // Update every 2 seconds
- handle = Meteor.setInterval(updateStatus, 2000);
- self.ready();
- self.onStop(() => {
- if (handle) {
- Meteor.clearInterval(handle);
- }
- });
- });
- Meteor.publish('attachmentMonitoringData', function() {
- if (!this.userId) {
- return this.ready();
- }
- const user = ReactiveCache.getUser(this.userId);
- if (!user || !user.isAdmin) {
- return this.ready();
- }
- const self = this;
- let handle;
- function updateMonitoring() {
- const data = getAttachmentMonitoringData();
- self.changed('attachmentMonitoringData', 'data', data);
- }
- self.added('attachmentMonitoringData', 'data', getAttachmentMonitoringData());
- // Update every 10 seconds
- handle = Meteor.setInterval(updateMonitoring, 10000);
- self.ready();
- self.onStop(() => {
- if (handle) {
- Meteor.clearInterval(handle);
- }
- });
- });
- }
|