attachmentSettings.js 16 KB

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