diff --git a/packages/api-client/src/modules/archon/backups-queue/v1.ts b/packages/api-client/src/modules/archon/backups-queue/v1.ts index 99d5d8531e..4e36201f14 100644 --- a/packages/api-client/src/modules/archon/backups-queue/v1.ts +++ b/packages/api-client/src/modules/archon/backups-queue/v1.ts @@ -37,6 +37,14 @@ export class ArchonBackupsQueueV1Module extends AbstractModule { ) } + /** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/create/:operation_id/cancel */ + public async cancelCreate(serverId: string, worldId: string, operationId: number): Promise { + await this.client.request( + `/servers/${serverId}/worlds/${worldId}/backups-queue/history/create/${operationId}/cancel`, + { api: 'archon', version: 1, method: 'POST' }, + ) + } + /** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/restore/:operation_id/ack */ public async ackRestore(serverId: string, worldId: string, operationId: number): Promise { await this.client.request( @@ -45,6 +53,18 @@ export class ArchonBackupsQueueV1Module extends AbstractModule { ) } + /** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/restore/:operation_id/cancel */ + public async cancelRestore( + serverId: string, + worldId: string, + operationId: number, + ): Promise { + await this.client.request( + `/servers/${serverId}/worlds/${worldId}/backups-queue/history/restore/${operationId}/cancel`, + { api: 'archon', version: 1, method: 'POST' }, + ) + } + /** DELETE /v1/servers/:server_id/worlds/:world_id/backups-queue/:backup_id */ public async delete(serverId: string, worldId: string, backupId: string): Promise { await this.client.request( diff --git a/packages/api-client/src/modules/archon/backups/v1.ts b/packages/api-client/src/modules/archon/backups/v1.ts index 86fadfdf73..589ee72b9d 100644 --- a/packages/api-client/src/modules/archon/backups/v1.ts +++ b/packages/api-client/src/modules/archon/backups/v1.ts @@ -66,7 +66,8 @@ export class ArchonBackupsV1Module extends AbstractModule { } /** - * @deprecated Use `client.archon.backups_queue_v1.delete` instead. + * @deprecated Use `client.archon.backups_queue_v1.delete` for backup deletion, or + * `client.archon.backups_queue_v1.cancelCreate` / `cancelRestore` for active operations. */ /** DELETE /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */ public async delete(serverId: string, worldId: string, backupId: string): Promise { diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue index c157327f99..c932904f96 100644 --- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue +++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue @@ -314,7 +314,21 @@ async function onBackupCancel(item: BackupAdmonitionEntry) { if (cancellingIds.has(item.key)) return cancellingIds.add(item.key) try { - await client.archon.backups_v1.delete(ctx.serverId, ctx.worldId.value!, item.backupId) + if (item.operationId == null) { + await client.archon.backups_v1.delete(ctx.serverId, ctx.worldId.value!, item.backupId) + } else if (item.type === 'create') { + await client.archon.backups_queue_v1.cancelCreate( + ctx.serverId, + ctx.worldId.value!, + item.operationId, + ) + } else { + await client.archon.backups_queue_v1.cancelRestore( + ctx.serverId, + ctx.worldId.value!, + item.operationId, + ) + } await invalidate() } catch (err) { cancellingIds.delete(item.key) diff --git a/packages/ui/src/layouts/shared/content-tab/composables/use-inline-backup.ts b/packages/ui/src/layouts/shared/content-tab/composables/use-inline-backup.ts index 7123f91d38..552a5a61ef 100644 --- a/packages/ui/src/layouts/shared/content-tab/composables/use-inline-backup.ts +++ b/packages/ui/src/layouts/shared/content-tab/composables/use-inline-backup.ts @@ -69,6 +69,7 @@ export function useInlineBackup(backupName: string | (() => string)) { ) const createdBackupId = ref(null) + const createdOperationId = ref(null) const pendingCreate = ref(false) const backupFailed = ref(false) const backupComplete = ref(false) @@ -102,6 +103,16 @@ export function useInlineBackup(backupName: string | (() => string)) { { immediate: true }, ) + watch( + myActiveOp, + (op) => { + if (op?.operation_id != null) { + createdOperationId.value = op.operation_id + } + }, + { immediate: true }, + ) + async function startBackup() { if (!worldId.value) return @@ -112,6 +123,7 @@ export function useInlineBackup(backupName: string | (() => string)) { backupCancelled.value = false isCancelling.value = false createdBackupId.value = null + createdOperationId.value = null pendingCreate.value = true try { @@ -137,7 +149,18 @@ export function useInlineBackup(backupName: string | (() => string)) { isCancelling.value = true try { - await client.archon.backups_v1.delete(serverId, worldId.value, createdBackupId.value) + let operationId = createdOperationId.value ?? myActiveOp.value?.operation_id ?? null + if (operationId == null) { + const queue = await client.archon.backups_queue_v1.list(serverId, worldId.value) + const operation = queue.active_operations.find( + (op) => op.backup_id === createdBackupId.value && op.operation_type === 'create', + ) + operationId = operation?.operation_id ?? null + } + if (operationId == null) { + throw new Error('Could not find the backup creation operation to cancel.') + } + await client.archon.backups_queue_v1.cancelCreate(serverId, worldId.value, operationId) backupCancelled.value = true isCancelling.value = false await invalidate() diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue b/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue index 74e044186a..d59b25e5f9 100644 --- a/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue +++ b/packages/ui/src/layouts/wrapped/hosting/manage/backups.vue @@ -347,10 +347,11 @@ const serverId = route.params.id as string defineEmits(['onDownload']) -const { backups, invalidate, hasActiveCreate, hasActiveRestore, query } = useServerBackupsQueue( - computed(() => serverId), - worldId, -) +const { backups, invalidate, activeOperationByBackupId, hasActiveCreate, hasActiveRestore, query } = + useServerBackupsQueue( + computed(() => serverId), + worldId, + ) const error = computed(() => { const err = query.error.value @@ -386,17 +387,37 @@ const deleteQueueMutation = useMutation({ }, }) -/** In-progress / incomplete backups: legacy cancel + delete path. */ -const deleteLegacyMutation = useMutation({ - mutationFn: (backupId: string) => - client.archon.backups_v1.delete(serverId, worldId.value!, backupId), +const cancelOperationMutation = useMutation({ + mutationFn: async (backup: Archon.BackupsQueue.v1.BackupQueueBackup) => { + const activeOperation = activeOperationByBackupId.value.get(backup.id) + const historyOperation = backup.history.find( + (op) => (op.state === 'pending' || op.state === 'ongoing') && op.operation_id != null, + ) + const operation = activeOperation?.operation_id != null ? activeOperation : historyOperation + + if (operation?.operation_id == null) { + await client.archon.backups_v1.delete(serverId, worldId.value!, backup.id) + } else if (operation.operation_type === 'create') { + await client.archon.backups_queue_v1.cancelCreate( + serverId, + worldId.value!, + operation.operation_id, + ) + } else { + await client.archon.backups_queue_v1.cancelRestore( + serverId, + worldId.value!, + operation.operation_id, + ) + } + }, onSuccess: async () => { await invalidate() await queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] }) }, }) -/** Bulk delete via queue API — handles both completed and in-progress backups (cancels the latter). */ +/** Bulk delete selected backups via queue API. */ const deleteManyMutation = useMutation({ mutationFn: (backupIds: string[]) => client.archon.backups_queue_v1.deleteMany(serverId, worldId.value!, backupIds), @@ -557,18 +578,20 @@ function deleteBackup(backup?: Archon.BackupsQueue.v1.BackupQueueBackup) { return } - const mutation = useQueueDeleteFor(backup) ? deleteQueueMutation : deleteLegacyMutation - - mutation.mutate(backup.id, { - onError: (err) => { - const message = err instanceof Error ? err.message : String(err) - addNotification({ - type: 'error', - title: 'Error deleting backup', - text: message, - }) - }, - }) + const onError = (err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + addNotification({ + type: 'error', + title: 'Error deleting backup', + text: message, + }) + } + + if (useQueueDeleteFor(backup)) { + deleteQueueMutation.mutate(backup.id, { onError }) + } else { + cancelOperationMutation.mutate(backup, { onError }) + } }