| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 | import { ReactiveCache } from '/imports/reactiveCache';import { TAPi18n } from '/imports/i18n';// Template helpers for attachmentSettingsTemplate.attachmentSettings.helpers({  loading() {    const instance = Template.instance();    if (instance && instance.loading) {      return instance.loading.get();    }    return attachmentSettings.loading.get();  },  showStorageSettings() {    const instance = Template.instance();    if (instance && instance.showStorageSettings) {      return instance.showStorageSettings.get();    }    return attachmentSettings.showStorageSettings.get();  },  showMigration() {    const instance = Template.instance();    if (instance && instance.showMigration) {      return instance.showMigration.get();    }    return attachmentSettings.showMigration.get();  },  showMonitoring() {    const instance = Template.instance();    if (instance && instance.showMonitoring) {      return instance.showMonitoring.get();    }    return attachmentSettings.showMonitoring.get();  }});import { Meteor } from 'meteor/meteor';import { Session } from 'meteor/session';import { Tracker } from 'meteor/tracker';import { ReactiveVar } from 'meteor/reactive-var';import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';import { Chart } from 'chart.js';// Global reactive variables for attachment settingsconst attachmentSettings = {  loading: new ReactiveVar(false),  showStorageSettings: new ReactiveVar(false),  showMigration: new ReactiveVar(false),  showMonitoring: new ReactiveVar(false),    // Storage configuration  filesystemPath: new ReactiveVar(''),  attachmentsPath: new ReactiveVar(''),  avatarsPath: new ReactiveVar(''),  gridfsEnabled: new ReactiveVar(false),  s3Enabled: new ReactiveVar(false),  s3Endpoint: new ReactiveVar(''),  s3Bucket: new ReactiveVar(''),  s3Region: new ReactiveVar(''),  s3SslEnabled: new ReactiveVar(false),  s3Port: new ReactiveVar(443),    // Migration settings  migrationBatchSize: new ReactiveVar(10),  migrationDelayMs: new ReactiveVar(1000),  migrationCpuThreshold: new ReactiveVar(70),  migrationProgress: new ReactiveVar(0),  migrationStatus: new ReactiveVar('idle'),  migrationLog: new ReactiveVar(''),    // Monitoring data  totalAttachments: new ReactiveVar(0),  filesystemAttachments: new ReactiveVar(0),  gridfsAttachments: new ReactiveVar(0),  s3Attachments: new ReactiveVar(0),  totalSize: new ReactiveVar(0),  filesystemSize: new ReactiveVar(0),  gridfsSize: new ReactiveVar(0),  s3Size: new ReactiveVar(0),    // Migration state  isMigrationRunning: new ReactiveVar(false),  isMigrationPaused: new ReactiveVar(false),  migrationQueue: new ReactiveVar([]),  currentMigration: new ReactiveVar(null)};// Main attachment settings componentBlazeComponent.extendComponent({  onCreated() {    this.loading = attachmentSettings.loading;    this.showStorageSettings = attachmentSettings.showStorageSettings;    this.showMigration = attachmentSettings.showMigration;    this.showMonitoring = attachmentSettings.showMonitoring;        // Set default sub-menu state    this.showStorageSettings.set(true);    this.showMigration.set(false);    this.showMonitoring.set(false);        // Load initial data    this.loadStorageConfiguration();    this.loadMigrationSettings();    this.loadMonitoringData();  },  events() {    return [      {        'click a.js-attachment-storage-settings': this.switchToStorageSettings,        'click a.js-attachment-migration': this.switchToMigration,        'click a.js-attachment-monitoring': this.switchToMonitoring,      }    ];  },  switchToStorageSettings(event) {    this.switchMenu(event, 'storage-settings');    this.showStorageSettings.set(true);    this.showMigration.set(false);    this.showMonitoring.set(false);  },  switchToMigration(event) {    this.switchMenu(event, 'attachment-migration');    this.showStorageSettings.set(false);    this.showMigration.set(true);    this.showMonitoring.set(false);  },  switchToMonitoring(event) {    this.switchMenu(event, 'attachment-monitoring');    this.showStorageSettings.set(false);    this.showMigration.set(false);    this.showMonitoring.set(true);  },  switchMenu(event, targetId) {    const target = $(event.target);    if (!target.hasClass('active')) {      this.loading.set(true);            $('.side-menu li.active').removeClass('active');      target.parent().addClass('active');            // Load data based on target      if (targetId === 'storage-settings') {        this.loadStorageConfiguration();      } else if (targetId === 'attachment-migration') {        this.loadMigrationSettings();      } else if (targetId === 'attachment-monitoring') {        this.loadMonitoringData();      }            this.loading.set(false);    }  },  loadStorageConfiguration() {    Meteor.call('getAttachmentStorageConfiguration', (error, result) => {      if (!error && result) {        attachmentSettings.filesystemPath.set(result.filesystemPath || '');        attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');        attachmentSettings.avatarsPath.set(result.avatarsPath || '');        attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);        attachmentSettings.s3Enabled.set(result.s3Enabled || false);        attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');        attachmentSettings.s3Bucket.set(result.s3Bucket || '');        attachmentSettings.s3Region.set(result.s3Region || '');        attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);        attachmentSettings.s3Port.set(result.s3Port || 443);      }    });  },  loadMigrationSettings() {    Meteor.call('getAttachmentMigrationSettings', (error, result) => {      if (!error && result) {        attachmentSettings.migrationBatchSize.set(result.batchSize || 10);        attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);        attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);        attachmentSettings.migrationStatus.set(result.status || 'idle');        attachmentSettings.migrationProgress.set(result.progress || 0);      }    });  },  loadMonitoringData() {    Meteor.call('getAttachmentMonitoringData', (error, result) => {      if (!error && result) {        attachmentSettings.totalAttachments.set(result.totalAttachments || 0);        attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);        attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);        attachmentSettings.s3Attachments.set(result.s3Attachments || 0);        attachmentSettings.totalSize.set(result.totalSize || 0);        attachmentSettings.filesystemSize.set(result.filesystemSize || 0);        attachmentSettings.gridfsSize.set(result.gridfsSize || 0);        attachmentSettings.s3Size.set(result.s3Size || 0);      }    });  }}).register('attachmentSettings');// Storage settings componentBlazeComponent.extendComponent({  onCreated() {    this.filesystemPath = attachmentSettings.filesystemPath;    this.attachmentsPath = attachmentSettings.attachmentsPath;    this.avatarsPath = attachmentSettings.avatarsPath;    this.gridfsEnabled = attachmentSettings.gridfsEnabled;    this.s3Enabled = attachmentSettings.s3Enabled;    this.s3Endpoint = attachmentSettings.s3Endpoint;    this.s3Bucket = attachmentSettings.s3Bucket;    this.s3Region = attachmentSettings.s3Region;    this.s3SslEnabled = attachmentSettings.s3SslEnabled;    this.s3Port = attachmentSettings.s3Port;  },  events() {    return [      {        'click button.js-test-s3-connection': this.testS3Connection,        'click button.js-save-s3-settings': this.saveS3Settings,        'change input#s3-secret-key': this.updateS3SecretKey      }    ];  },  testS3Connection() {    const secretKey = $('#s3-secret-key').val();    if (!secretKey) {      alert(TAPi18n.__('s3-secret-key-required'));      return;    }    Meteor.call('testS3Connection', { secretKey }, (error, result) => {      if (error) {        alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);      } else {        alert(TAPi18n.__('s3-connection-success'));      }    });  },  saveS3Settings() {    const secretKey = $('#s3-secret-key').val();    if (!secretKey) {      alert(TAPi18n.__('s3-secret-key-required'));      return;    }    Meteor.call('saveS3Settings', { secretKey }, (error, result) => {      if (error) {        alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);      } else {        alert(TAPi18n.__('s3-settings-saved'));        $('#s3-secret-key').val(''); // Clear the password field      }    });  },  updateS3SecretKey(event) {    // This method can be used to validate the secret key format    const secretKey = event.target.value;    // Add validation logic here if needed  }}).register('storageSettings');// Migration componentBlazeComponent.extendComponent({  onCreated() {    this.migrationBatchSize = attachmentSettings.migrationBatchSize;    this.migrationDelayMs = attachmentSettings.migrationDelayMs;    this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;    this.migrationProgress = attachmentSettings.migrationProgress;    this.migrationStatus = attachmentSettings.migrationStatus;    this.migrationLog = attachmentSettings.migrationLog;    this.isMigrationRunning = attachmentSettings.isMigrationRunning;    this.isMigrationPaused = attachmentSettings.isMigrationPaused;        // Subscribe to migration updates    this.subscription = Meteor.subscribe('attachmentMigrationStatus');        // Set up reactive updates    this.autorun(() => {      const status = attachmentSettings.migrationStatus.get();      if (status === 'running') {        this.isMigrationRunning.set(true);      } else {        this.isMigrationRunning.set(false);      }    });  },  onDestroyed() {    if (this.subscription) {      this.subscription.stop();    }  },  events() {    return [      {        'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),        'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),        'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),        'click button.js-pause-migration': this.pauseMigration,        'click button.js-resume-migration': this.resumeMigration,        'click button.js-stop-migration': this.stopMigration,        'change input#migration-batch-size': this.updateBatchSize,        'change input#migration-delay-ms': this.updateDelayMs,        'change input#migration-cpu-threshold': this.updateCpuThreshold      }    ];  },  startMigration(targetStorage) {    const batchSize = parseInt($('#migration-batch-size').val()) || 10;    const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;    const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;    Meteor.call('startAttachmentMigration', {      targetStorage,      batchSize,      delayMs,      cpuThreshold    }, (error, result) => {      if (error) {        alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);      } else {        this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);      }    });  },  pauseMigration() {    Meteor.call('pauseAttachmentMigration', (error, result) => {      if (error) {        alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);      } else {        this.addToLog(TAPi18n.__('migration-paused'));      }    });  },  resumeMigration() {    Meteor.call('resumeAttachmentMigration', (error, result) => {      if (error) {        alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);      } else {        this.addToLog(TAPi18n.__('migration-resumed'));      }    });  },  stopMigration() {    if (confirm(TAPi18n.__('migration-stop-confirm'))) {      Meteor.call('stopAttachmentMigration', (error, result) => {        if (error) {          alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);        } else {          this.addToLog(TAPi18n.__('migration-stopped'));        }      });    }  },  updateBatchSize(event) {    const value = parseInt(event.target.value);    if (value >= 1 && value <= 100) {      attachmentSettings.migrationBatchSize.set(value);    }  },  updateDelayMs(event) {    const value = parseInt(event.target.value);    if (value >= 100 && value <= 10000) {      attachmentSettings.migrationDelayMs.set(value);    }  },  updateCpuThreshold(event) {    const value = parseInt(event.target.value);    if (value >= 10 && value <= 90) {      attachmentSettings.migrationCpuThreshold.set(value);    }  },  addToLog(message) {    const timestamp = new Date().toISOString();    const currentLog = attachmentSettings.migrationLog.get();    const newLog = `[${timestamp}] ${message}\n${currentLog}`;    attachmentSettings.migrationLog.set(newLog);  }}).register('attachmentMigration');// Monitoring componentBlazeComponent.extendComponent({  onCreated() {    this.totalAttachments = attachmentSettings.totalAttachments;    this.filesystemAttachments = attachmentSettings.filesystemAttachments;    this.gridfsAttachments = attachmentSettings.gridfsAttachments;    this.s3Attachments = attachmentSettings.s3Attachments;    this.totalSize = attachmentSettings.totalSize;    this.filesystemSize = attachmentSettings.filesystemSize;    this.gridfsSize = attachmentSettings.gridfsSize;    this.s3Size = attachmentSettings.s3Size;        // Subscribe to monitoring updates    this.subscription = Meteor.subscribe('attachmentMonitoringData');        // Set up chart    this.autorun(() => {      this.updateChart();    });  },  onDestroyed() {    if (this.subscription) {      this.subscription.stop();    }  },  events() {    return [      {        'click button.js-refresh-monitoring': this.refreshMonitoring,        'click button.js-export-monitoring': this.exportMonitoring      }    ];  },  refreshMonitoring() {    Meteor.call('refreshAttachmentMonitoringData', (error, result) => {      if (error) {        alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);      }    });  },  exportMonitoring() {    Meteor.call('exportAttachmentMonitoringData', (error, result) => {      if (error) {        alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);      } else {        // Download the exported data        const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });        const url = URL.createObjectURL(blob);        const a = document.createElement('a');        a.href = url;        a.download = 'wekan-attachment-monitoring.json';        document.body.appendChild(a);        a.click();        document.body.removeChild(a);        URL.revokeObjectURL(url);      }    });  },  updateChart() {    const ctx = document.getElementById('storage-distribution-chart');    if (!ctx) return;    const filesystemCount = this.filesystemAttachments.get();    const gridfsCount = this.gridfsAttachments.get();    const s3Count = this.s3Attachments.get();    if (this.chart) {      this.chart.destroy();    }    this.chart = new Chart(ctx, {      type: 'doughnut',      data: {        labels: [          TAPi18n.__('filesystem-storage'),          TAPi18n.__('gridfs-storage'),          TAPi18n.__('s3-storage')        ],        datasets: [{          data: [filesystemCount, gridfsCount, s3Count],          backgroundColor: [            '#28a745',            '#007bff',            '#ffc107'          ]        }]      },      options: {        responsive: true,        maintainAspectRatio: false,        plugins: {          legend: {            position: 'bottom'          }        }      }    });  }}).register('attachmentMonitoring');// Export the attachment settings for use in other componentsexport { attachmentSettings };
 |