attachmentSettings.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. // Template helpers for attachmentSettings
  4. Template.attachmentSettings.helpers({
  5. loading() {
  6. return attachmentSettings.loading.get();
  7. },
  8. showStorageSettings() {
  9. return attachmentSettings.showStorageSettings.get();
  10. },
  11. showMigration() {
  12. return attachmentSettings.showMigration.get();
  13. },
  14. showMonitoring() {
  15. return attachmentSettings.showMonitoring.get();
  16. }
  17. });
  18. import { Meteor } from 'meteor/meteor';
  19. import { Session } from 'meteor/session';
  20. import { Tracker } from 'meteor/tracker';
  21. import { ReactiveVar } from 'meteor/reactive-var';
  22. import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
  23. import { Chart } from 'chart.js';
  24. // Global reactive variables for attachment settings
  25. const attachmentSettings = {
  26. loading: new ReactiveVar(false),
  27. showStorageSettings: new ReactiveVar(false),
  28. showMigration: new ReactiveVar(false),
  29. showMonitoring: new ReactiveVar(false),
  30. // Storage configuration
  31. filesystemPath: new ReactiveVar(''),
  32. attachmentsPath: new ReactiveVar(''),
  33. avatarsPath: new ReactiveVar(''),
  34. gridfsEnabled: new ReactiveVar(false),
  35. s3Enabled: new ReactiveVar(false),
  36. s3Endpoint: new ReactiveVar(''),
  37. s3Bucket: new ReactiveVar(''),
  38. s3Region: new ReactiveVar(''),
  39. s3SslEnabled: new ReactiveVar(false),
  40. s3Port: new ReactiveVar(443),
  41. // Migration settings
  42. migrationBatchSize: new ReactiveVar(10),
  43. migrationDelayMs: new ReactiveVar(1000),
  44. migrationCpuThreshold: new ReactiveVar(70),
  45. migrationProgress: new ReactiveVar(0),
  46. migrationStatus: new ReactiveVar('idle'),
  47. migrationLog: new ReactiveVar(''),
  48. // Monitoring data
  49. totalAttachments: new ReactiveVar(0),
  50. filesystemAttachments: new ReactiveVar(0),
  51. gridfsAttachments: new ReactiveVar(0),
  52. s3Attachments: new ReactiveVar(0),
  53. totalSize: new ReactiveVar(0),
  54. filesystemSize: new ReactiveVar(0),
  55. gridfsSize: new ReactiveVar(0),
  56. s3Size: new ReactiveVar(0),
  57. // Migration state
  58. isMigrationRunning: new ReactiveVar(false),
  59. isMigrationPaused: new ReactiveVar(false),
  60. migrationQueue: new ReactiveVar([]),
  61. currentMigration: new ReactiveVar(null)
  62. };
  63. // Main attachment settings component
  64. BlazeComponent.extendComponent({
  65. onCreated() {
  66. this.loading = attachmentSettings.loading;
  67. this.showStorageSettings = attachmentSettings.showStorageSettings;
  68. this.showMigration = attachmentSettings.showMigration;
  69. this.showMonitoring = attachmentSettings.showMonitoring;
  70. // Set default sub-menu state
  71. this.showStorageSettings.set(true);
  72. this.showMigration.set(false);
  73. this.showMonitoring.set(false);
  74. // Load initial data
  75. this.loadStorageConfiguration();
  76. this.loadMigrationSettings();
  77. this.loadMonitoringData();
  78. },
  79. events() {
  80. return [
  81. {
  82. 'click a.js-attachment-storage-settings': this.switchToStorageSettings,
  83. 'click a.js-attachment-migration': this.switchToMigration,
  84. 'click a.js-attachment-monitoring': this.switchToMonitoring,
  85. }
  86. ];
  87. },
  88. switchToStorageSettings(event) {
  89. this.switchMenu(event, 'storage-settings');
  90. this.showStorageSettings.set(true);
  91. this.showMigration.set(false);
  92. this.showMonitoring.set(false);
  93. },
  94. switchToMigration(event) {
  95. this.switchMenu(event, 'attachment-migration');
  96. this.showStorageSettings.set(false);
  97. this.showMigration.set(true);
  98. this.showMonitoring.set(false);
  99. },
  100. switchToMonitoring(event) {
  101. this.switchMenu(event, 'attachment-monitoring');
  102. this.showStorageSettings.set(false);
  103. this.showMigration.set(false);
  104. this.showMonitoring.set(true);
  105. },
  106. switchMenu(event, targetId) {
  107. const target = $(event.target);
  108. if (!target.hasClass('active')) {
  109. this.loading.set(true);
  110. $('.side-menu li.active').removeClass('active');
  111. target.parent().addClass('active');
  112. // Load data based on target
  113. if (targetId === 'storage-settings') {
  114. this.loadStorageConfiguration();
  115. } else if (targetId === 'attachment-migration') {
  116. this.loadMigrationSettings();
  117. } else if (targetId === 'attachment-monitoring') {
  118. this.loadMonitoringData();
  119. }
  120. this.loading.set(false);
  121. }
  122. },
  123. loadStorageConfiguration() {
  124. Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
  125. if (!error && result) {
  126. attachmentSettings.filesystemPath.set(result.filesystemPath || '');
  127. attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
  128. attachmentSettings.avatarsPath.set(result.avatarsPath || '');
  129. attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
  130. attachmentSettings.s3Enabled.set(result.s3Enabled || false);
  131. attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
  132. attachmentSettings.s3Bucket.set(result.s3Bucket || '');
  133. attachmentSettings.s3Region.set(result.s3Region || '');
  134. attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
  135. attachmentSettings.s3Port.set(result.s3Port || 443);
  136. }
  137. });
  138. },
  139. loadMigrationSettings() {
  140. Meteor.call('getAttachmentMigrationSettings', (error, result) => {
  141. if (!error && result) {
  142. attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
  143. attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
  144. attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
  145. attachmentSettings.migrationStatus.set(result.status || 'idle');
  146. attachmentSettings.migrationProgress.set(result.progress || 0);
  147. }
  148. });
  149. },
  150. loadMonitoringData() {
  151. Meteor.call('getAttachmentMonitoringData', (error, result) => {
  152. if (!error && result) {
  153. attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
  154. attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
  155. attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
  156. attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
  157. attachmentSettings.totalSize.set(result.totalSize || 0);
  158. attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
  159. attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
  160. attachmentSettings.s3Size.set(result.s3Size || 0);
  161. }
  162. });
  163. }
  164. }).register('attachmentSettings');
  165. // Storage settings component
  166. BlazeComponent.extendComponent({
  167. onCreated() {
  168. this.filesystemPath = attachmentSettings.filesystemPath;
  169. this.attachmentsPath = attachmentSettings.attachmentsPath;
  170. this.avatarsPath = attachmentSettings.avatarsPath;
  171. this.gridfsEnabled = attachmentSettings.gridfsEnabled;
  172. this.s3Enabled = attachmentSettings.s3Enabled;
  173. this.s3Endpoint = attachmentSettings.s3Endpoint;
  174. this.s3Bucket = attachmentSettings.s3Bucket;
  175. this.s3Region = attachmentSettings.s3Region;
  176. this.s3SslEnabled = attachmentSettings.s3SslEnabled;
  177. this.s3Port = attachmentSettings.s3Port;
  178. },
  179. events() {
  180. return [
  181. {
  182. 'click button.js-test-s3-connection': this.testS3Connection,
  183. 'click button.js-save-s3-settings': this.saveS3Settings,
  184. 'change input#s3-secret-key': this.updateS3SecretKey
  185. }
  186. ];
  187. },
  188. testS3Connection() {
  189. const secretKey = $('#s3-secret-key').val();
  190. if (!secretKey) {
  191. alert(TAPi18n.__('s3-secret-key-required'));
  192. return;
  193. }
  194. Meteor.call('testS3Connection', { secretKey }, (error, result) => {
  195. if (error) {
  196. alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
  197. } else {
  198. alert(TAPi18n.__('s3-connection-success'));
  199. }
  200. });
  201. },
  202. saveS3Settings() {
  203. const secretKey = $('#s3-secret-key').val();
  204. if (!secretKey) {
  205. alert(TAPi18n.__('s3-secret-key-required'));
  206. return;
  207. }
  208. Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
  209. if (error) {
  210. alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
  211. } else {
  212. alert(TAPi18n.__('s3-settings-saved'));
  213. $('#s3-secret-key').val(''); // Clear the password field
  214. }
  215. });
  216. },
  217. updateS3SecretKey(event) {
  218. // This method can be used to validate the secret key format
  219. const secretKey = event.target.value;
  220. // Add validation logic here if needed
  221. }
  222. }).register('storageSettings');
  223. // Migration component
  224. BlazeComponent.extendComponent({
  225. onCreated() {
  226. this.migrationBatchSize = attachmentSettings.migrationBatchSize;
  227. this.migrationDelayMs = attachmentSettings.migrationDelayMs;
  228. this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
  229. this.migrationProgress = attachmentSettings.migrationProgress;
  230. this.migrationStatus = attachmentSettings.migrationStatus;
  231. this.migrationLog = attachmentSettings.migrationLog;
  232. this.isMigrationRunning = attachmentSettings.isMigrationRunning;
  233. this.isMigrationPaused = attachmentSettings.isMigrationPaused;
  234. // Subscribe to migration updates
  235. this.subscription = Meteor.subscribe('attachmentMigrationStatus');
  236. // Set up reactive updates
  237. this.autorun(() => {
  238. const status = attachmentSettings.migrationStatus.get();
  239. if (status === 'running') {
  240. this.isMigrationRunning.set(true);
  241. } else {
  242. this.isMigrationRunning.set(false);
  243. }
  244. });
  245. },
  246. onDestroyed() {
  247. if (this.subscription) {
  248. this.subscription.stop();
  249. }
  250. },
  251. events() {
  252. return [
  253. {
  254. 'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
  255. 'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
  256. 'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
  257. 'click button.js-pause-migration': this.pauseMigration,
  258. 'click button.js-resume-migration': this.resumeMigration,
  259. 'click button.js-stop-migration': this.stopMigration,
  260. 'change input#migration-batch-size': this.updateBatchSize,
  261. 'change input#migration-delay-ms': this.updateDelayMs,
  262. 'change input#migration-cpu-threshold': this.updateCpuThreshold
  263. }
  264. ];
  265. },
  266. startMigration(targetStorage) {
  267. const batchSize = parseInt($('#migration-batch-size').val()) || 10;
  268. const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
  269. const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
  270. Meteor.call('startAttachmentMigration', {
  271. targetStorage,
  272. batchSize,
  273. delayMs,
  274. cpuThreshold
  275. }, (error, result) => {
  276. if (error) {
  277. alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
  278. } else {
  279. this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
  280. }
  281. });
  282. },
  283. pauseMigration() {
  284. Meteor.call('pauseAttachmentMigration', (error, result) => {
  285. if (error) {
  286. alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
  287. } else {
  288. this.addToLog(TAPi18n.__('migration-paused'));
  289. }
  290. });
  291. },
  292. resumeMigration() {
  293. Meteor.call('resumeAttachmentMigration', (error, result) => {
  294. if (error) {
  295. alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
  296. } else {
  297. this.addToLog(TAPi18n.__('migration-resumed'));
  298. }
  299. });
  300. },
  301. stopMigration() {
  302. if (confirm(TAPi18n.__('migration-stop-confirm'))) {
  303. Meteor.call('stopAttachmentMigration', (error, result) => {
  304. if (error) {
  305. alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
  306. } else {
  307. this.addToLog(TAPi18n.__('migration-stopped'));
  308. }
  309. });
  310. }
  311. },
  312. updateBatchSize(event) {
  313. const value = parseInt(event.target.value);
  314. if (value >= 1 && value <= 100) {
  315. attachmentSettings.migrationBatchSize.set(value);
  316. }
  317. },
  318. updateDelayMs(event) {
  319. const value = parseInt(event.target.value);
  320. if (value >= 100 && value <= 10000) {
  321. attachmentSettings.migrationDelayMs.set(value);
  322. }
  323. },
  324. updateCpuThreshold(event) {
  325. const value = parseInt(event.target.value);
  326. if (value >= 10 && value <= 90) {
  327. attachmentSettings.migrationCpuThreshold.set(value);
  328. }
  329. },
  330. addToLog(message) {
  331. const timestamp = new Date().toISOString();
  332. const currentLog = attachmentSettings.migrationLog.get();
  333. const newLog = `[${timestamp}] ${message}\n${currentLog}`;
  334. attachmentSettings.migrationLog.set(newLog);
  335. }
  336. }).register('attachmentMigration');
  337. // Monitoring component
  338. BlazeComponent.extendComponent({
  339. onCreated() {
  340. this.totalAttachments = attachmentSettings.totalAttachments;
  341. this.filesystemAttachments = attachmentSettings.filesystemAttachments;
  342. this.gridfsAttachments = attachmentSettings.gridfsAttachments;
  343. this.s3Attachments = attachmentSettings.s3Attachments;
  344. this.totalSize = attachmentSettings.totalSize;
  345. this.filesystemSize = attachmentSettings.filesystemSize;
  346. this.gridfsSize = attachmentSettings.gridfsSize;
  347. this.s3Size = attachmentSettings.s3Size;
  348. // Subscribe to monitoring updates
  349. this.subscription = Meteor.subscribe('attachmentMonitoringData');
  350. // Set up chart
  351. this.autorun(() => {
  352. this.updateChart();
  353. });
  354. },
  355. onDestroyed() {
  356. if (this.subscription) {
  357. this.subscription.stop();
  358. }
  359. },
  360. events() {
  361. return [
  362. {
  363. 'click button.js-refresh-monitoring': this.refreshMonitoring,
  364. 'click button.js-export-monitoring': this.exportMonitoring
  365. }
  366. ];
  367. },
  368. refreshMonitoring() {
  369. Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
  370. if (error) {
  371. alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
  372. }
  373. });
  374. },
  375. exportMonitoring() {
  376. Meteor.call('exportAttachmentMonitoringData', (error, result) => {
  377. if (error) {
  378. alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
  379. } else {
  380. // Download the exported data
  381. const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
  382. const url = URL.createObjectURL(blob);
  383. const a = document.createElement('a');
  384. a.href = url;
  385. a.download = 'wekan-attachment-monitoring.json';
  386. document.body.appendChild(a);
  387. a.click();
  388. document.body.removeChild(a);
  389. URL.revokeObjectURL(url);
  390. }
  391. });
  392. },
  393. updateChart() {
  394. const ctx = document.getElementById('storage-distribution-chart');
  395. if (!ctx) return;
  396. const filesystemCount = this.filesystemAttachments.get();
  397. const gridfsCount = this.gridfsAttachments.get();
  398. const s3Count = this.s3Attachments.get();
  399. if (this.chart) {
  400. this.chart.destroy();
  401. }
  402. this.chart = new Chart(ctx, {
  403. type: 'doughnut',
  404. data: {
  405. labels: [
  406. TAPi18n.__('filesystem-storage'),
  407. TAPi18n.__('gridfs-storage'),
  408. TAPi18n.__('s3-storage')
  409. ],
  410. datasets: [{
  411. data: [filesystemCount, gridfsCount, s3Count],
  412. backgroundColor: [
  413. '#28a745',
  414. '#007bff',
  415. '#ffc107'
  416. ]
  417. }]
  418. },
  419. options: {
  420. responsive: true,
  421. maintainAspectRatio: false,
  422. plugins: {
  423. legend: {
  424. position: 'bottom'
  425. }
  426. }
  427. }
  428. });
  429. }
  430. }).register('attachmentMonitoring');
  431. // Export the attachment settings for use in other components
  432. export { attachmentSettings };