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 { this.data = await useServersFetch(`servers/${this.serverId}/backups`, {}, 'backups') } async create(backupName: string): Promise { 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 { await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, { method: 'POST', body: { name: newName }, }) await this.fetch() } async delete(backupId: string): Promise { await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, { method: 'DELETE', }) await this.fetch() } async restore(backupId: string): Promise { 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 { await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, { method: 'POST', }) await this.fetch() } async unlock(backupId: string): Promise { await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, { method: 'POST', }) await this.fetch() } async retry(backupId: string): Promise { await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, { method: 'POST', }) } async updateAutoBackup(autoBackup: 'enable' | 'disable', interval: number): Promise { await useServersFetch(`servers/${this.serverId}/autobackup`, { method: 'POST', body: { set: autoBackup, interval }, }) } async getAutoBackup(): Promise { return await useServersFetch(`servers/${this.serverId}/autobackup`) } downloadBackup( backupId: string, backupName: string, ): { promise: Promise 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((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(), } } }