diff --git a/packages/api-client/src/modules/archon/backup-repositories/internal.ts b/packages/api-client/src/modules/archon/backup-repositories/internal.ts new file mode 100644 index 00000000000..e92a1efa7ac --- /dev/null +++ b/packages/api-client/src/modules/archon/backup-repositories/internal.ts @@ -0,0 +1,62 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { Archon } from '../types' + +export class ArchonBackupRepositoriesInternalModule extends AbstractModule { + public getModuleID(): string { + return 'archon_backup_repositories_internal' + } + + /** + * List the latest reparation runs of a backup repository with their operations, newest first. + * GET /_internal/backups/repositories/:id/reparation + */ + public async getReparations( + repositoryId: number, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation`, + { + api: 'archon', + version: 'internal', + method: 'GET', + }, + ) + } + + /** + * Start a reparation run of a backup repository by checking it. + * POST /_internal/backups/repositories/:id/reparation + */ + public async startReparation( + repositoryId: number, + options?: Archon.BackupRepositories.Internal.StartReparationQuery, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation`, + { + api: 'archon', + version: 'internal', + method: 'POST', + params: options?.read ? { read: true } : undefined, + }, + ) + } + + /** + * Start an operation in the backup repository's latest reparation run. + * POST /_internal/backups/repositories/:id/reparation/:operation + */ + public async startOperation( + repositoryId: number, + operationType: Archon.BackupRepositories.Internal.RunnableOperationType, + ): Promise { + return this.client.request( + `/backups/repositories/${repositoryId}/reparation/${operationType}`, + { + api: 'archon', + version: 'internal', + method: 'POST', + }, + ) + } +} diff --git a/packages/api-client/src/modules/archon/index.ts b/packages/api-client/src/modules/archon/index.ts index 335bd234345..44a71626377 100644 --- a/packages/api-client/src/modules/archon/index.ts +++ b/packages/api-client/src/modules/archon/index.ts @@ -1,4 +1,5 @@ export * from './actions/v1' +export * from './backup-repositories/internal' export * from './backups/v1' export * from './backups-queue/v1' export * from './content/v1' diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts index edc4205ec75..7492705f50a 100644 --- a/packages/api-client/src/modules/archon/types.ts +++ b/packages/api-client/src/modules/archon/types.ts @@ -758,6 +758,8 @@ export namespace Archon { tags: string[] location: ServerLocation worlds: WorldFull[] + /** IDs of the server's backup repositories. Only returned to support agents. */ + repository_ids?: number[] } export type SftpCredentials = { @@ -998,6 +1000,120 @@ export namespace Archon { } } + export namespace BackupRepositories { + export namespace Internal { + export type OperationType = + | 'check' + | 'check-read-data' + | 'repair-index' + | 'repair-packs' + | 'repair-snapshots' + | 'prune' + | 'restore-hidden-versions' + + /** Operations started in an existing run. Checks start a new run instead. */ + export type RunnableOperationType = Exclude + + /** + * `completed` when the operation ran, regardless of what it found, or `failed` when it + * couldn't run. + */ + export type OperationStatus = 'pending' | 'ongoing' | 'completed' | 'failed' + + export type StartReparationQuery = { + /** Whether the run's check reads all pack data, which is much slower. */ + read?: boolean + } + + export type RepairCheckOutput = { + type: 'check' + exit_code: number | null + num_errors: number + broken_packs: string[] + suggest_repair_index: boolean + suggest_prune: boolean + /** The first reported errors, capped to keep the result small. */ + errors: string[] + /** Whether more errors were reported than included in `errors`. */ + errors_truncated: boolean + } + + export type RepairSnapshot = { + id: string + time: string + original?: string + tags: string[] + } + + export type RepairOutput = + | RepairCheckOutput + | { + type: 'snapshots' + snapshots: RepairSnapshot[] + } + | { + type: 'command' + exit_code: number | null + output_tail: string + } + + /** Results of a completed `repair-snapshots` operation. */ + export type RepairSnapshotsResults = { + repair: RepairOutput + remapped_backups: { + backup_id: string + original_snapshot_id: string + snapshot_id: string + }[] + missing_snapshot_backups: string[] + } + + /** Results of a `restore-hidden-versions` operation. Only the destination is set while it runs. */ + export type RestoreHiddenVersionsResults = { + destination_repository_id: number + packs?: number + hidden_packs?: number + snapshots?: number + hidden_snapshots?: number + bytes?: number + copied_objects?: number + skipped_objects?: number + missing_objects?: string[] + repair_index?: RepairOutput + imported_backups?: number + matched_backups?: number + } + + export type ReparationOperation = { + id: number + operation_type: OperationType + status: OperationStatus + params: Record + /** + * A {@link RepairOutput} for checks, `repair-index`, `repair-packs` and `prune`, a + * {@link RepairSnapshotsResults} for `repair-snapshots`, and a + * {@link RestoreHiddenVersionsResults} for `restore-hidden-versions`. + */ + results: RepairOutput | RepairSnapshotsResults | RestoreHiddenVersionsResults | null + error: string | null + node_id: string | null + created_by: string | null + created_at: string + started_at: string | null + completed_at: string | null + } + + export type ReparationRun = { + id: number + repository_id: number + server_id: string | null + created_by: string | null + created_at: string + operations: ReparationOperation[] + } + } + } + export namespace Sync { export namespace v1 { export type SyncCategory = 'backup' | 'users' | 'server' | 'protocol' | 'world' diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 2463d7d1d7b..b0699c3280f 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -1,6 +1,7 @@ import type { AbstractModrinthClient } from '../core/abstract-client' import type { AbstractModule } from '../core/abstract-module' import { ArchonActionsV1Module } from './archon/actions/v1' +import { ArchonBackupRepositoriesInternalModule } from './archon/backup-repositories/internal' import { ArchonBackupsV1Module } from './archon/backups/v1' import { ArchonBackupsQueueV1Module } from './archon/backups-queue/v1' import { ArchonContentV1Module } from './archon/content/v1' @@ -81,6 +82,7 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule */ export const MODULE_REGISTRY = { archon_actions_v1: ArchonActionsV1Module, + archon_backup_repositories_internal: ArchonBackupRepositoriesInternalModule, archon_backups_queue_v1: ArchonBackupsQueueV1Module, archon_backups_v1: ArchonBackupsV1Module, archon_content_v1: ArchonContentV1Module, diff --git a/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue new file mode 100644 index 00000000000..41e0e701641 --- /dev/null +++ b/packages/ui/src/layouts/shared/server-settings/components/backup-repository-reparation.vue @@ -0,0 +1,507 @@ + + + diff --git a/packages/ui/src/layouts/shared/server-settings/pages/support.vue b/packages/ui/src/layouts/shared/server-settings/pages/support.vue index 68e686c2bd4..5f3dc4d08bd 100644 --- a/packages/ui/src/layouts/shared/server-settings/pages/support.vue +++ b/packages/ui/src/layouts/shared/server-settings/pages/support.vue @@ -70,6 +70,8 @@ + + @@ -84,6 +86,7 @@ import LockServerModal from '#ui/components/servers/LockServerModal.vue' import { defineMessages, useVIntl } from '#ui/composables/i18n' import { useModrinthServersConsole } from '#ui/composables/server-console' import { useServerPermissions } from '#ui/composables/server-permissions' +import BackupRepositoryReparation from '#ui/layouts/shared/server-settings/components/backup-repository-reparation.vue' import { injectServerSettings } from '#ui/layouts/shared/server-settings/providers/server-settings' import { injectModrinthClient,