Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@
@navigate="scrollFileTreeToTop"
/>
</div>
<div
class="flex flex-col gap-2 border-0 border-b border-solid border-surface-5 px-6 pb-4 pt-6"
>
<div class="flex flex-col gap-2 border-0 border-b border-solid border-surface-5 px-6 py-4">
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<span class="font-semibold text-contrast">
{{ formatMessage(messages.storageTitle) }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { Button } from '@modrinth/ui'
import { SegmentedControl } from '@modrinth/ui'

defineProps<{
const props = defineProps<{
modelValue?: boolean
disabled?: boolean
label: string
Expand All @@ -16,47 +16,14 @@ const emit = defineEmits<{
</script>

<template>
<div
<SegmentedControl
v-tooltip="modelValue === undefined ? placeholder : undefined"
role="group"
:model-value="modelValue"
:items="[true, false]"
:format-label="(value) => (value ? props.onLabel : props.offLabel)"
:neutral-items="[false]"
:aria-label="label"
class="inline-flex h-9 items-center gap-0.5 rounded-xl border border-solid border-surface-5 p-px shadow-[0_1px_3px_rgba(0,0,0,0.15)]"
:class="{ 'opacity-50': disabled }"
>
<Button
type="quiet"
size="sm"
:interaction="modelValue === true ? 'none' : 'surface'"
class="boolean-control-on !rounded-[10px] !px-3 !font-medium disabled:!opacity-100"
:class="modelValue === true ? '!text-green' : '!text-contrast'"
:aria-pressed="modelValue === true"
:disabled="disabled"
@click="emit('update:model-value', true)"
>
{{ onLabel }}
</Button>
<Button
type="quiet"
size="sm"
:interaction="modelValue === false ? 'none' : 'surface'"
class="boolean-control-off !rounded-[10px] !px-3 !font-medium !text-contrast disabled:!opacity-100"
:aria-pressed="modelValue === false"
:disabled="disabled"
@click="emit('update:model-value', false)"
>
{{ offLabel }}
</Button>
</div>
:disabled="disabled"
@update:model-value="(value) => value !== undefined && emit('update:model-value', value)"
/>
</template>

<style scoped>
.boolean-control-on[aria-pressed='true'] {
background-color: color-mix(in srgb, var(--color-green) 30%, var(--surface-3));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-green) 60%, transparent);
}

.boolean-control-off[aria-pressed='true'] {
background-color: var(--surface-4);
box-shadow: inset 0 0 0 1px var(--surface-5);
}
</style>
9 changes: 4 additions & 5 deletions apps/app-frontend/src/pages/hosting/manage/Backups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
injectModrinthClient,
injectModrinthServerContext,
serverBackupsQueueQueryOptions,
ServersManageBackupsPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
Expand All @@ -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.
}
Expand Down
32 changes: 25 additions & 7 deletions apps/app-frontend/src/providers/setup/file-download.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string>('plugin:files|files_save_external', {
saveId,
request: { url },
Expand Down
9 changes: 4 additions & 5 deletions apps/frontend/src/pages/hosting/manage/[id]/backups.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
injectModrinthClient,
injectModrinthServerContext,
serverBackupsQueueQueryOptions,
ServersManageBackupsPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
Expand All @@ -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.
}
Expand Down
30 changes: 30 additions & 0 deletions packages/api-client/src/modules/archon/backups-queue/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Archon.BackupsQueue.v1.BackupsQueueWithOptionsResponse> {
return this.client.request<Archon.BackupsQueue.v1.BackupsQueueWithOptionsResponse>(
`/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<Archon.BackupsQueue.v1.PatchWorldBackupOptionsResponse> {
return this.client.request<Archon.BackupsQueue.v1.PatchWorldBackupOptionsResponse>(
`/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,
Expand Down
24 changes: 24 additions & 0 deletions packages/api-client/src/modules/archon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,13 +964,37 @@ export namespace Archon {
status: BackupStatus
locked: boolean
automated: boolean
rotates_at?: string
history: BackupQueueOperation[]
}

export type BackupsQueueResponse = {
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[]
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions packages/api-client/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions packages/api-client/src/modules/kyros/backups/v1.ts
Original file line number Diff line number Diff line change
@@ -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<Kyros.Backups.v1.BackupDownloadAuthorization> {
return this.client.request<Kyros.Backups.v1.BackupDownloadAuthorization>(
`/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()
}
}
10 changes: 10 additions & 0 deletions packages/api-client/src/modules/kyros/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/assets/generated-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions packages/assets/icons/calendar-clock.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading