mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 05:25:58 +00:00
fix: ditch files v0
This commit is contained in:
@@ -7,15 +7,17 @@ import {
|
|||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { worldId } = injectModrinthServerContext()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryClient.ensureQueryData({
|
if (worldId.value) {
|
||||||
queryKey: ['files', serverId, '/'],
|
await queryClient.ensureQueryData({
|
||||||
queryFn: () => client.kyros.files_v0.listDirectory('/', 1, 2000),
|
queryKey: ['files', 'v1', worldId.value, '/'],
|
||||||
staleTime: 30_000,
|
queryFn: () => client.kyros.files_v1.listDescendants(worldId.value!, '/', 1, 200),
|
||||||
})
|
staleTime: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
// Let mounted layouts' useQuery surface errors; do not fail route setup.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ client.archon.servers_v1
|
|||||||
client.archon.backups_queue_v1
|
client.archon.backups_queue_v1
|
||||||
client.archon.backups_v1
|
client.archon.backups_v1
|
||||||
client.archon.content_v0
|
client.archon.content_v0
|
||||||
client.kyros.files_v0
|
client.kyros.files_v1
|
||||||
|
client.kyros.upload_sessions_v1
|
||||||
client.iso3166.data
|
client.iso3166.data
|
||||||
... etc.
|
... etc.
|
||||||
```
|
```
|
||||||
@@ -140,7 +141,8 @@ Uploads go through the feature chain (auth, retry, etc.). Features detect upload
|
|||||||
### Usage Example (server file upload)
|
### Usage Example (server file upload)
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const uploader = client.kyros.files_v0.uploadFile(path, file, {
|
await client.kyros.files_v1.ensureFile(worldId, path)
|
||||||
|
const uploader = client.kyros.files_v1.uploadFile(worldId, path, file, {
|
||||||
onProgress: ({ progress }) => {
|
onProgress: ({ progress }) => {
|
||||||
uploadProgress.value = Math.round(progress * 100)
|
uploadProgress.value = Math.round(progress * 100)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ Built-in features include authentication, node auth, retries, circuit breaking,
|
|||||||
Upload endpoints return an `UploadHandle<T>` with progress and cancellation support:
|
Upload endpoints return an `UploadHandle<T>` with progress and cancellation support:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const upload = client.kyros.files_v0.uploadFile(path, file)
|
await client.kyros.files_v1.ensureFile(worldId, path)
|
||||||
|
const upload = client.kyros.files_v1.uploadFile(worldId, path, file)
|
||||||
|
|
||||||
upload.onProgress(({ progress }) => {
|
upload.onProgress(({ progress }) => {
|
||||||
console.log(Math.round(progress * 100))
|
console.log(Math.round(progress * 100))
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { ArchonServersV1Module } from './archon/servers/v1'
|
|||||||
import { ArchonTransfersInternalModule } from './archon/transfers/internal'
|
import { ArchonTransfersInternalModule } from './archon/transfers/internal'
|
||||||
import { ISO3166Module } from './iso3166'
|
import { ISO3166Module } from './iso3166'
|
||||||
import { KyrosContentV1Module } from './kyros/content/v1'
|
import { KyrosContentV1Module } from './kyros/content/v1'
|
||||||
import { KyrosFilesV0Module } from './kyros/files/v0'
|
|
||||||
import { KyrosFilesV1Module } from './kyros/files/v1'
|
import { KyrosFilesV1Module } from './kyros/files/v1'
|
||||||
import { KyrosLogsV1Module } from './kyros/logs/v1'
|
import { KyrosLogsV1Module } from './kyros/logs/v1'
|
||||||
import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
|
import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
|
||||||
@@ -88,7 +87,6 @@ export const MODULE_REGISTRY = {
|
|||||||
mclogs_logs_v1: MclogsLogsV1Module,
|
mclogs_logs_v1: MclogsLogsV1Module,
|
||||||
launchermeta_manifest_v0: LauncherMetaManifestV0Module,
|
launchermeta_manifest_v0: LauncherMetaManifestV0Module,
|
||||||
kyros_content_v1: KyrosContentV1Module,
|
kyros_content_v1: KyrosContentV1Module,
|
||||||
kyros_files_v0: KyrosFilesV0Module,
|
|
||||||
kyros_files_v1: KyrosFilesV1Module,
|
kyros_files_v1: KyrosFilesV1Module,
|
||||||
kyros_logs_v1: KyrosLogsV1Module,
|
kyros_logs_v1: KyrosLogsV1Module,
|
||||||
kyros_upload_sessions_v1: KyrosUploadSessionsV1Module,
|
kyros_upload_sessions_v1: KyrosUploadSessionsV1Module,
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
import { AbstractModule } from '../../../core/abstract-module'
|
|
||||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
|
||||||
import { getNodeBaseUrl } from '../../../utils/node-url'
|
|
||||||
import type { Archon } from '../../archon/types'
|
|
||||||
import type { Kyros } from '../types'
|
|
||||||
|
|
||||||
type NodeFsAuth = Pick<Archon.Servers.v0.JWTAuth, 'url' | 'token'>
|
|
||||||
|
|
||||||
export class KyrosFilesV0Module extends AbstractModule {
|
|
||||||
public getModuleID(): string {
|
|
||||||
return 'kyros_files_v0'
|
|
||||||
}
|
|
||||||
|
|
||||||
private getNodeBaseUrl(auth: NodeFsAuth): string {
|
|
||||||
return getNodeBaseUrl(auth.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List directory contents with pagination
|
|
||||||
*
|
|
||||||
* @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 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: 'modrinth/v0',
|
|
||||||
method: 'GET',
|
|
||||||
params: { path, page, page_size: pageSize },
|
|
||||||
useNodeAuth: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: 'modrinth/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: 'modrinth/v0',
|
|
||||||
method: 'GET',
|
|
||||||
params: { path },
|
|
||||||
useNodeAuth: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a file using explicit filesystem auth credentials.
|
|
||||||
*
|
|
||||||
* @param auth - Filesystem auth (url + token) from Archon
|
|
||||||
* @param path - File path (e.g., "/server-icon.png")
|
|
||||||
* @returns Promise resolving to file Blob
|
|
||||||
*/
|
|
||||||
public async downloadFileWithAuth(auth: NodeFsAuth, path: string): Promise<Blob> {
|
|
||||||
return this.client.request<Blob>('/fs/download', {
|
|
||||||
api: this.getNodeBaseUrl(auth),
|
|
||||||
version: 'modrinth/v0',
|
|
||||||
method: 'GET',
|
|
||||||
params: { path },
|
|
||||||
headers: { Authorization: `Bearer ${auth.token}` },
|
|
||||||
skipAuth: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a file to a server's filesystem with progress tracking
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
* @deprecated Use `kyros.upload_sessions_v1` for bulk uploads so cancellation can remove staged files before finalize.
|
|
||||||
*/
|
|
||||||
public uploadFile(
|
|
||||||
path: string,
|
|
||||||
file: File | Blob,
|
|
||||||
options?: {
|
|
||||||
onProgress?: (progress: UploadProgress) => void
|
|
||||||
retry?: boolean | number
|
|
||||||
},
|
|
||||||
): UploadHandle<void> {
|
|
||||||
return this.client.upload<void>('/fs/create', {
|
|
||||||
api: '',
|
|
||||||
version: 'modrinth/v0',
|
|
||||||
file,
|
|
||||||
params: { path, type: 'file' },
|
|
||||||
onProgress: options?.onProgress,
|
|
||||||
retry: options?.retry,
|
|
||||||
useNodeAuth: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a file using explicit filesystem auth credentials.
|
|
||||||
*
|
|
||||||
* @param auth - Filesystem auth (url + token) from Archon
|
|
||||||
* @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 uploadFileWithAuth(
|
|
||||||
auth: NodeFsAuth,
|
|
||||||
path: string,
|
|
||||||
file: File | Blob,
|
|
||||||
options?: {
|
|
||||||
onProgress?: (progress: UploadProgress) => void
|
|
||||||
retry?: boolean | number
|
|
||||||
},
|
|
||||||
): UploadHandle<void> {
|
|
||||||
return this.client.upload<void>('/fs/create', {
|
|
||||||
api: this.getNodeBaseUrl(auth),
|
|
||||||
version: 'modrinth/v0',
|
|
||||||
file,
|
|
||||||
params: { path, type: 'file' },
|
|
||||||
headers: { Authorization: `Bearer ${auth.token}` },
|
|
||||||
onProgress: options?.onProgress,
|
|
||||||
retry: options?.retry,
|
|
||||||
skipAuth: 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: 'modrinth/v0',
|
|
||||||
method: 'PUT',
|
|
||||||
params: { path },
|
|
||||||
body: blob,
|
|
||||||
headers: { 'Content-Type': 'application/octet-stream' },
|
|
||||||
useNodeAuth: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a file or folder using explicit filesystem auth credentials.
|
|
||||||
*
|
|
||||||
* @param auth - Filesystem auth (url + token) from Archon
|
|
||||||
* @param path - Path to delete
|
|
||||||
* @param recursive - If true, delete directory contents recursively
|
|
||||||
*/
|
|
||||||
public async deleteFileOrFolderWithAuth(
|
|
||||||
auth: NodeFsAuth,
|
|
||||||
path: string,
|
|
||||||
recursive: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
return this.client.request<void>('/fs/delete', {
|
|
||||||
api: this.getNodeBaseUrl(auth),
|
|
||||||
version: 'modrinth/v0',
|
|
||||||
method: 'DELETE',
|
|
||||||
params: { path, recursive },
|
|
||||||
headers: { Authorization: `Bearer ${auth.token}` },
|
|
||||||
skipAuth: 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AbstractModule } from '../../../core/abstract-module'
|
import { AbstractModule } from '../../../core/abstract-module'
|
||||||
|
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||||
import type { Kyros } from '../types'
|
import type { Kyros } from '../types'
|
||||||
|
|
||||||
export class KyrosFilesV1Module extends AbstractModule {
|
export class KyrosFilesV1Module extends AbstractModule {
|
||||||
@@ -6,6 +7,25 @@ export class KyrosFilesV1Module extends AbstractModule {
|
|||||||
return 'kyros_files_v1'
|
return 'kyros_files_v1'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isConflict(error: unknown): boolean {
|
||||||
|
const err = error as { statusCode?: number; response?: { status?: number } }
|
||||||
|
return (err.statusCode ?? err.response?.status) === 409
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createDownloadSession(
|
||||||
|
worldId: string,
|
||||||
|
path: string,
|
||||||
|
zipped: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.client.request<void>(`/worlds/${worldId}/files/contents`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'POST',
|
||||||
|
body: { path, zipped },
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public async listDescendants(
|
public async listDescendants(
|
||||||
worldId: string,
|
worldId: string,
|
||||||
path: string,
|
path: string,
|
||||||
@@ -47,6 +67,70 @@ export class KyrosFilesV1Module extends AbstractModule {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async editFile(worldId: string, path: string, content: string | Blob): Promise<void> {
|
||||||
|
const body = typeof content === 'string' ? new Blob([content]) : content
|
||||||
|
|
||||||
|
return this.client.request<void>(`/worlds/${worldId}/files/edit`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'POST',
|
||||||
|
params: { path },
|
||||||
|
body,
|
||||||
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public uploadFile(
|
||||||
|
worldId: string,
|
||||||
|
path: string,
|
||||||
|
file: File | Blob,
|
||||||
|
options?: {
|
||||||
|
onProgress?: (progress: UploadProgress) => void
|
||||||
|
retry?: boolean | number
|
||||||
|
},
|
||||||
|
): UploadHandle<void> {
|
||||||
|
return this.client.upload<void>(`/worlds/${worldId}/files/edit`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
file,
|
||||||
|
params: { path },
|
||||||
|
onProgress: options?.onProgress,
|
||||||
|
retry: options?.retry,
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async touchFile(worldId: string, path: string): Promise<void> {
|
||||||
|
return this.client.request<void>(`/worlds/${worldId}/files/touch`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'POST',
|
||||||
|
body: { path },
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async mkdirFile(worldId: string, path: string): Promise<void> {
|
||||||
|
return this.client.request<void>(`/worlds/${worldId}/files/mkdir`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'POST',
|
||||||
|
body: { path },
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ensureFile(worldId: string, path: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.touchFile(worldId, path)
|
||||||
|
} catch (error) {
|
||||||
|
if (!this.isConflict(error)) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async deleteFile(worldId: string, path: string): Promise<void> {
|
public async deleteFile(worldId: string, path: string): Promise<void> {
|
||||||
return this.client.request<void>(`/worlds/${worldId}/files/delete`, {
|
return this.client.request<void>(`/worlds/${worldId}/files/delete`, {
|
||||||
api: '',
|
api: '',
|
||||||
@@ -90,4 +174,49 @@ export class KyrosFilesV1Module extends AbstractModule {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public unzipFile(
|
||||||
|
worldId: string,
|
||||||
|
request: Kyros.Files.v1.UnzipFileRequest,
|
||||||
|
): Promise<ReadableStream<Uint8Array>> {
|
||||||
|
return this.client.stream(`/worlds/${worldId}/files/unzip`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
method: 'POST',
|
||||||
|
body: request,
|
||||||
|
headers: { Accept: 'application/json-seq' },
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public uploadZip(
|
||||||
|
worldId: string,
|
||||||
|
path: string,
|
||||||
|
file: File | Blob,
|
||||||
|
options?: {
|
||||||
|
onProgress?: (progress: UploadProgress) => void
|
||||||
|
retry?: boolean | number
|
||||||
|
},
|
||||||
|
): UploadHandle<void> {
|
||||||
|
return this.client.upload<void>(`/worlds/${worldId}/files/upload-zip`, {
|
||||||
|
api: '',
|
||||||
|
version: 'v1',
|
||||||
|
file,
|
||||||
|
params: { path },
|
||||||
|
headers: { 'Content-Type': 'application/zip' },
|
||||||
|
onProgress: options?.onProgress,
|
||||||
|
retry: options?.retry,
|
||||||
|
useNodeAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,25 @@ export namespace Kyros {
|
|||||||
export namespace UploadSessions {
|
export namespace UploadSessions {
|
||||||
export namespace v1 {
|
export namespace v1 {
|
||||||
export type Scope = 'content' | 'files'
|
export type Scope = 'content' | 'files'
|
||||||
export type UploadSessionStatus =
|
|
||||||
| 'active'
|
export type UploadSessionFile = {
|
||||||
| 'uploading'
|
file: File | Blob
|
||||||
| 'finalizing'
|
filename: string
|
||||||
| 'cancelled'
|
}
|
||||||
| 'finalized'
|
|
||||||
| 'expired'
|
|
||||||
|
|
||||||
export interface UploadSessionResponse {
|
export interface UploadSessionResponse {
|
||||||
upload_id: string
|
upload_id: string
|
||||||
status: UploadSessionStatus
|
status: string
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
last_upload_at: number | null
|
last_upload_at?: number | null
|
||||||
expires_at: number
|
expires_at: number
|
||||||
entry_count: number
|
entry_count: number
|
||||||
uploaded_byte_count: number
|
uploaded_byte_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetUploadSessionResponse {
|
export interface GetUploadSessionResponse {
|
||||||
session: UploadSessionResponse | null
|
session?: UploadSessionResponse | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,6 +29,16 @@ export namespace Kyros {
|
|||||||
export namespace v1 {
|
export namespace v1 {
|
||||||
export type DescendantType = 'regular' | 'directory' | 'symlink' | 'other'
|
export type DescendantType = 'regular' | 'directory' | 'symlink' | 'other'
|
||||||
|
|
||||||
|
export type UnzipSource =
|
||||||
|
| {
|
||||||
|
type: 'zip_url'
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'zip_path'
|
||||||
|
path: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateDownloadSessionRequest {
|
export interface CreateDownloadSessionRequest {
|
||||||
path: string
|
path: string
|
||||||
zipped: boolean
|
zipped: boolean
|
||||||
@@ -81,29 +89,14 @@ export namespace Kyros {
|
|||||||
path: string
|
path: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export namespace v0 {
|
export interface PathMutationRequest {
|
||||||
export interface DirectoryItem {
|
|
||||||
name: string
|
|
||||||
type: 'file' | 'directory' | 'symlink'
|
|
||||||
path: string
|
path: string
|
||||||
modified: number
|
|
||||||
created: number
|
|
||||||
size?: number
|
|
||||||
count?: number
|
|
||||||
target?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DirectoryResponse {
|
export interface UnzipFileRequest {
|
||||||
items: DirectoryItem[]
|
source: UnzipSource
|
||||||
total: number
|
target: string
|
||||||
current: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExtractResult {
|
|
||||||
modpack_name: string | null
|
|
||||||
conflicting_files: string[]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,6 @@ import { AbstractModule } from '../../../core/abstract-module'
|
|||||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||||
import type { Kyros } from '../types'
|
import type { Kyros } from '../types'
|
||||||
|
|
||||||
export type UploadSessionFile = {
|
|
||||||
file: File | Blob
|
|
||||||
filename: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export class KyrosUploadSessionsV1Module extends AbstractModule {
|
export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||||
public getModuleID(): string {
|
public getModuleID(): string {
|
||||||
return 'kyros_upload_sessions_v1'
|
return 'kyros_upload_sessions_v1'
|
||||||
@@ -46,7 +41,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
|||||||
scope: Kyros.UploadSessions.v1.Scope,
|
scope: Kyros.UploadSessions.v1.Scope,
|
||||||
worldId: string,
|
worldId: string,
|
||||||
uploadId: string,
|
uploadId: string,
|
||||||
files: UploadSessionFile[],
|
files: Kyros.UploadSessions.v1.UploadSessionFile[],
|
||||||
options?: {
|
options?: {
|
||||||
onProgress?: (progress: UploadProgress) => void
|
onProgress?: (progress: UploadProgress) => void
|
||||||
retry?: boolean | number
|
retry?: boolean | number
|
||||||
|
|||||||
@@ -407,6 +407,7 @@ export type PendingChange = {
|
|||||||
|
|
||||||
type ServerListingProps = {
|
type ServerListingProps = {
|
||||||
server_id: string
|
server_id: string
|
||||||
|
worldId?: string | null
|
||||||
name: string
|
name: string
|
||||||
status: Archon.Servers.v0.Status
|
status: Archon.Servers.v0.Status
|
||||||
suspension_reason?: Archon.Servers.v0.SuspensionReason | null
|
suspension_reason?: Archon.Servers.v0.SuspensionReason | null
|
||||||
@@ -546,16 +547,28 @@ async function dataURLToBlob(dataURL: string): Promise<Blob> {
|
|||||||
return res.blob()
|
return res.blob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getActiveWorldId(serverId: string): Promise<string | null> {
|
||||||
|
const server = await archon.servers_v1.get(serverId)
|
||||||
|
const activeWorld = server.worlds.find((world) => world.is_active)
|
||||||
|
return activeWorld?.id ?? server.worlds[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadWorldFile(worldId: string, path: string, file: File | Blob) {
|
||||||
|
await kyros.files_v1.ensureFile(worldId, path)
|
||||||
|
await kyros.files_v1.uploadFile(worldId, path, file).promise
|
||||||
|
}
|
||||||
|
|
||||||
const { data: image } = useQuery({
|
const { data: image } = useQuery({
|
||||||
queryKey: ['server-icon', props.server_id] as const,
|
queryKey: computed(() => ['server-icon', props.server_id, props.worldId ?? null] as const),
|
||||||
queryFn: async (): Promise<string | null> => {
|
queryFn: async (): Promise<string | null> => {
|
||||||
if (!props.server_id || props.status !== 'available') return null
|
if (!props.server_id || props.status !== 'available') return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fsAuth = await archon.servers_v0.getFilesystemAuth(props.server_id)
|
const worldId = props.worldId ?? (await getActiveWorldId(props.server_id))
|
||||||
|
if (!worldId) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await kyros.files_v0.downloadFileWithAuth(fsAuth, '/server-icon.png')
|
const blob = await kyros.files_v1.downloadRawFileContents(worldId, '/server-icon.png')
|
||||||
return await processImageBlob(blob, 64)
|
return await processImageBlob(blob, 64)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const statusCode = (error as { statusCode?: number })?.statusCode
|
const statusCode = (error as { statusCode?: number })?.statusCode
|
||||||
@@ -564,8 +577,8 @@ const { data: image } = useQuery({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const originalBlob = await kyros.files_v0.downloadFileWithAuth(
|
const originalBlob = await kyros.files_v1.downloadRawFileContents(
|
||||||
fsAuth,
|
worldId,
|
||||||
'/server-icon-original.png',
|
'/server-icon-original.png',
|
||||||
)
|
)
|
||||||
return await processImageBlob(originalBlob, 64)
|
return await processImageBlob(originalBlob, 64)
|
||||||
@@ -585,13 +598,12 @@ const { data: image } = useQuery({
|
|||||||
const scaledBlob = await dataURLToBlob(scaledDataUrl)
|
const scaledBlob = await dataURLToBlob(scaledDataUrl)
|
||||||
const scaledFile = new File([scaledBlob], 'server-icon.png', { type: 'image/png' })
|
const scaledFile = new File([scaledBlob], 'server-icon.png', { type: 'image/png' })
|
||||||
|
|
||||||
await kyros.files_v0.uploadFileWithAuth(fsAuth, '/server-icon.png', scaledFile).promise
|
await uploadWorldFile(worldId, '/server-icon.png', scaledFile)
|
||||||
|
|
||||||
const originalFile = new File([blob], 'server-icon-original.png', {
|
const originalFile = new File([blob], 'server-icon-original.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
})
|
})
|
||||||
await kyros.files_v0.uploadFileWithAuth(fsAuth, '/server-icon-original.png', originalFile)
|
await uploadWorldFile(worldId, '/server-icon-original.png', originalFile)
|
||||||
.promise
|
|
||||||
|
|
||||||
return scaledDataUrl
|
return scaledDataUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const props = withDefaults(
|
|||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { serverId, server } = injectModrinthServerContext()
|
const { serverId, server, worldId } = injectModrinthServerContext()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const isUploadingIcon = ref(false)
|
const isUploadingIcon = ref(false)
|
||||||
const isSyncingIcon = ref(false)
|
const isSyncingIcon = ref(false)
|
||||||
@@ -95,6 +95,7 @@ const {
|
|||||||
computed(() => server.value?.upstream ?? null),
|
computed(() => server.value?.upstream ?? null),
|
||||||
{
|
{
|
||||||
includeProjectFallback: false,
|
includeProjectFallback: false,
|
||||||
|
worldId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +108,19 @@ function isNotFound(error: unknown): boolean {
|
|||||||
return getStatusCode(error) === 404
|
return getStatusCode(error) === 404
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWorldId() {
|
||||||
|
if (!worldId.value) {
|
||||||
|
throw new Error('World ID is not available.')
|
||||||
|
}
|
||||||
|
return worldId.value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadWorldFile(path: string, file: File | Blob) {
|
||||||
|
const id = getWorldId()
|
||||||
|
await client.kyros.files_v1.ensureFile(id, path)
|
||||||
|
await client.kyros.files_v1.uploadFile(id, path, file).promise
|
||||||
|
}
|
||||||
|
|
||||||
const uploadFile = async (e: Event) => {
|
const uploadFile = async (e: Event) => {
|
||||||
if (isIconActionDisabled.value) return
|
if (isIconActionDisabled.value) return
|
||||||
|
|
||||||
@@ -144,39 +158,11 @@ const uploadFile = async (e: Event) => {
|
|||||||
img.src = URL.createObjectURL(file)
|
img.src = URL.createObjectURL(file)
|
||||||
})
|
})
|
||||||
|
|
||||||
const fsAuth = await client.archon.servers_v0.getFilesystemAuth(serverId)
|
await uploadWorldFile('/server-icon.png', scaledFile)
|
||||||
|
|
||||||
try {
|
|
||||||
await client.kyros.files_v0.uploadFileWithAuth(fsAuth, '/server-icon.png', scaledFile).promise
|
|
||||||
} catch (scaledUploadError) {
|
|
||||||
// Node FS may reject create when file already exists. Delete and retry once.
|
|
||||||
try {
|
|
||||||
await client.kyros.files_v0.deleteFileOrFolderWithAuth(fsAuth, '/server-icon.png', false)
|
|
||||||
} catch (deleteError) {
|
|
||||||
if (!isNotFound(deleteError)) {
|
|
||||||
throw scaledUploadError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.kyros.files_v0.uploadFileWithAuth(fsAuth, '/server-icon.png', scaledFile).promise
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep original file in sync when possible, but don't block icon updates on failures here.
|
// Keep original file in sync when possible, but don't block icon updates on failures here.
|
||||||
try {
|
try {
|
||||||
await client.kyros.files_v0.deleteFileOrFolderWithAuth(
|
await uploadWorldFile('/server-icon-original.png', file)
|
||||||
fsAuth,
|
|
||||||
'/server-icon-original.png',
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
} catch (deleteOriginalError) {
|
|
||||||
if (!isNotFound(deleteOriginalError)) {
|
|
||||||
// best effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await client.kyros.files_v0.uploadFileWithAuth(fsAuth, '/server-icon-original.png', file)
|
|
||||||
.promise
|
|
||||||
} catch (originalUploadError) {
|
} catch (originalUploadError) {
|
||||||
if (!isNotFound(originalUploadError)) {
|
if (!isNotFound(originalUploadError)) {
|
||||||
// best effort
|
// best effort
|
||||||
@@ -222,10 +208,10 @@ const resetIcon = async () => {
|
|||||||
isSyncingIcon.value = true
|
isSyncingIcon.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fsAuth = await client.archon.servers_v0.getFilesystemAuth(serverId)
|
const id = getWorldId()
|
||||||
const deleteResults = await Promise.allSettled([
|
const deleteResults = await Promise.allSettled([
|
||||||
client.kyros.files_v0.deleteFileOrFolderWithAuth(fsAuth, '/server-icon.png', false),
|
client.kyros.files_v1.deleteFile(id, '/server-icon.png'),
|
||||||
client.kyros.files_v0.deleteFileOrFolderWithAuth(fsAuth, '/server-icon-original.png', false),
|
client.kyros.files_v1.deleteFile(id, '/server-icon-original.png'),
|
||||||
])
|
])
|
||||||
|
|
||||||
for (const result of deleteResults) {
|
for (const result of deleteResults) {
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ export function useServerManageCoreRuntime(options: UseServerManageCoreRuntimeOp
|
|||||||
dismissedOpIds.value = new Set([...dismissedOpIds.value, opId])
|
dismissedOpIds.value = new Set([...dismissedOpIds.value, opId])
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await client.kyros.files_v0.modifyOperation(opId, action)
|
await client.kyros.files_v1.modifyOperation(opId, action)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (action === 'dismiss') return
|
if (action === 'dismiss') return
|
||||||
console.error(`Failed to ${action} operation:`, error)
|
console.error(`Failed to ${action} operation:`, error)
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import { injectModrinthClient } from '#ui/providers'
|
|||||||
|
|
||||||
type UpstreamRef = ComputedRef<Archon.Servers.v0.Server['upstream'] | null | undefined>
|
type UpstreamRef = ComputedRef<Archon.Servers.v0.Server['upstream'] | null | undefined>
|
||||||
type ServerIdSource = string | { readonly value: string }
|
type ServerIdSource = string | { readonly value: string }
|
||||||
|
type WorldIdSource = string | null | undefined | { readonly value: string | null | undefined }
|
||||||
|
|
||||||
type UseServerImageOptions = {
|
type UseServerImageOptions = {
|
||||||
enabled?: ComputedRef<boolean> | boolean
|
enabled?: ComputedRef<boolean> | boolean
|
||||||
size?: number
|
size?: number
|
||||||
includeProjectFallback?: boolean
|
includeProjectFallback?: boolean
|
||||||
|
worldId?: WorldIdSource
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processImageBlob(blob: Blob, size: number): Promise<string> {
|
export async function processImageBlob(blob: Blob, size: number): Promise<string> {
|
||||||
@@ -49,6 +51,7 @@ export function useServerImage(
|
|||||||
const iconSize = options.size ?? 512
|
const iconSize = options.size ?? 512
|
||||||
const includeProjectFallback = options.includeProjectFallback ?? false
|
const includeProjectFallback = options.includeProjectFallback ?? false
|
||||||
const resolvedServerId = computed(() => resolveServerId(serverId))
|
const resolvedServerId = computed(() => resolveServerId(serverId))
|
||||||
|
const resolvedWorldId = computed(() => resolveWorldId(options.worldId))
|
||||||
|
|
||||||
const queryKey = computed(
|
const queryKey = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -57,6 +60,7 @@ export function useServerImage(
|
|||||||
'detail',
|
'detail',
|
||||||
resolvedServerId.value,
|
resolvedServerId.value,
|
||||||
'icon',
|
'icon',
|
||||||
|
resolvedWorldId.value ?? 'active',
|
||||||
upstream.value?.project_id ?? null,
|
upstream.value?.project_id ?? null,
|
||||||
] as const,
|
] as const,
|
||||||
)
|
)
|
||||||
@@ -74,18 +78,22 @@ export function useServerImage(
|
|||||||
if (!id) return null
|
if (!id) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fsAuth = await client.archon.servers_v0.getFilesystemAuth(id)
|
const targetWorldId = resolvedWorldId.value ?? (await getActiveWorldId(id))
|
||||||
|
if (!targetWorldId) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await client.kyros.files_v0.downloadFileWithAuth(fsAuth, '/server-icon.png')
|
const blob = await client.kyros.files_v1.downloadRawFileContents(
|
||||||
|
targetWorldId,
|
||||||
|
'/server-icon.png',
|
||||||
|
)
|
||||||
return await processImageBlob(blob, iconSize)
|
return await processImageBlob(blob, iconSize)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!isNotFound(error)) throw error
|
if (!isNotFound(error)) throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await client.kyros.files_v0.downloadFileWithAuth(
|
const blob = await client.kyros.files_v1.downloadRawFileContents(
|
||||||
fsAuth,
|
targetWorldId,
|
||||||
'/server-icon-original.png',
|
'/server-icon-original.png',
|
||||||
)
|
)
|
||||||
return await processImageBlob(blob, iconSize)
|
return await processImageBlob(blob, iconSize)
|
||||||
@@ -94,7 +102,6 @@ export function useServerImage(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug('Server image fetch failed:', error)
|
console.debug('Server image fetch failed:', error)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!includeProjectFallback || !upstream.value?.project_id) return null
|
if (!includeProjectFallback || !upstream.value?.project_id) return null
|
||||||
@@ -133,6 +140,12 @@ export function useServerImage(
|
|||||||
localImage.value = undefined
|
localImage.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getActiveWorldId(id: string): Promise<string | null> {
|
||||||
|
const server = await client.archon.servers_v1.get(id)
|
||||||
|
const activeWorld = server.worlds.find((world) => world.is_active)
|
||||||
|
return activeWorld?.id ?? server.worlds[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
image,
|
image,
|
||||||
queryKey,
|
queryKey,
|
||||||
@@ -146,3 +159,9 @@ export function useServerImage(
|
|||||||
function resolveServerId(serverId: ServerIdSource): string {
|
function resolveServerId(serverId: ServerIdSource): string {
|
||||||
return typeof serverId === 'string' ? serverId : serverId.value
|
return typeof serverId === 'string' ? serverId : serverId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveWorldId(worldId: WorldIdSource): string | null {
|
||||||
|
if (worldId == null) return null
|
||||||
|
if (typeof worldId === 'string') return worldId
|
||||||
|
return worldId.value ?? null
|
||||||
|
}
|
||||||
|
|||||||
+10
-20
@@ -121,6 +121,7 @@ const { formatMessage } = useVIntl()
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
worldId: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
disabledTooltip?: string
|
disabledTooltip?: string
|
||||||
}>(),
|
}>(),
|
||||||
@@ -187,14 +188,6 @@ const messages = defineMessages({
|
|||||||
id: 'files.zip-url-modal.error-url-invalid',
|
id: 'files.zip-url-modal.error-url-invalid',
|
||||||
defaultMessage: 'URL must be valid.',
|
defaultMessage: 'URL must be valid.',
|
||||||
},
|
},
|
||||||
cfNotFoundTitle: {
|
|
||||||
id: 'files.zip-url-modal.cf-not-found-title',
|
|
||||||
defaultMessage: 'CurseForge modpack not found',
|
|
||||||
},
|
|
||||||
cfNotFoundText: {
|
|
||||||
id: 'files.zip-url-modal.cf-not-found-text',
|
|
||||||
defaultMessage: 'Could not find CurseForge modpack at that URL.',
|
|
||||||
},
|
|
||||||
installFailedTitle: {
|
installFailedTitle: {
|
||||||
id: 'files.zip-url-modal.install-failed-title',
|
id: 'files.zip-url-modal.install-failed-title',
|
||||||
defaultMessage: 'Installation failed',
|
defaultMessage: 'Installation failed',
|
||||||
@@ -265,19 +258,16 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
submitted.value = true
|
submitted.value = true
|
||||||
try {
|
try {
|
||||||
const dry = await client.kyros.files_v0.extractFile(trimmedUrl.value, true, true)
|
const stream = await client.kyros.files_v1.unzipFile(props.worldId, {
|
||||||
|
source: { type: 'zip_url', url: trimmedUrl.value },
|
||||||
if (!cf.value || dry.modpack_name) {
|
target: '/',
|
||||||
await client.kyros.files_v0.extractFile(trimmedUrl.value, true, false)
|
})
|
||||||
hide()
|
const reader = stream.getReader()
|
||||||
} else {
|
while (true) {
|
||||||
submitted.value = false
|
const { done } = await reader.read()
|
||||||
addNotification({
|
if (done) break
|
||||||
title: formatMessage(messages.cfNotFoundTitle),
|
|
||||||
text: formatMessage(messages.cfNotFoundText),
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
submitted.value = false
|
submitted.value = false
|
||||||
console.error('Error installing:', err)
|
console.error('Error installing:', err)
|
||||||
|
|||||||
@@ -181,12 +181,13 @@ interface UploadItem {
|
|||||||
| 'cancelled'
|
| 'cancelled'
|
||||||
| 'incorrect-type'
|
| 'incorrect-type'
|
||||||
size: string
|
size: string
|
||||||
uploader?: ReturnType<typeof client.kyros.files_v0.uploadFile>
|
uploader?: ReturnType<typeof client.kyros.files_v1.uploadFile>
|
||||||
error?: Error
|
error?: Error
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
currentPath: string
|
currentPath: string
|
||||||
|
worldId: string
|
||||||
fileType?: string
|
fileType?: string
|
||||||
marginBottom?: number
|
marginBottom?: number
|
||||||
acceptedTypes?: Array<string>
|
acceptedTypes?: Array<string>
|
||||||
@@ -281,11 +282,12 @@ const uploadFile = async (file: File) => {
|
|||||||
uploadItem.status = 'uploading'
|
uploadItem.status = 'uploading'
|
||||||
const filePath = `${props.currentPath}/${file.name}`.replace('//', '/')
|
const filePath = `${props.currentPath}/${file.name}`.replace('//', '/')
|
||||||
|
|
||||||
const uploader = client.kyros.files_v0.uploadFile(filePath, file, {
|
await client.kyros.files_v1.ensureFile(props.worldId, filePath)
|
||||||
|
const uploader = client.kyros.files_v1.uploadFile(props.worldId, filePath, file, {
|
||||||
onProgress: ({ progress }) => {
|
onProgress: ({ progress }) => {
|
||||||
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
const index = uploadQueue.value.findIndex((item) => item.file.name === file.name)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
uploadQueue.value[index].progress = Math.round(progress)
|
uploadQueue.value[index].progress = Math.round(progress * 100)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
<FileCreateItemModal ref="createItemModal" :type="newItemType" @create="handleCreateNewItem" />
|
<FileCreateItemModal ref="createItemModal" :type="newItemType" @create="handleCreateNewItem" />
|
||||||
<FileUploadConflictModal ref="uploadConflictModal" @proceed="handleExtractConfirm" />
|
<FileUploadConflictModal ref="uploadConflictModal" @proceed="handleExtractConfirm" />
|
||||||
<FileUploadZipUrlModal
|
<FileUploadZipUrlModal
|
||||||
v-if="ctx.showInstallFromUrl"
|
v-if="showInstallFromUrl"
|
||||||
ref="uploadZipUrlModal"
|
ref="uploadZipUrlModal"
|
||||||
|
:world-id="installFromUrlWorldId"
|
||||||
:disabled="isBusy"
|
:disabled="isBusy"
|
||||||
:disabled-tooltip="busyTooltip"
|
:disabled-tooltip="busyTooltip"
|
||||||
/>
|
/>
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
:is-editor-find-open="fileEditorRef?.isFindOpen"
|
:is-editor-find-open="fileEditorRef?.isFindOpen"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
:show-refresh-button="showRefreshButton"
|
:show-refresh-button="showRefreshButton"
|
||||||
:show-install-from-url="ctx.showInstallFromUrl"
|
:show-install-from-url="showInstallFromUrl"
|
||||||
:base-id="baseId"
|
:base-id="baseId"
|
||||||
:disabled="isBusy"
|
:disabled="isBusy"
|
||||||
:disabled-tooltip="busyTooltip"
|
:disabled-tooltip="busyTooltip"
|
||||||
@@ -378,6 +379,8 @@ const selectedItem = ref<FileItem | null>(null)
|
|||||||
const unsavedChangesModal = ref<InstanceType<typeof FileUnsavedChangesModal>>()
|
const unsavedChangesModal = ref<InstanceType<typeof FileUnsavedChangesModal>>()
|
||||||
|
|
||||||
const hasUnsavedChanges = computed(() => fileEditorRef.value?.hasUnsavedChanges ?? false)
|
const hasUnsavedChanges = computed(() => fileEditorRef.value?.hasUnsavedChanges ?? false)
|
||||||
|
const installFromUrlWorldId = computed(() => ctx.worldId?.value ?? '')
|
||||||
|
const showInstallFromUrl = computed(() => !!ctx.showInstallFromUrl && !!installFromUrlWorldId.value)
|
||||||
|
|
||||||
async function confirmDiscardChanges(): Promise<boolean> {
|
async function confirmDiscardChanges(): Promise<boolean> {
|
||||||
if (!hasUnsavedChanges.value) return true
|
if (!hasUnsavedChanges.value) return true
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface FileManagerContext {
|
|||||||
uploadFiles: (files: File[]) => void
|
uploadFiles: (files: File[]) => void
|
||||||
cancelUpload?: () => void
|
cancelUpload?: () => void
|
||||||
uploadState?: Ref<UploadState> | ComputedRef<UploadState>
|
uploadState?: Ref<UploadState> | ComputedRef<UploadState>
|
||||||
|
worldId?: Ref<string | null> | ComputedRef<string | null>
|
||||||
|
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
|
|
||||||
|
|||||||
@@ -818,6 +818,7 @@ const { disconnect: disconnectPanelSync } = useServerPanelSync({
|
|||||||
const { image: serverImage } = useServerImage(
|
const { image: serverImage } = useServerImage(
|
||||||
props.serverId,
|
props.serverId,
|
||||||
computed(() => serverData.value?.upstream ?? null),
|
computed(() => serverData.value?.upstream ?? null),
|
||||||
|
{ worldId },
|
||||||
)
|
)
|
||||||
const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null))
|
const { data: serverProject } = useServerProject(computed(() => serverData.value?.upstream ?? null))
|
||||||
|
|
||||||
@@ -1228,7 +1229,7 @@ const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) =>
|
|||||||
const cancelled = allOps.filter((x) => x.state === 'cancelled')
|
const cancelled = allOps.filter((x) => x.state === 'cancelled')
|
||||||
Promise.all(
|
Promise.all(
|
||||||
cancelled.map((x) =>
|
cancelled.map((x) =>
|
||||||
client.kyros.files_v0.modifyOperation(x.id, 'dismiss').catch((error) => {
|
client.kyros.files_v1.modifyOperation(x.id, 'dismiss').catch((error) => {
|
||||||
console.error('Failed to dismiss cancelled operation:', error)
|
console.error('Failed to dismiss cancelled operation:', error)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -1260,20 +1261,25 @@ const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallation
|
|||||||
installError.value = new Error(errorMessage.value)
|
installError.value = new Error(errorMessage.value)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let files = await client.kyros.files_v0.listDirectory('/', 1, 100)
|
if (!worldId.value) break
|
||||||
if (files && files.total > 1) {
|
let files = await client.kyros.files_v1.listDescendants(worldId.value, '/', 1, 100)
|
||||||
for (let i = 2; i <= files.total; i++) {
|
for (let i = 2; i <= files.page_total; i++) {
|
||||||
const nextFiles = await client.kyros.files_v0.listDirectory('/', i, 100)
|
const nextFiles = await client.kyros.files_v1.listDescendants(worldId.value, '/', i, 100)
|
||||||
if (nextFiles?.items?.length === 0) break
|
if (nextFiles.items.length === 0) break
|
||||||
if (nextFiles) files = nextFiles
|
files = {
|
||||||
|
...nextFiles,
|
||||||
|
items: [...files.items, ...nextFiles.items],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const fileName = files?.items?.find((file) =>
|
const file = files.items.find((file) =>
|
||||||
file.name.startsWith('modrinth-installation'),
|
file.name.startsWith('modrinth-installation'),
|
||||||
)?.name
|
)
|
||||||
errorLogFile.value = fileName ?? ''
|
errorLogFile.value = file?.full_path ?? ''
|
||||||
if (fileName) {
|
if (file) {
|
||||||
const content = await client.kyros.files_v0.downloadFile(fileName)
|
const content = await client.kyros.files_v1.downloadRawFileContents(
|
||||||
|
worldId.value,
|
||||||
|
file.full_path,
|
||||||
|
)
|
||||||
errorLog.value = await content.text()
|
errorLog.value = await content.text()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+16
-10
@@ -326,9 +326,11 @@ const moveMutation = useMutation({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (_vars: { path: string; type: 'file' | 'directory' }) => {
|
mutationFn: ({ path, type }: { path: string; type: 'file' | 'directory' }) => {
|
||||||
// await client.kyros.files_v0.createFileOrFolder(path, type)
|
const id = getWorldId()
|
||||||
throw new Error('Creating files or folders is not supported by the v1 world-scoped files API.')
|
return type === 'directory'
|
||||||
|
? client.kyros.files_v1.mkdirFile(id, path)
|
||||||
|
: client.kyros.files_v1.touchFile(id, path)
|
||||||
},
|
},
|
||||||
onError: (err: Error) => {
|
onError: (err: Error) => {
|
||||||
addNotification({
|
addNotification({
|
||||||
@@ -337,6 +339,9 @@ const createMutation = useMutation({
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
refreshList()
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// File I/O
|
// File I/O
|
||||||
@@ -344,7 +349,7 @@ async function readFile(path: string): Promise<string> {
|
|||||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||||
const id = getWorldId()
|
const id = getWorldId()
|
||||||
const cachedContent = queryClient.getQueryData<string>(['file-content', id, normalizedPath])
|
const cachedContent = queryClient.getQueryData<string>(['file-content', id, normalizedPath])
|
||||||
if (cachedContent) return cachedContent
|
if (cachedContent != null) return cachedContent
|
||||||
const blob = await client.kyros.files_v1.downloadRawFileContents(id, normalizedPath)
|
const blob = await client.kyros.files_v1.downloadRawFileContents(id, normalizedPath)
|
||||||
return await blob.text()
|
return await blob.text()
|
||||||
}
|
}
|
||||||
@@ -356,10 +361,11 @@ async function readFileAsBlob(path: string): Promise<Blob> {
|
|||||||
|
|
||||||
async function writeFile(path: string, content: string): Promise<void> {
|
async function writeFile(path: string, content: string): Promise<void> {
|
||||||
if (fileWriteDisabled.value) return
|
if (fileWriteDisabled.value) return
|
||||||
void path
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||||
void content
|
const id = getWorldId()
|
||||||
// await client.kyros.files_v0.updateFile(path, content)
|
await client.kyros.files_v1.editFile(id, normalizedPath, content)
|
||||||
throw new Error('Updating file contents is not supported by the v1 world-scoped files API.')
|
queryClient.setQueryData(['file-content', id, normalizedPath], content)
|
||||||
|
refreshList()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(path: string, fileName: string): Promise<void> {
|
async function downloadFile(path: string, fileName: string): Promise<void> {
|
||||||
@@ -462,14 +468,14 @@ provideFileManager({
|
|||||||
uploadFiles,
|
uploadFiles,
|
||||||
cancelUpload,
|
cancelUpload,
|
||||||
uploadState,
|
uploadState,
|
||||||
|
worldId,
|
||||||
refresh: refreshList,
|
refresh: refreshList,
|
||||||
isBusy: fileWriteDisabled,
|
isBusy: fileWriteDisabled,
|
||||||
busyTooltip: fileWriteDisabledTooltip,
|
busyTooltip: fileWriteDisabledTooltip,
|
||||||
busyWarning,
|
busyWarning,
|
||||||
// extractFile: async (path, override, dry) => client.kyros.files_v0.extractFile(path, override, dry),
|
|
||||||
prefetchDirectory,
|
prefetchDirectory,
|
||||||
prefetchFile,
|
prefetchFile,
|
||||||
showInstallFromUrl: false,
|
showInstallFromUrl: true,
|
||||||
canRestart: canUsePowerActions.value,
|
canRestart: canUsePowerActions.value,
|
||||||
restartServer,
|
restartServer,
|
||||||
canShareToMclogs: true,
|
canShareToMclogs: true,
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const client = injectModrinthClient()
|
|||||||
const {
|
const {
|
||||||
server: _serverData,
|
server: _serverData,
|
||||||
serverId,
|
serverId,
|
||||||
|
worldId,
|
||||||
isConnected,
|
isConnected,
|
||||||
isWsAuthIncorrect,
|
isWsAuthIncorrect,
|
||||||
stats,
|
stats,
|
||||||
@@ -83,9 +84,13 @@ const isDismissed = () => Date.now() < dismissedUntil.value
|
|||||||
|
|
||||||
const inspectError = async () => {
|
const inspectError = async () => {
|
||||||
if (isDismissed()) return
|
if (isDismissed()) return
|
||||||
|
if (!worldId.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await client.kyros.files_v0.downloadFile('/logs/latest.log')
|
const blob = await client.kyros.files_v1.downloadRawFileContents(
|
||||||
|
worldId.value,
|
||||||
|
'/logs/latest.log',
|
||||||
|
)
|
||||||
const log = await blob.text()
|
const log = await blob.text()
|
||||||
if (!log) return
|
if (!log) return
|
||||||
|
|
||||||
|
|||||||
@@ -180,6 +180,7 @@
|
|||||||
v-for="server in ownedFilteredData.filter((s) => !s.is_medal)"
|
v-for="server in ownedFilteredData.filter((s) => !s.is_medal)"
|
||||||
:key="`owned-${server.server_id}`"
|
:key="`owned-${server.server_id}`"
|
||||||
v-bind="server"
|
v-bind="server"
|
||||||
|
:world-id="getServerWorldId(server.server_id)"
|
||||||
:cancellation-date="serverBillingMap.get(server.server_id)?.cancellationDate"
|
:cancellation-date="serverBillingMap.get(server.server_id)?.cancellationDate"
|
||||||
:is-provisioning="serverBillingMap.get(server.server_id)?.isProvisioning"
|
:is-provisioning="serverBillingMap.get(server.server_id)?.isProvisioning"
|
||||||
:on-resubscribe="serverBillingMap.get(server.server_id)?.onResubscribe"
|
:on-resubscribe="serverBillingMap.get(server.server_id)?.onResubscribe"
|
||||||
@@ -211,6 +212,7 @@
|
|||||||
v-for="server in sharedFilteredData.filter((s) => !s.is_medal)"
|
v-for="server in sharedFilteredData.filter((s) => !s.is_medal)"
|
||||||
:key="`shared-${server.server_id}`"
|
:key="`shared-${server.server_id}`"
|
||||||
v-bind="server"
|
v-bind="server"
|
||||||
|
:world-id="getServerWorldId(server.server_id)"
|
||||||
/>
|
/>
|
||||||
</TransitionGroup>
|
</TransitionGroup>
|
||||||
<div v-else class="text-secondary">
|
<div v-else class="text-secondary">
|
||||||
@@ -762,6 +764,12 @@ const { data: serverFullList } = useQuery({
|
|||||||
enabled: loggedIn,
|
enabled: loggedIn,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function getServerWorldId(serverId: string): string | null {
|
||||||
|
const server = serverFullList.value?.find((server) => server.id === serverId)
|
||||||
|
const activeWorld = server?.worlds.find((world) => world.is_active)
|
||||||
|
return activeWorld?.id ?? server?.worlds[0]?.id ?? null
|
||||||
|
}
|
||||||
|
|
||||||
type ServerBillingInfo = {
|
type ServerBillingInfo = {
|
||||||
cancellationDate?: string | null
|
cancellationDate?: string | null
|
||||||
isProvisioning?: boolean
|
isProvisioning?: boolean
|
||||||
|
|||||||
@@ -177,15 +177,17 @@ import { injectModrinthClient, injectModrinthServerContext, ServersManageFilesPa
|
|||||||
import { useQueryClient } from '@tanstack/vue-query'
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { serverId } = injectModrinthServerContext()
|
const { worldId } = injectModrinthServerContext()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryClient.ensureQueryData({
|
if (worldId.value) {
|
||||||
queryKey: ['files', serverId, '/'],
|
await queryClient.ensureQueryData({
|
||||||
queryFn: () => client.kyros.files_v0.listDirectory('/', 1, 2000),
|
queryKey: ['files', 'v1', worldId.value, '/'],
|
||||||
staleTime: 30_000,
|
queryFn: () => client.kyros.files_v1.listDescendants(worldId.value!, '/', 1, 200),
|
||||||
})
|
staleTime: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Let the mounted layout’s useQuery surface errors; do not fail route setup.
|
// Let the mounted layout’s useQuery surface errors; do not fail route setup.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user