diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 7219d12e5..f1bb4f666 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -11,6 +11,7 @@ import { ISO3166Module } from './iso3166' import { KyrosContentV1Module } from './kyros/content/v1' import { KyrosFilesV0Module } from './kyros/files/v0' import { KyrosLogsV1Module } from './kyros/logs/v1' +import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1' import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth' import { LabrinthAffiliateInternalModule } from './labrinth/affiliate/internal' import { LabrinthAuthInternalModule } from './labrinth/auth/internal' @@ -71,6 +72,7 @@ export const MODULE_REGISTRY = { kyros_content_v1: KyrosContentV1Module, kyros_files_v0: KyrosFilesV0Module, kyros_logs_v1: KyrosLogsV1Module, + kyros_upload_sessions_v1: KyrosUploadSessionsV1Module, labrinth_affiliate_internal: LabrinthAffiliateInternalModule, labrinth_auth_internal: LabrinthAuthInternalModule, labrinth_auth_v2: LabrinthAuthV2Module, diff --git a/packages/api-client/src/modules/kyros/content/v1.ts b/packages/api-client/src/modules/kyros/content/v1.ts index 205c2ff69..9f5f4f8b1 100644 --- a/packages/api-client/src/modules/kyros/content/v1.ts +++ b/packages/api-client/src/modules/kyros/content/v1.ts @@ -14,6 +14,7 @@ export class KyrosContentV1Module extends AbstractModule { * @param files - Files to upload as addons * @param options - Optional progress callback * @returns UploadHandle with promise, onProgress, and cancel + * @deprecated Use `kyros.upload_sessions_v1` so cancellation can remove staged addon files before finalize. */ public uploadAddonFile( worldId: string, diff --git a/packages/api-client/src/modules/kyros/files/v0.ts b/packages/api-client/src/modules/kyros/files/v0.ts index fb8f4d19b..693b89a81 100644 --- a/packages/api-client/src/modules/kyros/files/v0.ts +++ b/packages/api-client/src/modules/kyros/files/v0.ts @@ -94,6 +94,7 @@ export class KyrosFilesV0Module extends AbstractModule { * @param file - File to upload * @param options - Optional progress callback and feature overrides * @returns UploadHandle with promise, onProgress, and cancel + * @deprecated Use `kyros.upload_sessions_v1` for bulk uploads so cancellation can remove staged files before finalize. */ public uploadFile( path: string, diff --git a/packages/api-client/src/modules/kyros/types.ts b/packages/api-client/src/modules/kyros/types.ts index 102c72be7..b649ba645 100644 --- a/packages/api-client/src/modules/kyros/types.ts +++ b/packages/api-client/src/modules/kyros/types.ts @@ -1,4 +1,32 @@ export namespace Kyros { + export namespace UploadSessions { + export namespace v1 { + export type Scope = 'content' | 'files' + export type UploadSessionStatus = + | 'active' + | 'uploading' + | 'finalizing' + | 'cancelled' + | 'finalized' + | 'expired' + + export interface UploadSessionResponse { + upload_id: string + status: UploadSessionStatus + created_at: number + updated_at: number + last_upload_at: number | null + expires_at: number + entry_count: number + uploaded_byte_count: number + } + + export interface GetUploadSessionResponse { + session: UploadSessionResponse | null + } + } + } + export namespace Files { export namespace v0 { export interface DirectoryItem { diff --git a/packages/api-client/src/modules/kyros/upload-sessions/v1.ts b/packages/api-client/src/modules/kyros/upload-sessions/v1.ts new file mode 100644 index 000000000..d88d6dacc --- /dev/null +++ b/packages/api-client/src/modules/kyros/upload-sessions/v1.ts @@ -0,0 +1,104 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { UploadHandle, UploadProgress } from '../../../types/upload' +import type { Kyros } from '../types' + +export type UploadSessionFile = { + file: File | Blob + filename: string +} + +export class KyrosUploadSessionsV1Module extends AbstractModule { + public getModuleID(): string { + return 'kyros_upload_sessions_v1' + } + + public async create( + scope: Kyros.UploadSessions.v1.Scope, + worldId: string, + ): Promise { + return this.client.request( + `/worlds/${worldId}/files/upload-session`, + { + api: '', + version: 'v1', + method: 'POST', + useNodeAuth: true, + }, + ) + } + + public async get( + scope: Kyros.UploadSessions.v1.Scope, + worldId: string, + ): Promise { + return this.client.request( + `/worlds/${worldId}/files/upload-session`, + { + api: '', + version: 'v1', + method: 'GET', + useNodeAuth: true, + }, + ) + } + + public uploadFiles( + scope: Kyros.UploadSessions.v1.Scope, + worldId: string, + uploadId: string, + files: UploadSessionFile[], + options?: { + onProgress?: (progress: UploadProgress) => void + retry?: boolean | number + }, + ): UploadHandle { + const formData = new FormData() + for (const { file, filename } of files) { + formData.append('file', file, filename) + } + + return this.client.upload( + `/worlds/${worldId}/files/upload-session/${uploadId}/files`, + { + api: '', + version: 'v1', + formData, + onProgress: options?.onProgress, + retry: options?.retry, + useNodeAuth: true, + }, + ) + } + + public async finalize( + scope: Kyros.UploadSessions.v1.Scope, + worldId: string, + uploadId: string, + ): Promise { + return this.client.request( + `/worlds/${worldId}/files/upload-session/${uploadId}/finalize`, + { + api: '', + version: 'v1', + method: 'POST', + useNodeAuth: true, + }, + ) + } + + public async cancel( + scope: Kyros.UploadSessions.v1.Scope, + worldId: string, + uploadId: string, + ): Promise { + return this.client.request( + `/worlds/${worldId}/files/upload-session/${uploadId}`, + { + api: '', + version: 'v1', + method: 'DELETE', + useNodeAuth: true, + }, + ) + } +} diff --git a/packages/ui/src/layouts/wrapped/hosting/manage/components/ServerManageStats.vue b/packages/ui/src/components/servers/ServerManageStats.vue similarity index 100% rename from packages/ui/src/layouts/wrapped/hosting/manage/components/ServerManageStats.vue rename to packages/ui/src/components/servers/ServerManageStats.vue diff --git a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue index 6d429417c..c157327f9 100644 --- a/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue +++ b/packages/ui/src/components/servers/admonitions/ServerPanelAdmonitions.vue @@ -92,6 +92,7 @@ const filesBusyHeader = computed(() => const dismissedIds = reactive(new Set()) const cancellingIds = reactive(new Set()) +const uploadCancelling = ref(false) const dismissedContentErrorKey = ref(null) const contentErrorKey = computed(() => @@ -327,6 +328,21 @@ async function onBackupRetry(item: BackupAdmonitionEntry) { await invalidate() } +async function onUploadCancel() { + if (uploadCancelling.value) return + const cancel = ctx.cancelUpload.value + if (!cancel) return + + uploadCancelling.value = true + try { + await cancel() + } catch (err) { + console.error('Failed to cancel upload', err) + } finally { + uploadCancelling.value = false + } +} + async function onDismissAll() { const tasks: Promise[] = [] for (const it of stackItems.value) { @@ -375,7 +391,12 @@ function onContentErrorDismiss() { @dismiss="onContentErrorDismiss" @retry="emit('content-retry')" /> - + -