Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/components/standalone/backup_and_restore/BackupContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ const retryTimeout = ref<ReturnType<typeof setTimeout>>()

interface Backup {
id: string
name: string
created: BigInteger
filename: string
uploaded_at: string
size: number
mimetype: string
sha256?: string
}

const loading = computed((): boolean => {
Expand Down Expand Up @@ -183,8 +184,11 @@ async function getBackups() {
const res = await ubusCall('ns.backup', 'registered-list-backups')
if (res?.data?.values?.backups?.length) {
listBackups.value = res.data.values.backups
// sort by created date in unix timestamp
listBackups.value.sort((a, b) => Number(b.created) - Number(a.created))
// newest first — the server already sorts, but keep the guard
// so a stray ordering change upstream doesn't break the UI.
listBackups.value.sort(
(a, b) => new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime()
)
}
} catch (exception: unknown) {
if (exception instanceof Error) {
Expand Down Expand Up @@ -233,6 +237,9 @@ function successRunBackup() {

function successSetPassphrase() {
showPassphraseDrawer.value = false
// Refresh the store so the "Passphrase not configured" warning and the
// "Run backup" disabled state update without a full page reload.
backups.loadData()
}

function getMimetypeDescription(mimetype: string) {
Expand All @@ -258,7 +265,7 @@ function getDropdownItems(item: Backup) {
action: () => {
openDeleteBackup(
item.id,
formatDateLoc(new Date(Number(item.created) * 1000), 'PPpp') +
formatDateLoc(new Date(item.uploaded_at), 'PPpp') +
' (' +
byteFormat1024(item.size) +
')'
Expand Down Expand Up @@ -509,11 +516,11 @@ function successDeleteBackup() {
</NeTableHeadCell>
</NeTableHead>
<NeTableBody>
<NeTableRow v-for="item in listBackups" :key="item.name">
<NeTableRow v-for="item in listBackups" :key="item.id">
<NeTableCell :data-label="t('standalone.backup_and_restore.backup.date')">
<div>
<FontAwesomeIcon :icon="faClock" class="mr-2" />
{{ formatDateLoc(new Date(Number(item.created) * 1000), 'PPpp') }}
{{ formatDateLoc(new Date(item.uploaded_at), 'PPpp') }}
</div>
</NeTableCell>
<NeTableCell :data-label="t('standalone.backup_and_restore.backup.mimetype')">
Expand All @@ -533,7 +540,7 @@ function successDeleteBackup() {
<div class="-ml-2.5 flex items-center gap-2 xl:ml-0 xl:justify-end">
<NeButton
:kind="'tertiary'"
@click="openDownloadEnterprise(item.id, item.mimetype, item.created.toString())"
@click="openDownloadEnterprise(item.id, item.mimetype, item.uploaded_at)"
>
<template #prefix>
<FontAwesomeIcon :icon="faArrowCircleDown" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async function deleteBackup() {
}

const res = await ubusCall('ns.backup', methodCall, payload)
if (res?.data?.result === 'success') {
if (res?.data?.message === 'success') {
emit('close')
backups.loadData()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const errorDownloadBackup = ref({ ...objNotification })

function getBackupName() {
if (props.selectedBackupTime) {
return formatDateLoc(new Date(Number(props.selectedBackupTime) * 1000), 'PPpp')
return formatDateLoc(new Date(props.selectedBackupTime), 'PPpp')
} else {
return props.unitName
}
Expand Down Expand Up @@ -104,7 +104,12 @@ async function downloadBackup() {
const link = document.createElement('a')
link.href = fileURL
if (props.selectedBackupTime && !props.unencrypted) {
link.download = 'backup-' + props.unitName + '-' + props.selectedBackupTime + extension
// selectedBackupTime is an RFC3339 string from the my API.
// Reduce it to an epoch-second integer so the downloaded file
// name does not contain the ":" characters that some file
// systems and shells refuse.
const epoch = Math.floor(new Date(props.selectedBackupTime).getTime() / 1000)
link.download = 'backup-' + props.unitName + '-' + epoch + extension
} else {
link.download = 'backup-' + props.unitName + '-' + Date.now().toString() + extension
}
Expand Down
36 changes: 32 additions & 4 deletions src/components/standalone/backup_and_restore/RestoreContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-->

<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { ubusCall, ValidationError } from '@/lib/standalone/ubus'
import {
Expand Down Expand Up @@ -83,6 +83,20 @@ const errorRestore = ref({
backup: ''
})

// The passphrase input flips from "Optional" to required whenever the
// selected backup is encrypted. We detect that from the id suffix: the
// ingest pipeline writes .gpg only when the appliance encrypted the
// payload with the configured passphrase.
const isPassphraseRequired = computed(() => {
if (typeRestore.value === 'from_backup') {
return formRestore.value.backup.toLowerCase().endsWith('.gpg')
}
if (typeRestore.value === 'upload_file' && formRestore.value.file) {
return (formRestore.value.file as File).name.toLowerCase().endsWith('.gpg')
}
return false
})

onMounted(() => {
getSubscription()
})
Expand Down Expand Up @@ -110,11 +124,14 @@ async function getBackups() {
const res = await ubusCall('ns.backup', 'registered-list-backups')
if (res?.data?.values?.backups?.length) {
listBackups.value = res.data.values.backups
.sort((a: any, b: any) => Number(b.created) - Number(a.created))
.sort(
(a: any, b: any) =>
new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime()
)
.map((item: any) => ({
id: item.id,
label:
formatDateLoc(new Date(Number(item.created) * 1000), 'PPpp') +
formatDateLoc(new Date(item.uploaded_at), 'PPpp') +
' (' +
byteFormat1024(item.size) +
')'
Expand Down Expand Up @@ -152,6 +169,17 @@ function validateRestore(): boolean {
}
}

if (isPassphraseRequired.value) {
const { valid, errMessage } = validateRequired(formRestore.value.passphrase)
if (!valid) {
errorRestore.value.passphrase = t(errMessage as string)
isValidationOk = false
if (!isFocusInput) {
focusElement(passphraseRef)
}
}
}

return isValidationOk
}

Expand Down Expand Up @@ -355,7 +383,7 @@ function setRestoreTimer() {
:invalid-message="errorRestore.passphrase"
:label="t('standalone.backup_and_restore.restore.passphrase')"
is-password
optional
:optional="!isPassphraseRequired"
:optional-label="t('common.optional')"
>
<template #tooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ async function runBackup() {
:secondary-label="t('common.close')"
secondary-button-kind="tertiary"
@close="$emit('close')"
@secondary-click="$emit('close')"
@primary-click="runBackup()"
>
<div>
Expand Down
11 changes: 7 additions & 4 deletions src/components/standalone/dashboard/BackupStatusCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import type { AxiosResponse } from 'axios'
import { useBackupsStore } from '@/stores/standalone/backups.ts'

type BackupData = {
created: number
uploaded_at: string
id: string
mimetype: string
name: string
filename: string
size: number
sha256?: string
}

type BackupResponse = AxiosResponse<{
Expand All @@ -45,9 +46,11 @@ watchEffect(() => {
latestBackupLoading.value = true
ubusCall('ns.backup', 'registered-list-backups')
.then((response: BackupResponse) => {
const backup = response.data.values.backups.sort((a, b) => b.created - a.created).shift()
const backup = response.data.values.backups
.sort((a, b) => new Date(b.uploaded_at).getTime() - new Date(a.uploaded_at).getTime())
.shift()
if (backup != undefined) {
lastBackup.value = formatDateLoc(new Date(backup.created * 1000), 'PPpp')
lastBackup.value = formatDateLoc(new Date(backup.uploaded_at), 'PPpp')
} else {
lastBackup.value = undefined
}
Expand Down
Loading