Skip to content

Commit

Permalink
Merge pull request #500 from arabcoders/dev
Browse files Browse the repository at this point in the history
Added Backup to the API and exposed via WebUI
  • Loading branch information
arabcoders committed Jun 19, 2024
2 parents 8359b93 + 5a0678c commit daa3e84
Show file tree
Hide file tree
Showing 22 changed files with 573 additions and 104 deletions.
5 changes: 5 additions & 0 deletions frontend/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
<span class="icon"><i class="fas fa-bug-slash"></i></span>
<span>Log Suppression</span>
</NuxtLink>

<NuxtLink class="navbar-item" to="/backup" @click.native="showMenu=false">
<span class="icon"><i class="fas fa-sd-card"></i></span>
<span>Backups</span>
</NuxtLink>
</div>
</div>

Expand Down
7 changes: 4 additions & 3 deletions frontend/pages/backend/[backend]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@
<div class="column is-4-tablet is-6-mobile has-text-left-mobile">
<span class="icon-text">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="moment.unix(history.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(history.updated).fromNow() }}
<span class="has-tooltip"
v-tooltip="`Updated at: ${moment.unix(history.updated_at ?? history.updated).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment.unix(history.updated_at ?? history.updated).fromNow() }}
</span>
</span>
</div>
Expand Down Expand Up @@ -139,7 +140,7 @@
<script setup>
import moment from 'moment'
import Message from '~/components/Message.vue'
import {formatDuration, makeName, notification} from "~/utils/index.js";
import {formatDuration, makeName, notification, TOOLTIP_DATE_FORMAT} from "~/utils/index.js";
const backend = useRoute().params.backend
Expand Down
7 changes: 4 additions & 3 deletions frontend/pages/backend/[backend]/search.vue
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@
<div class="card-footer">
<div class="card-footer-item">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="moment.unix(item.updated).format('YYYY-MM-DD h:mm:ss A')">
{{ moment.unix(item.updated).fromNow() }}
<span class="has-tooltip"
v-tooltip="moment.unix(item.updated_at ?? item.updated).format(TOOLTIP_DATE_FORMAT)">
{{ moment.unix(item.updated_at ?? item.updated).fromNow() }}
</span>
</div>
<div class="card-footer-item">
Expand Down Expand Up @@ -196,7 +197,7 @@
<script setup>
import request from '~/utils/request.js'
import moment from 'moment'
import {makeName, makeSearchLink, notification} from '~/utils/index.js'
import {makeName, makeSearchLink, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js'
import Message from "~/components/Message.vue";
import {useStorage} from "@vueuse/core";
Expand Down
7 changes: 5 additions & 2 deletions frontend/pages/backend/[backend]/users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@
</div>
<div class="column is-6" v-if="item?.updatedAt">
<strong>Updated:</strong> {{ moment(item.updatedAt).fromNow() }}
<strong>Updated:&nbsp;</strong>
<span class="has-tooltip" v-tooltip="moment(item.updatedAt).format(TOOLTIP_DATE_FORMAT)">
{{ moment(item.updatedAt).fromNow() }}
</span>
</div>
<div class="column is-6 has-text-right" v-if="undefined !== item?.restricted">
Expand Down Expand Up @@ -83,7 +86,7 @@
</template>
<script setup>
import {notification} from '~/utils/index.js'
import {notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js'
import {useStorage} from '@vueuse/core'
import request from '~/utils/request.js'
import moment from "moment";
Expand Down
48 changes: 41 additions & 7 deletions frontend/pages/backends/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,50 @@
</header>
<div class="card-content">
<div class="columns is-multiline has-text-centered">
<div class="column is-6 has-text-left-mobile" v-if="backend.export.enabled">
<strong>Last Export:</strong>
{{ backend.export.lastSync ? moment(backend.export.lastSync).fromNow() : 'None' }}
<div class="column is-6 has-text-left-mobile">
<strong>Last Export:&nbsp;</strong>
<template v-if="backend.export.enabled">
<span v-if="backend.export.lastSync" class="has-tooltip"
v-tooltip="moment(backend.export.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.export.lastSync).fromNow() }}
</span>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light">Disabled</span>
</template>
</div>
<div class="column is-6 has-text-left-mobile" v-if="backend.import.enabled">
<strong>Last Import:</strong>
{{ backend.import.lastSync ? moment(backend.import.lastSync).fromNow() : 'None' }}
<div class="column is-6 has-text-left-mobile">
<strong>Last Import:&nbsp;</strong>
<template v-if="backend.import.enabled">
<span v-if="backend.import.lastSync" class="has-tooltip"
v-tooltip="moment(backend.import.lastSync).format(TOOLTIP_DATE_FORMAT)">
{{ moment(backend.import.lastSync).fromNow() }}
</span>
<template v-else>Never</template>
</template>
<template v-else>
<span class="tag is-danger is-light">Disabled</span>
</template>
</div>
</div>
</div>
<footer class="card-footer">
<div class="card-footer-item" v-if="backend.export.enabled">
<NuxtLink class="button is-danger is-fullwidth"
:to="makeConsoleCommand(`state:export -v -s ${backend.name}`)">
<span class="icon"><i class="fas fa-upload"></i></span>
<span>Run export now</span>
</NuxtLink>
</div>
<div class="card-footer-item" v-if="backend.import.enabled">
<NuxtLink class="button is-primary is-fullwidth"
:to="makeConsoleCommand(`state:import -v -s ${backend.name}`)">
<span class="icon"><i class="fas fa-download"></i></span>
<span>Run import now</span>
</NuxtLink>
</div>
</footer>
<footer class="card-footer">
<div class="card-footer-item">
<div class="field">
Expand Down Expand Up @@ -133,7 +167,7 @@ import 'assets/css/bulma-switch.css'
import moment from 'moment'
import request from '~/utils/request.js'
import BackendAdd from '~/components/BackendAdd.vue'
import {copyText, notification} from '~/utils/index.js'
import {copyText, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js'
import {useStorage} from "@vueuse/core";
import Message from "~/components/Message.vue";
Expand Down
212 changes: 212 additions & 0 deletions frontend/pages/backup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<template>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-sd-card"></i></span>
Backups
</span>
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" @click="queueTask" :disabled="isLoading"
:class="{'is-loading':isLoading, 'is-primary':!queued, 'is-danger':queued}">
<span class="icon"><i class="fas fa-sd-card"></i></span>
<span>{{ !queued ? 'Queue backup' : 'Remove from queue' }}</span>
</button>
</p>
<p class="control">
<button class="button is-info" @click="loadContent" :disabled="isLoading" :class="{'is-loading':isLoading}">
<span class="icon"><i class="fas fa-sync"></i></span>
</button>
</p>
</div>
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains all of your manually generated and automatic backups.
</span>
</div>
</div>

<div class="column is-12" v-if="items.length < 1 || isLoading">
<Message v-if="isLoading" message_class="is-background-info-90 has-text-dark" icon="fas fa-spinner fa-spin"
title="Loading" message="Loading data. Please wait..."/>
<Message v-else title="Warning" message_class="is-background-warning-80 has-text-dark"
icon="fas fa-exclamation-triangle">
No backups found.
</Message>
</div>

<div class="column is-6-tablet" v-for="(item, index) in items" :key="'backup-'+index">
<div class="card">
<header class="card-header">
<p class="card-header-title is-text-overflow pr-1">
<span class="icon"><i class="fas fa-download" :class="{'fa-spin':item?.isDownloading}"></i>&nbsp;</span>
<span>
<NuxtLink @click="downloadFile(item)" v-text="item.filename"/>
</span>
</p>
<span class="card-header-icon">
<NuxtLink @click="deleteFile(item)" class="has-text-danger" v-tooltip="'Delete this backup file.'">
<span class="icon"><i class="fas fa-trash"></i></span>
</NuxtLink>
</span>
</header>
<div class="card-footer-item">
<div class="card-footer-item">
<span class="icon"><i class="fas fa-calendar"></i>&nbsp;</span>
<span class="has-tooltip" v-tooltip="`Last Update: ${moment(item.date).format(TOOLTIP_DATE_FORMAT)}`">
{{ moment(item.date).fromNow() }}
</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-hdd"></i>&nbsp;</span>
<span>{{ humanFileSize(item.size) }}</span>
</div>
<div class="card-footer-item">
<span class="icon"><i class="fas fa-tag"></i>&nbsp;</span>
<span class="is-capitalized">{{ item.type }}</span>
</div>
</div>
</div>
</div>
<div class="column is-12">
<Message message_class="has-background-info-90 has-text-dark" :toggle="show_page_tips"
@toggle="show_page_tips = !show_page_tips" :use-toggle="true" title="Tips" icon="fas fa-info-circle">
<ul>
<li>
Backups that are tagged <code>Automatic</code> are subject to auto deletion after <code>9</code> days from
the date of creation.
</li>
<li>
You can trigger a backup task to run in the background by clicking the
<code><span class="icon"><i class="fas fa-sd-card"></i></span> Queue backup</code> button. on top right.
Those backups will be tagged as <code>Automatic</code>.
</li>
<li>
To generate a manual backup, you need to use the <code>state:backup</code> command from the console.
or by <span class="icon"><i class="fas fa-terminal"></i></span>
<NuxtLink :to="makeConsoleCommand('state:backup -s [backend] --file /config/backup/[file]')"
v-text="'Web Console'"/>
page.
</li>
</ul>
</Message>
</div>
</div>
</template>
<script setup>
import request from '~/utils/request.js'
import moment from 'moment'
import {humanFileSize, makeConsoleCommand, notification, TOOLTIP_DATE_FORMAT} from '~/utils/index.js'
import Message from '~/components/Message.vue'
import {useStorage} from '@vueuse/core'
useHead({title: 'Backups'})
const items = ref([])
const isLoading = ref(false)
const queued = ref(true)
const show_page_tips = useStorage('show_page_tips', true)
const loadContent = async () => {
items.value = []
isLoading.value = true
try {
const response = await request('/system/backup')
items.value = await response.json()
queued.value = await isQueued()
} catch (e) {
notification('error', 'Error', e.message)
} finally {
isLoading.value = false
}
}
const downloadFile = async item => {
if (true === item?.isDownloading) {
return
}
const filename = item.filename
item.isDownloading = true
const response = request(`/system/backup/${filename}`)
if ('showSaveFilePicker' in window) {
response.then(async res => {
item.isDownloading = false
return res.body.pipeTo(await (await showSaveFilePicker({
suggestedName: `${filename}`
})).createWritable())
})
} else {
response.then(res => res.blob()).then(blob => {
const fileURL = URL.createObjectURL(blob)
const fileLink = document.createElement('a')
fileLink.href = fileURL
fileLink.download = `${filename}`
fileLink.click()
item.isDownloading = false
})
}
}
const queueTask = async () => {
const is_queued = await isQueued()
const message = is_queued ? 'Remove backup task from queue?' : 'Queue backup task to run in background?'
if (!confirm(message)) {
return
}
try {
const response = await request(`/tasks/backup/queue`, {method: is_queued ? 'DELETE' : 'POST'})
if (response.ok) {
notification('success', 'Success', `Task backup has been ${is_queued ? 'removed from the queue' : 'queued'}.`)
queued.value = !is_queued
}
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`)
}
}
const deleteFile = async (item) => {
if (!confirm(`Delete backup file '${item.filename}'?`)) {
return
}
try {
const response = await request(`/system/backup/${item.filename}`, {method: 'DELETE'})
if (200 === response.status) {
notification('success', 'Success', `Backup file '${item.filename}' has been deleted.`)
items.value = items.value.filter(i => i.filename !== item.filename)
return
}
let json
try {
json = await response.json()
} catch (e) {
json = {error: {code: response.status, message: response.statusText}}
}
notification('error', 'Error', `API error. ${json.error.code}: ${json.error.message}`)
} catch (e) {
notification('error', 'Error', `Request error. ${e.message}`)
}
}
const isQueued = async () => {
const response = await request('/tasks/backup')
const json = await response.json()
return Boolean(json.queued)
}
onMounted(async () => await loadContent())
</script>
Loading

0 comments on commit daa3e84

Please sign in to comment.