From 8224ca7650617e172b89e62dcf95850803787fbd Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 18 Aug 2022 18:46:42 -0500 Subject: [PATCH] Add:Set schedule for automatic backups #822 --- .../components/modals/BackupScheduleModal.vue | 91 +++++++++++++ client/components/ui/Tooltip.vue | 7 - .../widgets/CronExpressionBuilder.vue | 6 - client/nuxt.config.js | 3 +- client/pages/config/backups.vue | 54 ++++---- client/plugins/init.client.js | 86 ------------ client/plugins/utils.js | 128 ++++++++++++++++++ 7 files changed, 247 insertions(+), 128 deletions(-) create mode 100644 client/components/modals/BackupScheduleModal.vue create mode 100644 client/plugins/utils.js diff --git a/client/components/modals/BackupScheduleModal.vue b/client/components/modals/BackupScheduleModal.vue new file mode 100644 index 00000000..422c970d --- /dev/null +++ b/client/components/modals/BackupScheduleModal.vue @@ -0,0 +1,91 @@ + + + diff --git a/client/components/ui/Tooltip.vue b/client/components/ui/Tooltip.vue index eed16b25..dbaf4efe 100644 --- a/client/components/ui/Tooltip.vue +++ b/client/components/ui/Tooltip.vue @@ -41,13 +41,6 @@ export default { this.setTooltipPosition(this.tooltip) } }, - getTextWidth() { - var styles = { - 'font-size': '0.75rem' - } - var size = this.$calculateTextSize(this.text, styles) - return size.width - }, createTooltip() { if (!this.$refs.box) return var tooltip = document.createElement('div') diff --git a/client/components/widgets/CronExpressionBuilder.vue b/client/components/widgets/CronExpressionBuilder.vue index 0ad6c50f..061f80d7 100644 --- a/client/components/widgets/CronExpressionBuilder.vue +++ b/client/components/widgets/CronExpressionBuilder.vue @@ -231,11 +231,6 @@ export default { this.isValid = false return } - // if (this.customCronExpression.split(' ')[0] === '*') { - // this.customCronError = 'Cannot use * in minutes position' - // this.isValid = false - // return - // } if (this.customCronExpression !== this.cronExpression) { this.selectedWeekdays = [] @@ -306,7 +301,6 @@ export default { this.selectedMinute = pieces[0] } } - this.cronExpression = this.value this.customCronExpression = this.value } diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 4264694e..c5f9436f 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -48,7 +48,8 @@ module.exports = { '@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', - '@/plugins/toast.js' + '@/plugins/toast.js', + '@/plugins/utils.js' ], // Auto import components: https://go.nuxtjs.dev/config-components diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index bd713141..5dbbf54a 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -8,17 +8,19 @@

Backups include users, user progress, book details, server settings and covers stored in /metadata/items.
Backups do not include any files stored in your library folders.

- - -

Run daily backups info_outlined

+ + +

Enable automatic backups info_outlined

- +
+
+ schedule +

{{ scheduleDescription }}

