diff --git a/apps/app-frontend/src/components/ui/shared-instances/SharedInstancePublishModal.vue b/apps/app-frontend/src/components/ui/shared-instances/SharedInstancePublishModal.vue index 4319853c03d..bf90a1079ae 100644 --- a/apps/app-frontend/src/components/ui/shared-instances/SharedInstancePublishModal.vue +++ b/apps/app-frontend/src/components/ui/shared-instances/SharedInstancePublishModal.vue @@ -39,9 +39,7 @@ @navigate="scrollFileTreeToTop" /> -
+
{{ formatMessage(messages.storageTitle) }} diff --git a/apps/app-frontend/src/features/settings/syncing/game-settings-modal/boolean-control.vue b/apps/app-frontend/src/features/settings/syncing/game-settings-modal/boolean-control.vue index 7b299577450..50feba1b019 100644 --- a/apps/app-frontend/src/features/settings/syncing/game-settings-modal/boolean-control.vue +++ b/apps/app-frontend/src/features/settings/syncing/game-settings-modal/boolean-control.vue @@ -1,7 +1,7 @@ - - diff --git a/apps/app-frontend/src/pages/hosting/manage/Backups.vue b/apps/app-frontend/src/pages/hosting/manage/Backups.vue index 0ebb7bd800b..2c0b548e88b 100644 --- a/apps/app-frontend/src/pages/hosting/manage/Backups.vue +++ b/apps/app-frontend/src/pages/hosting/manage/Backups.vue @@ -2,6 +2,7 @@ import { injectModrinthClient, injectModrinthServerContext, + serverBackupsQueueQueryOptions, ServersManageBackupsPage, } from '@modrinth/ui' import { useQueryClient } from '@tanstack/vue-query' @@ -12,11 +13,9 @@ const queryClient = useQueryClient() if (worldId.value) { try { - await queryClient.ensureQueryData({ - queryKey: ['backups', 'list', serverId], - queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!), - staleTime: 30_000, - }) + await queryClient.ensureQueryData( + serverBackupsQueueQueryOptions(client, serverId, worldId.value), + ) } catch { // Let mounted layouts' useQuery surface errors; do not fail route setup. } diff --git a/apps/app-frontend/src/providers/setup/file-download.ts b/apps/app-frontend/src/providers/setup/file-download.ts index eb6be894ac1..da4a07d63dc 100644 --- a/apps/app-frontend/src/providers/setup/file-download.ts +++ b/apps/app-frontend/src/providers/setup/file-download.ts @@ -1,5 +1,5 @@ import type { AbstractModrinthClient } from '@modrinth/api-client' -import { provideFileDownload } from '@modrinth/ui' +import { type FileDownload, provideFileDownload } from '@modrinth/ui' import { Channel, invoke } from '@tauri-apps/api/core' import { @@ -11,6 +11,29 @@ interface NativeSaveError { message?: string } +async function authorizeDownloadUrl(client: AbstractModrinthClient, file: FileDownload) { + if (file.type === 'server-backup') { + const { token } = await client.kyros.backups_v1.authorizeDownload( + file.nodeUrlHost, + file.worldId, + file.backupId, + ) + return client.kyros.backups_v1.getDownloadUrl( + file.nodeUrlHost, + file.worldId, + file.backupId, + token, + ) + } + + const { token } = await client.kyros.files_v1.authorizeFileDownload( + file.nodeUrlHost, + file.worldId, + file.path, + ) + return client.kyros.files_v1.getFileDownloadUrl(file.nodeUrlHost, file.worldId, token) +} + export function setupFileDownloadProvider(client: AbstractModrinthClient) { provideFileDownload({ async download(file) { @@ -39,13 +62,8 @@ export function setupFileDownloadProvider(client: AbstractModrinthClient) { }) try { - const { token } = await client.kyros.files_v1.authorizeFileDownload( - file.nodeUrlHost, - file.worldId, - file.path, - ) + const url = await authorizeDownloadUrl(client, file) if (cancelled) return false - const url = client.kyros.files_v1.getFileDownloadUrl(file.nodeUrlHost, file.worldId, token) const savedFilename = await invoke('plugin:files|files_save_external', { saveId, request: { url }, diff --git a/apps/frontend/src/pages/hosting/manage/[id]/backups.vue b/apps/frontend/src/pages/hosting/manage/[id]/backups.vue index 467662653f8..70232e31b36 100644 --- a/apps/frontend/src/pages/hosting/manage/[id]/backups.vue +++ b/apps/frontend/src/pages/hosting/manage/[id]/backups.vue @@ -2,6 +2,7 @@ import { injectModrinthClient, injectModrinthServerContext, + serverBackupsQueueQueryOptions, ServersManageBackupsPage, } from '@modrinth/ui' import { useQueryClient } from '@tanstack/vue-query' @@ -13,11 +14,9 @@ const flags = useFeatureFlags() if (worldId.value) { try { - await queryClient.ensureQueryData({ - queryKey: ['backups', 'list', serverId], - queryFn: () => client.archon.backups_v1.list(serverId, worldId.value!), - staleTime: 30_000, - }) + await queryClient.ensureQueryData( + serverBackupsQueueQueryOptions(client, serverId, worldId.value), + ) } catch { // Let mounted layouts' useQuery surface errors; do not fail route setup. } 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 4e36201f144..2cee50301e7 100644 --- a/packages/api-client/src/modules/archon/backups-queue/v1.ts +++ b/packages/api-client/src/modules/archon/backups-queue/v1.ts @@ -17,6 +17,36 @@ export class ArchonBackupsQueueV1Module extends AbstractModule { ) } + /** GET /v1/servers/:server_id/worlds/:world_id/backups-queue/with-options */ + public async listWithOptions( + serverId: string, + worldId: string, + ): Promise { + return this.client.request( + `/servers/${serverId}/worlds/${worldId}/backups-queue/with-options`, + { api: 'archon', version: 1, method: 'GET' }, + ) + } + + /** PATCH /v1/servers/:server_id/worlds/:world_id/backups-queue/with-options */ + public async patchOptions( + serverId: string, + worldId: string, + request: Archon.BackupsQueue.v1.PatchWorldBackupOptions, + acknowledgeRotation = false, + ): Promise { + return this.client.request( + `/servers/${serverId}/worlds/${worldId}/backups-queue/with-options`, + { + api: 'archon', + version: 1, + method: 'PATCH', + body: request, + params: acknowledgeRotation ? { ar: 'true' } : undefined, + }, + ) + } + /** POST /v1/servers/:server_id/worlds/:world_id/backups-queue */ public async create( serverId: string, diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index 6a0ecaa07dd..edc4205ec75 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -964,6 +964,7 @@ export namespace Archon { status: BackupStatus locked: boolean automated: boolean + rotates_at?: string history: BackupQueueOperation[] } @@ -971,6 +972,29 @@ export namespace Archon { active_operations: ActiveOperation[] backups: BackupQueueBackup[] } + + export type WorldBackupOptions = { + split_automatic: number + split_manual: number + limit: number + interval: number + } + + export type BackupsQueueWithOptionsResponse = BackupsQueueResponse & { + options: WorldBackupOptions + next_auto_backup_at: string | null + } + + export type PatchWorldBackupOptions = { + split_automatic?: number + split_manual?: number + interval?: number + } + + export type PatchWorldBackupOptionsResponse = { + applied: boolean + backup_ids?: string[] + } } } diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 6740b7a674e..93fcbad9ada 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -14,6 +14,7 @@ import { ArchonServersV0Module } from './archon/servers/v0' import { ArchonServersV1Module } from './archon/servers/v1' import { ArchonTransfersInternalModule } from './archon/transfers/internal' import { ISO3166Module } from './iso3166' +import { KyrosBackupsV1Module } from './kyros/backups/v1' import { KyrosContentV1Module } from './kyros/content/v1' import { KyrosFilesV0Module } from './kyros/files/v0' import { KyrosFilesV1Module } from './kyros/files/v1' @@ -97,6 +98,7 @@ export const MODULE_REGISTRY = { mclogs_insights_v1: MclogsInsightsV1Module, mclogs_logs_v1: MclogsLogsV1Module, launchermeta_manifest_v0: LauncherMetaManifestV0Module, + kyros_backups_v1: KyrosBackupsV1Module, kyros_content_v1: KyrosContentV1Module, kyros_files_v0: KyrosFilesV0Module, kyros_files_v1: KyrosFilesV1Module, diff --git a/packages/api-client/src/modules/kyros/backups/v1.ts b/packages/api-client/src/modules/kyros/backups/v1.ts new file mode 100644 index 00000000000..439ffd025d9 --- /dev/null +++ b/packages/api-client/src/modules/kyros/backups/v1.ts @@ -0,0 +1,46 @@ +import { AbstractModule } from '../../../core/abstract-module' +import { getNodeBaseUrl } from '../../../utils/node-url' +import type { Kyros } from '../types' + +export class KyrosBackupsV1Module extends AbstractModule { + public getModuleID(): string { + return 'kyros_backups_v1' + } + + /** + * Authorize a short-lived, single-use backup download. + */ + public async authorizeDownload( + nodeUrlHost: string, + worldId: string, + backupId: string, + format?: Kyros.Backups.v1.BackupDumpFormat, + ): Promise { + return this.client.request( + `/worlds/${worldId}/backups/${backupId}/download/authorize`, + { + api: getNodeBaseUrl(nodeUrlHost), + version: 'v1', + method: 'GET', + params: format ? { format } : undefined, + }, + ) + } + + /** + * Build the browser URL for a previously authorized backup download. + */ + public getDownloadUrl( + nodeUrlHost: string, + worldId: string, + backupId: string, + token: string, + ): string { + const url = new URL( + `/v1/worlds/${worldId}/backups/${backupId}/download`, + getNodeBaseUrl(nodeUrlHost), + ) + url.searchParams.set('token', token) + return url.toString() + } +} diff --git a/packages/api-client/src/modules/kyros/types.ts b/packages/api-client/src/modules/kyros/types.ts index 533a2178073..5cce0079906 100644 --- a/packages/api-client/src/modules/kyros/types.ts +++ b/packages/api-client/src/modules/kyros/types.ts @@ -27,6 +27,16 @@ export namespace Kyros { } } + export namespace Backups { + export namespace v1 { + export type BackupDumpFormat = 'zip' + + export interface BackupDownloadAuthorization { + token: string + } + } + } + export namespace Files { export namespace v1 { export interface FileDownloadAuthorization { diff --git a/packages/assets/generated-icons.ts b/packages/assets/generated-icons.ts index ab13292e761..392dc102040 100644 --- a/packages/assets/generated-icons.ts +++ b/packages/assets/generated-icons.ts @@ -70,6 +70,7 @@ import _BrushCleaningIcon from './icons/brush-cleaning.svg?component' import _BugIcon from './icons/bug.svg?component' import _CalendarIcon from './icons/calendar.svg?component' import _CalendarArrowDownIcon from './icons/calendar-arrow-down.svg?component' +import _CalendarClockIcon from './icons/calendar-clock.svg?component' import _CardIcon from './icons/card.svg?component' import _ChartIcon from './icons/chart.svg?component' import _ChartAreaIcon from './icons/chart-area.svg?component' @@ -551,6 +552,7 @@ export const BrushCleaningIcon = _BrushCleaningIcon export const BugIcon = _BugIcon export const CalendarIcon = _CalendarIcon export const CalendarArrowDownIcon = _CalendarArrowDownIcon +export const CalendarClockIcon = _CalendarClockIcon export const CardIcon = _CardIcon export const ChartIcon = _ChartIcon export const ChartAreaIcon = _ChartAreaIcon diff --git a/packages/assets/icons/calendar-clock.svg b/packages/assets/icons/calendar-clock.svg new file mode 100644 index 00000000000..a23eaff0b60 --- /dev/null +++ b/packages/assets/icons/calendar-clock.svg @@ -0,0 +1,20 @@ + + + + + + + + + diff --git a/packages/ui/src/components/base/SegmentedControl.vue b/packages/ui/src/components/base/SegmentedControl.vue new file mode 100644 index 00000000000..f2d285b707f --- /dev/null +++ b/packages/ui/src/components/base/SegmentedControl.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 65f391109d9..6e44e3f7216 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -126,6 +126,7 @@ export { default as RadialHeader } from './RadialHeader.vue' export { default as RadioButtons } from './RadioButtons.vue' export { default as ReadyTransition } from './ReadyTransition.vue' export { default as ScrollablePanel } from './ScrollablePanel.vue' +export { default as SegmentedControl } from './SegmentedControl.vue' export { default as ServerNotice } from './ServerNotice.vue' export { default as SettingsFormGroup } from './SettingsFormGroup.vue' export { default as SettingsInlineWarning } from './SettingsInlineWarning.vue' diff --git a/packages/ui/src/components/servers/SaveBanner.vue b/packages/ui/src/components/servers/SaveBanner.vue index a13fbbbec4f..6a3359408cb 100644 --- a/packages/ui/src/components/servers/SaveBanner.vue +++ b/packages/ui/src/components/servers/SaveBanner.vue @@ -3,26 +3,27 @@

You have unsaved changes.

-