feat: modrinth hosting - files tab refactor (#4912)

* feat: api-client module for content v0

* feat: delete unused components + modules + setting

* feat: xhr uploading

* feat: fs module -> api-client

* feat: migrate files.vue to use tanstack

* fix: mem leak + other issues

* fix: build

* feat: switch to monaco

* fix: go back to using ace, but improve preloading + theme

* fix: styling + dead attrs

* feat: match figma

* fix: padding

* feat: files-new for ui page structure

* feat: finalize files.vue

* fix: lint

* fix: qa

* fix: dep

* fix: lint

* fix: lockfile merge

* feat: icons on navtab

* fix: surface alternating on table

* fix: hover surface color

---------

Signed-off-by: Calum H. <contact@cal.engineer>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Calum H.
2026-01-06 00:35:51 +00:00
committed by GitHub
parent 61d4a34f0f
commit 099011a177
89 changed files with 5863 additions and 2091 deletions

View File

@@ -0,0 +1,56 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { Archon } from '../types'
export class ArchonContentV0Module extends AbstractModule {
public getModuleID(): string {
return 'archon_content_v0'
}
/** GET /modrinth/v0/servers/:server_id/mods */
public async list(serverId: string): Promise<Archon.Content.v0.Mod[]> {
return this.client.request<Archon.Content.v0.Mod[]>(`/servers/${serverId}/mods`, {
api: 'archon',
version: 'modrinth/v0',
method: 'GET',
})
}
/** POST /modrinth/v0/servers/:server_id/mods */
public async install(
serverId: string,
request: Archon.Content.v0.InstallModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/mods`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
/** POST /modrinth/v0/servers/:server_id/deleteMod */
public async delete(
serverId: string,
request: Archon.Content.v0.DeleteModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/deleteMod`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
/** POST /modrinth/v0/servers/:server_id/mods/update */
public async update(
serverId: string,
request: Archon.Content.v0.UpdateModRequest,
): Promise<void> {
await this.client.request<void>(`/servers/${serverId}/mods/update`, {
api: 'archon',
version: 'modrinth/v0',
method: 'POST',
body: request,
})
}
}

View File

@@ -1,5 +1,6 @@
export * from './backups/v0'
export * from './backups/v1'
export * from './content/v0'
export * from './servers/v0'
export * from './servers/v1'
export * from './types'

View File

@@ -78,4 +78,20 @@ export class ArchonServersV0Module extends AbstractModule {
method: 'GET',
})
}
/**
* Send a power action to a server (Start, Stop, Restart, Kill)
* POST /modrinth/v0/servers/:id/power
*/
public async power(
serverId: string,
action: 'Start' | 'Stop' | 'Restart' | 'Kill',
): Promise<void> {
await this.client.request(`/servers/${serverId}/power`, {
api: 'archon',
method: 'POST',
version: 'modrinth/v0',
body: { action },
})
}
}

View File