+ edit +
+
@@ -36,6 +38,8 @@
+ + @@ -44,11 +48,12 @@ export default { data() { return { updatingServerSettings: false, - dailyBackups: true, + enableBackups: true, backupsToKeep: 2, maxBackupSize: 1, - // cronExpression: '', - newServerSettings: {} + cronExpression: '', + newServerSettings: {}, + showCronBuilder: false } }, watch: { @@ -60,29 +65,22 @@ export default { } }, computed: { - dailyBackupsTooltip() { - return 'Runs at 1:30am every day (your server time). Saved in /metadata/backups.' + backupsTooltip() { + return 'Backups saved in /metadata/backups' }, maxBackupSizeTooltip() { return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.' }, serverSettings() { return this.$store.state.serverSettings + }, + scheduleDescription() { + if (!this.cronExpression) return '' + const parsed = this.$parseCronExpression(this.cronExpression) + return parsed ? parsed.description : 'Custom cron expression ' + this.cronExpression } }, methods: { - // changedCronExpression() { - // this.$axios - // .$post('/api/validate-cron', { expression: this.cronExpression }) - // .then(() => { - // console.log('Cron is valid') - // }) - // .catch((error) => { - // console.error('Cron validation failed', error) - // const msg = (error.response ? error.response.data : null) || 'Unknown cron validation error' - // this.$toast.error(msg) - // }) - // }, updateBackupsSettings() { if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) { this.$toast.error('Invalid maximum backup size') @@ -93,7 +91,7 @@ export default { return } var updatePayload = { - backupSchedule: this.dailyBackups ? '30 1 * * *' : false, + backupSchedule: this.enableBackups ? this.cronExpression : false, backupsToKeep: Number(this.backupsToKeep), maxBackupSize: Number(this.maxBackupSize) } @@ -116,9 +114,9 @@ export default { this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} this.backupsToKeep = this.newServerSettings.backupsToKeep || 2 - this.dailyBackups = !!this.newServerSettings.backupSchedule + this.enableBackups = !!this.newServerSettings.backupSchedule this.maxBackupSize = this.newServerSettings.maxBackupSize || 1 - // this.cronExpression = '30 1 * * *' + this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *' } }, mounted() { diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index a2c9ceb6..75841d64 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -30,92 +30,6 @@ Vue.prototype.$addDaysToDate = (jsdate, daysToAdd) => { return date } -Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { - if (isNaN(bytes) || bytes == 0) { - return '0 Bytes' - } - const k = 1024 - const dm = decimals < 0 ? 0 : decimals - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] -} - -Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { - if (seconds < 60) { - return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}` - } - var minutes = Math.floor(seconds / 60) - if (minutes < 70) { - return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}` - } - var hours = Math.floor(minutes / 60) - minutes -= hours * 60 - if (!minutes) { - return `${hours} ${useFullNames ? 'hours' : 'hr'}` - } - return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}` -} - -Vue.prototype.$secondsToTimestamp = (seconds) => { - if (!seconds) return '0:00' - var _seconds = seconds - var _minutes = Math.floor(seconds / 60) - _seconds -= _minutes * 60 - var _hours = Math.floor(_minutes / 60) - _minutes -= _hours * 60 - _seconds = Math.floor(_seconds) - if (!_hours) { - return `${_minutes}:${_seconds.toString().padStart(2, '0')}` - } - return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}` -} - -Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => { - if (isNaN(seconds) || seconds === null) return '' - seconds = Math.round(seconds) - - var minutes = Math.floor(seconds / 60) - seconds -= minutes * 60 - var hours = Math.floor(minutes / 60) - minutes -= hours * 60 - - var days = 0 - if (useDays || Math.floor(hours / 24) >= 100) { - days = Math.floor(hours / 24) - hours -= days * 24 - } - - var strs = [] - if (days) strs.push(`${days}d`) - if (hours) strs.push(`${hours}h`) - if (minutes) strs.push(`${minutes}m`) - if (seconds) strs.push(`${seconds}s`) - return strs.join(' ') -} - -Vue.prototype.$calculateTextSize = (text, styles = {}) => { - const el = document.createElement('p') - - let attr = 'margin:0px;opacity:1;position:absolute;top:100px;left:100px;z-index:99;' - for (const key in styles) { - if (styles[key] && String(styles[key]).length > 0) { - attr += `${key}:${styles[key]};` - } - } - - el.setAttribute('style', attr) - el.innerText = text - - document.body.appendChild(el) - const boundingBox = el.getBoundingClientRect() - el.remove() - return { - height: boundingBox.height, - width: boundingBox.width - } -} - Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => { if (typeof input !== 'string') { return false diff --git a/client/plugins/utils.js b/client/plugins/utils.js new file mode 100644 index 00000000..cb7198c8 --- /dev/null +++ b/client/plugins/utils.js @@ -0,0 +1,128 @@ +import Vue from 'vue' + +Vue.prototype.$bytesPretty = (bytes, decimals = 2) => { + if (isNaN(bytes) || bytes == 0) { + return '0 Bytes' + } + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] +} + +Vue.prototype.$elapsedPretty = (seconds, useFullNames = false) => { + if (seconds < 60) { + return `${Math.floor(seconds)} sec${useFullNames ? 'onds' : ''}` + } + var minutes = Math.floor(seconds / 60) + if (minutes < 70) { + return `${minutes} min${useFullNames ? `ute${minutes === 1 ? '' : 's'}` : ''}` + } + var hours = Math.floor(minutes / 60) + minutes -= hours * 60 + if (!minutes) { + return `${hours} ${useFullNames ? 'hours' : 'hr'}` + } + return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}` +} + +Vue.prototype.$secondsToTimestamp = (seconds) => { + if (!seconds) return '0:00' + var _seconds = seconds + var _minutes = Math.floor(seconds / 60) + _seconds -= _minutes * 60 + var _hours = Math.floor(_minutes / 60) + _minutes -= _hours * 60 + _seconds = Math.floor(_seconds) + if (!_hours) { + return `${_minutes}:${_seconds.toString().padStart(2, '0')}` + } + return `${_hours}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}` +} + +Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => { + if (isNaN(seconds) || seconds === null) return '' + seconds = Math.round(seconds) + + var minutes = Math.floor(seconds / 60) + seconds -= minutes * 60 + var hours = Math.floor(minutes / 60) + minutes -= hours * 60 + + var days = 0 + if (useDays || Math.floor(hours / 24) >= 100) { + days = Math.floor(hours / 24) + hours -= days * 24 + } + + var strs = [] + if (days) strs.push(`${days}d`) + if (hours) strs.push(`${hours}h`) + if (minutes) strs.push(`${minutes}m`) + if (seconds) strs.push(`${seconds}s`) + return strs.join(' ') +} + +Vue.prototype.$parseCronExpression = (expression) => { + if (!expression) return null + const pieces = expression.split(' ') + if (pieces.length !== 5) { + return null + } + + const commonPatterns = [ + { + text: 'Every 12 hours', + value: '0 */12 * * *' + }, + { + text: 'Every 6 hours', + value: '0 */6 * * *' + }, + { + text: 'Every 2 hours', + value: '0 */2 * * *' + }, + { + text: 'Every hour', + value: '0 * * * *' + }, + { + text: 'Every 30 minutes', + value: '*/30 * * * *' + }, + { + text: 'Every 15 minutes', + value: '*/15 * * * *' + }, + { + text: 'Every minute', + value: '* * * * *' + } + ] + const patternMatch = commonPatterns.find(p => p.value === expression) + if (patternMatch) { + return { + description: patternMatch.text + } + } + + if (isNaN(pieces[0]) || isNaN(pieces[1])) { + return null + } + if (pieces[2] !== '*' || pieces[3] !== '*') { + return null + } + if (pieces[4] !== '*' && pieces[4].split(',').some(p => isNaN(p))) { + return null + } + + const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + var weekdayText = 'day' + if (pieces[4] !== '*') weekdayText = pieces[4].split(',').map(p => weekdays[p]).join(', ') + + return { + description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}` + } +} \ No newline at end of file