Skip to content
Merged
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
89 changes: 15 additions & 74 deletions apps/frontend/src/components/ui/servers/BackupItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,14 @@ import {
TrashIcon,
XIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
commonMessages,
injectNotificationManager,
OverflowMenu,
ProgressBar,
} from '@modrinth/ui'
import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrinth/ui'
import type { Backup } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import dayjs from 'dayjs'
import { computed, ref } from 'vue'

import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
import { computed } from 'vue'

const flags = useFeatureFlags()
const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()

const emit = defineEmits<{
(e: 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
Expand All @@ -42,13 +33,11 @@ const props = withDefaults(
preview?: boolean
kyrosUrl?: string
jwt?: string
server?: ModrinthServer
}>(),
{
preview: false,
kyrosUrl: undefined,
jwt: undefined,
server: undefined,
},
)

Expand Down Expand Up @@ -135,48 +124,7 @@ const messages = defineMessages({
id: 'servers.backups.item.retry',
defaultMessage: 'Retry',
},
downloadingBackup: {
id: 'servers.backups.item.downloading-backup',
defaultMessage: 'Downloading backup...',
},
downloading: {
id: 'servers.backups.item.downloading',
defaultMessage: 'Downloading',
},
})

const downloadingState = ref<{ progress: number; state: string } | undefined>(undefined)

const downloading = computed(() => downloadingState.value)

const handleDownload = async () => {
if (!props.server?.backups || downloading.value) {
return
}

downloadingState.value = { progress: 0, state: 'ongoing' }

try {
const download = props.server.backups.downloadBackup(props.backup.id, props.backup.name)

download.onProgress((p) => {
downloadingState.value = { progress: p.progress, state: 'ongoing' }
})

await download.promise

emit('download')
} catch (error) {
console.error('Failed to download backup:', error)
addNotification({
type: 'error',
title: 'Download failed',
text: error instanceof Error ? error.message : 'Failed to download backup',
})
} finally {
downloadingState.value = undefined
}
}
</script>
<template>
<div
Expand Down Expand Up @@ -244,15 +192,6 @@ const handleDownload = async () => {
class="max-w-full"
/>
</div>
<div v-else-if="downloading" class="col-span-2 flex flex-col gap-3 text-blue">
{{ formatMessage(messages.downloadingBackup) }}
<ProgressBar
:progress="downloading.progress >= 0 ? downloading.progress : 0"
color="blue"
:waiting="downloading.progress <= 0"
class="max-w-full"
/>
</div>
<template v-else>
<div class="col-span-2">
{{ dayjs(backup.created_at).format('MMMM D, YYYY [at] h:mm A') }}
Expand Down Expand Up @@ -284,32 +223,34 @@ const handleDownload = async () => {
</button>
</ButtonStyled>
<template v-else>
<ButtonStyled v-show="!downloading">
<button :disabled="!server?.backups" @click="handleDownload">
<ButtonStyled>
<a
:class="{
disabled: !kyrosUrl || !jwt,
}"
:href="`https://${kyrosUrl}/modrinth/v0/backups/${backup.id}/download?auth=${jwt}`"
@click="() => emit('download')"
>
<DownloadIcon />
{{ formatMessage(commonMessages.downloadButton) }}
</button>
</a>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
:options="[
{
id: 'rename',
action: () => emit('rename'),
disabled: !!restoring || !!downloading,
},
{ id: 'rename', action: () => emit('rename') },
{
id: 'restore',
action: () => emit('restore'),
disabled: !!restoring || !!downloading,
disabled: !!restoring,
},
{ id: 'lock', action: () => emit('lock'), disabled: !!restoring || !!downloading },
{ id: 'lock', action: () => emit('lock') },
{ divider: true },
{
id: 'delete',
color: 'red',
action: () => emit('delete'),
disabled: !!restoring || !!downloading,
disabled: !!restoring,
},
]"
>
Expand Down
83 changes: 0 additions & 83 deletions apps/frontend/src/composables/servers/modules/backups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,87 +106,4 @@ export class BackupsModule extends ServerModule {
async getAutoBackup(): Promise<AutoBackupSettings> {
return await useServersFetch(`servers/${this.serverId}/autobackup`)
}

downloadBackup(
backupId: string,
backupName: string,
): {
promise: Promise<void>
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void
cancel: () => void
} {
const progressSubject = new EventTarget()
const abortController = new AbortController()

const downloadPromise = new Promise<void>((resolve, reject) => {
const auth = this.server.general?.node
if (!auth?.instance || !auth?.token) {
reject(new Error('Missing authentication credentials'))
return
}

const xhr = new XMLHttpRequest()

xhr.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const progress = e.loaded / e.total
progressSubject.dispatchEvent(
new CustomEvent('progress', {
detail: { loaded: e.loaded, total: e.total, progress },
}),
)
} else {
// progress = -1 to indicate indeterminate size
progressSubject.dispatchEvent(
new CustomEvent('progress', {
detail: { loaded: e.loaded, total: 0, progress: -1 },
}),
)
}
})

xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const blob = xhr.response
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${backupName}.zip`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
resolve()
} catch (error) {
reject(error)
}
} else {
reject(new Error(`Download failed with status ${xhr.status}`))
}
}

xhr.onerror = () => reject(new Error('Download failed'))
xhr.onabort = () => reject(new Error('Download cancelled'))

xhr.open(
'GET',
`https://${auth.instance}/modrinth/v0/backups/${backupId}/download?auth=${auth.token}`,
)
xhr.responseType = 'blob'
xhr.send()

abortController.signal.addEventListener('abort', () => xhr.abort())
})

return {
promise: downloadPromise,
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => {
progressSubject.addEventListener('progress', ((e: CustomEvent) => {
cb(e.detail)
}) as EventListener)
},
cancel: () => abortController.abort(),
}
}
}
6 changes: 0 additions & 6 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -1934,12 +1934,6 @@
"servers.backups.item.creating-backup": {
"message": "Creating backup..."
},
"servers.backups.item.downloading": {
"message": "Downloading"
},
"servers.backups.item.downloading-backup": {
"message": "Downloading backup..."
},
"servers.backups.item.failed-to-create-backup": {
"message": "Failed to create backup"
},
Expand Down
1 change: 0 additions & 1 deletion apps/frontend/src/pages/servers/manage/[id]/backups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@
v-for="backup in backups"
:key="`backup-${backup.id}`"
:backup="backup"
:server="props.server"
:kyros-url="props.server.general?.node.instance"
:jwt="props.server.general?.node.token"
@download="() => triggerDownloadAnimation()"
Expand Down