213 lines
7.3 KiB
Vue
213 lines
7.3 KiB
Vue
<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> </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> </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> </span>
|
|
<span>{{ humanFileSize(item.size) }}</span>
|
|
</div>
|
|
<div class="card-footer-item">
|
|
<span class="icon"><i class="fas fa-tag"></i> </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>
|