@@ -1,4 +1,40 @@
export namespace Archon {
export namespace Content {
export namespace v0 {
export type ContentKind = 'mod' | 'plugin'
export type Mod = {
filename: string
project_id: string | undefined
version_id: string | undefined
name: string | undefined
version_number: string | undefined
icon_url: string | undefined
owner: string | undefined
disabled: boolean
installing: boolean
}
export type InstallModRequest = {
rinth_ids: {
project_id: string
version_id: string
}
install_as: ContentKind
}
export type DeleteModRequest = {
path: string
}
export type UpdateModRequest = {
replace: string
project_id: string
version_id: string
}
}
}
export namespace Servers {
export namespace v0 {
export type ServerGetResponse = {
@@ -274,6 +310,11 @@ export namespace Archon {
started: string
}
export type QueuedFilesystemOp = {
op: FilesystemOpKind
src: string
}
export type WSFilesystemOpsEvent = {
event: 'filesystem-ops'
all: FilesystemOperation[]

View File

@@ -2,6 +2,7 @@ import type { AbstractModrinthClient } from '../core/abstract-client'
import type { AbstractModule } from '../core/abstract-module'
import { ArchonBackupsV0Module } from './archon/backups/v0'
import { ArchonBackupsV1Module } from './archon/backups/v1'
import { ArchonContentV0Module } from './archon/content/v0'
import { ArchonServersV0Module } from './archon/servers/v0'
import { ArchonServersV1Module } from './archon/servers/v1'
import { ISO3166Module } from './iso3166'
@@ -28,6 +29,7 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
export const MODULE_REGISTRY = {
archon_backups_v0: ArchonBackupsV0Module,
archon_backups_v1: ArchonBackupsV1Module,
archon_content_v0: ArchonContentV0Module,
archon_servers_v0: ArchonServersV0Module,
archon_servers_v1: ArchonServersV1Module,
iso3166_data: ISO3166Module,

View File

@@ -1,4 +1,6 @@
import { AbstractModule } from '../../../core/abstract-module'
import type { UploadHandle, UploadProgress } from '../../../types/upload'
import type { Kyros } from '../types'
export class KyrosFilesV0Module extends AbstractModule {
public getModuleID(): string {
@@ -6,47 +8,189 @@ export class KyrosFilesV0Module extends AbstractModule {
}
/**
* Download a file from a server's filesystem
* List directory contents with pagination
*
* @param nodeInstance - Node instance URL (e.g., "node-xyz.modrinth.com/modrinth/v0/fs")
* @param nodeToken - JWT token from getFilesystemAuth
* @param path - File path (e.g., "/server-icon-original.png")
* @returns Promise resolving to file Blob
* @param path - Directory path (e.g., "/")
* @param page - Page number (1-indexed)
* @param pageSize - Items per page
* @returns Directory listing with items and pagination info
*/
public async downloadFile(nodeInstance: string, nodeToken: string, path: string): Promise<Blob> {
return this.client.request<Blob>(`/fs/download`, {
api: `https://${nodeInstance.replace('v0/fs', '')}`,
method: 'GET',
public async listDirectory(
path: string,
page: number = 1,
pageSize: number = 100,
): Promise<Kyros.Files.v0.DirectoryResponse> {
return this.client.request<Kyros.Files.v0.DirectoryResponse>('/fs/list', {
api: '',
version: 'v0',
params: { path },
headers: { Authorization: `Bearer ${nodeToken}` },
method: 'GET',
params: { path, page, page_size: pageSize },
useNodeAuth: true,
})
}
/**
* Upload a file to a server's filesystem
* Create a file or directory
*
* @param path - Path for new item (e.g., "/new-folder")
* @param type - Type of item to create
*/
public async createFileOrFolder(path: string, type: 'file' | 'directory'): Promise<void> {
return this.client.request<void>('/fs/create', {
api: '',
version: 'v0',
method: 'POST',
params: { path, type },
headers: { 'Content-Type': 'application/octet-stream' },
useNodeAuth: true,
})
}
/**
* Download a file from a server's filesystem
*
* @param path - File path (e.g., "/server-icon-original.png")
* @returns Promise resolving to file Blob
*/
public async downloadFile(path: string): Promise<Blob> {
return this.client.request<Blob>('/fs/download', {
api: '',
version: 'v0',
method: 'GET',
params: { path },
useNodeAuth: true,
})
}
/**
* Upload a file to a server's filesystem with progress tracking
*
* @param nodeInstance - Node instance URL
* @param nodeToken - JWT token from getFilesystemAuth
* @param path - Destination path (e.g., "/server-icon.png")
* @param file - File to upload
* @param options - Optional progress callback and feature overrides
* @returns UploadHandle with promise, onProgress, and cancel
*/
public async uploadFile(
nodeInstance: string,
nodeToken: string,
public uploadFile(
path: string,
file: File,
): Promise<void> {
return this.client.request<void>(`/fs/create`, {
api: `https://${nodeInstance.replace('v0/fs', '')}`,
method: 'POST',
file: File | Blob,
options?: {
onProgress?: (progress: UploadProgress) => void
retry?: boolean | number
},
): UploadHandle<void> {
return this.client.upload<void>('/fs/create', {
api: '',
version: 'v0',
file,
params: { path, type: 'file' },
headers: {
Authorization: `Bearer ${nodeToken}`,
'Content-Type': 'application/octet-stream',
},
body: file,
onProgress: options?.onProgress,
retry: options?.retry,
useNodeAuth: true,
})
}
/**
* Update file contents
*
* @param path - File path to update
* @param content - New file content (string or Blob)
*/
public async updateFile(path: string, content: string | Blob): Promise<void> {
const blob = typeof content === 'string' ? new Blob([content]) : content
return this.client.request<void>('/fs/update', {
api: '',
version: 'v0',
method: 'PUT',
params: { path },
body: blob,
headers: { 'Content-Type': 'application/octet-stream' },
useNodeAuth: true,
})
}
/**
* Move a file or folder to a new location
*
* @param sourcePath - Current path
* @param destPath - New path
*/
public async moveFileOrFolder(sourcePath: string, destPath: string): Promise<void> {
return this.client.request<void>('/fs/move', {
api: '',
version: 'v0',
method: 'POST',
body: { source: sourcePath, destination: destPath },
useNodeAuth: true,
})
}
/**
* Rename a file or folder (convenience wrapper around move)
*
* @param path - Current file/folder path
* @param newName - New name (not full path)
*/
public async renameFileOrFolder(path: string, newName: string): Promise<void> {
const newPath = path.split('/').slice(0, -1).join('/') + '/' + newName
return this.moveFileOrFolder(path, newPath)
}
/**
* Delete a file or folder
*
* @param path - Path to delete
* @param recursive - If true, delete directory contents recursively
*/
public async deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
return this.client.request<void>('/fs/delete', {
api: '',
version: 'v0',
method: 'DELETE',
params: { path, recursive },
useNodeAuth: true,
})
}
/**
* Extract an archive file (zip, tar, etc.)
*
* Uses v1 API endpoint.
*
* @param path - Path to archive file
* @param override - If true, overwrite existing files
* @param dry - If true, perform dry run (returns conflicts without extracting)
* @returns Extract result with modpack name and conflicting files
*/
public async extractFile(
path: string,
override: boolean = true,
dry: boolean = false,
): Promise<Kyros.Files.v0.ExtractResult> {
return this.client.request<Kyros.Files.v0.ExtractResult>('/fs/unarchive', {
api: '',
version: 'v1',
method: 'POST',
params: { src: path, trg: '/', override, dry },
useNodeAuth: true,
})
}
/**
* Modify a filesystem operation (dismiss or cancel)
*
* Uses v1 API endpoint.
*
* @param opId - Operation ID (UUID)
* @param action - Action to perform
*/
public async modifyOperation(opId: string, action: 'dismiss' | 'cancel'): Promise<void> {
return this.client.request<void>(`/fs/ops/${action}`, {
api: '',
version: 'v1',
method: 'POST',
params: { id: opId },
useNodeAuth: true,
})
}
}

View File

@@ -1,7 +1,27 @@
export namespace Kyros {
export namespace Files {
export namespace v0 {
// Empty for now
export interface DirectoryItem {
name: string
type: 'file' | 'directory' | 'symlink'
path: string
modified: number
created: number
size?: number
count?: number
target?: string
}
export interface DirectoryResponse {
items: DirectoryItem[]
total: number
current: number
}
export interface ExtractResult {
modpack_name: string | null
conflicting_files: string[]
}
}
}
}