1
0
Files
AstralRinth/apps/frontend/src/composables/servers/modules/backups.ts
Calum H. c74460fffa fix: firefox backup download issues (#4679)
* fix: firefox backup download issues

* fix: lint

* fix: hide download button when downloading & disable overflow menu options
2025-10-30 21:20:08 +00:00

193 lines
5.1 KiB
TypeScript

import type { AutoBackupSettings, Backup } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class BackupsModule extends ServerModule {
data: Backup[] = []
async fetch(): Promise<void> {
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, 'backups')
}
async create(backupName: string): Promise<string> {
const tempId = `temp-${Date.now()}-${Math.random().toString(36).substring(7)}`
const tempBackup: Backup = {
id: tempId,
name: backupName,
created_at: new Date().toISOString(),
locked: false,
automated: false,
interrupted: false,
ongoing: true,
task: { create: { progress: 0, state: 'ongoing' } },
}
this.data.push(tempBackup)
try {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: 'POST',
body: { name: backupName },
})
const backup = this.data.find((b) => b.id === tempId)
if (backup) {
backup.id = response.id
}
return response.id
} catch (error) {
this.data = this.data.filter((b) => b.id !== tempId)
throw error
}
}
async rename(backupId: string, newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
method: 'POST',
body: { name: newName },
})
await this.fetch()
}
async delete(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: 'DELETE',
})
await this.fetch()
}
async restore(backupId: string): Promise<void> {
const backup = this.data.find((b) => b.id === backupId)
if (backup) {
if (!backup.task) backup.task = {}
backup.task.restore = { progress: 0, state: 'ongoing' }
}
try {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: 'POST',
})
} catch (error) {
if (backup?.task?.restore) {
delete backup.task.restore
}
throw error
}
}
async lock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: 'POST',
})
await this.fetch()
}
async unlock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: 'POST',
})
await this.fetch()
}
async retry(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
method: 'POST',
})
}
async updateAutoBackup(autoBackup: 'enable' | 'disable', interval: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/autobackup`, {
method: 'POST',
body: { set: autoBackup, interval },
})
}
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(),
}
}
}