mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 03:55:59 +00:00
feat: migrate files to v1
This commit is contained in:
@@ -15,6 +15,7 @@ import { ArchonTransfersInternalModule } from './archon/transfers/internal'
|
||||
import { ISO3166Module } from './iso3166'
|
||||
import { KyrosContentV1Module } from './kyros/content/v1'
|
||||
import { KyrosFilesV0Module } from './kyros/files/v0'
|
||||
import { KyrosFilesV1Module } from './kyros/files/v1'
|
||||
import { KyrosLogsV1Module } from './kyros/logs/v1'
|
||||
import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
|
||||
import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth'
|
||||
@@ -88,6 +89,7 @@ export const MODULE_REGISTRY = {
|
||||
launchermeta_manifest_v0: LauncherMetaManifestV0Module,
|
||||
kyros_content_v1: KyrosContentV1Module,
|
||||
kyros_files_v0: KyrosFilesV0Module,
|
||||
kyros_files_v1: KyrosFilesV1Module,
|
||||
kyros_logs_v1: KyrosLogsV1Module,
|
||||
kyros_upload_sessions_v1: KyrosUploadSessionsV1Module,
|
||||
labrinth_affiliate_internal: LabrinthAffiliateInternalModule,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Kyros } from '../types'
|
||||
|
||||
export class KyrosFilesV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_files_v1'
|
||||
}
|
||||
|
||||
public async listDescendants(
|
||||
worldId: string,
|
||||
path: string,
|
||||
page: number = 1,
|
||||
itemsPerPage: number = 100,
|
||||
): Promise<Kyros.Files.v1.FileListingResponse> {
|
||||
return this.client.request<Kyros.Files.v1.FileListingResponse>(
|
||||
`/worlds/${worldId}/files/list`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
body: {
|
||||
path,
|
||||
page,
|
||||
items_per_page: itemsPerPage,
|
||||
},
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async downloadRawFileContents(worldId: string, path: string): Promise<Blob> {
|
||||
return this.client.request<Blob>(`/worlds/${worldId}/files/contents-raw`, {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'GET',
|
||||
params: { path },
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
public async downloadTokenizedContents(worldId: string, downloadId: string): Promise<Blob> {
|
||||
return this.client.request<Blob>(`/worlds/${worldId}/files/contents/${downloadId}`, {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'GET',
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
public async deleteFile(worldId: string, path: string): Promise<void> {
|
||||
return this.client.request<void>(`/worlds/${worldId}/files/delete`, {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
body: { path },
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
public async moveFile(
|
||||
worldId: string,
|
||||
source: string,
|
||||
destination: string,
|
||||
): Promise<Kyros.Files.v1.FileMutationResponse> {
|
||||
return this.client.request<Kyros.Files.v1.FileMutationResponse>(
|
||||
`/worlds/${worldId}/files/move`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
body: { source, destination },
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async renameFile(
|
||||
worldId: string,
|
||||
path: string,
|
||||
name: string,
|
||||
): Promise<Kyros.Files.v1.FileMutationResponse> {
|
||||
return this.client.request<Kyros.Files.v1.FileMutationResponse>(
|
||||
`/worlds/${worldId}/files/rename`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
body: { path, name },
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,61 @@ export namespace Kyros {
|
||||
}
|
||||
|
||||
export namespace Files {
|
||||
export namespace v1 {
|
||||
export type DescendantType = 'regular' | 'directory' | 'symlink' | 'other'
|
||||
|
||||
export interface CreateDownloadSessionRequest {
|
||||
path: string
|
||||
zipped: boolean
|
||||
}
|
||||
|
||||
export interface DeleteFileRequest {
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface FileListingItem {
|
||||
name: string
|
||||
full_path: string
|
||||
size_bytes: number
|
||||
type: DescendantType
|
||||
mtime: string
|
||||
ctime: string
|
||||
descendants: number
|
||||
}
|
||||
|
||||
export interface FileListingRequest {
|
||||
path: string
|
||||
page: number
|
||||
items_per_page: number
|
||||
}
|
||||
|
||||
export interface FileListingResponse {
|
||||
items: FileListingItem[]
|
||||
page: number
|
||||
items_per_page: number
|
||||
page_total: number
|
||||
items_total: number
|
||||
too_many_descendants: boolean
|
||||
descendants_limit: number
|
||||
digest?: string | null
|
||||
}
|
||||
|
||||
export interface FileMutationResponse {
|
||||
source: string
|
||||
destination: string
|
||||
}
|
||||
|
||||
export interface MoveFileRequest {
|
||||
source: string
|
||||
destination: string
|
||||
}
|
||||
|
||||
export interface RenameFileRequest {
|
||||
path: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export namespace v0 {
|
||||
export interface DirectoryItem {
|
||||
name: string
|
||||
|
||||
+82
-76
@@ -119,38 +119,77 @@ function initializeFileEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
function getWorldId() {
|
||||
if (!worldId.value) {
|
||||
throw new Error('World ID is not available.')
|
||||
}
|
||||
return worldId.value
|
||||
}
|
||||
|
||||
function timestampSeconds(value: string) {
|
||||
return Math.floor(new Date(value).getTime() / 1000)
|
||||
}
|
||||
|
||||
function toFileItem(item: Kyros.Files.v1.FileListingItem): FileItem | null {
|
||||
if (item.type === 'other') return null
|
||||
|
||||
return {
|
||||
name: item.name,
|
||||
type: item.type === 'regular' ? 'file' : item.type,
|
||||
path: item.full_path,
|
||||
modified: timestampSeconds(item.mtime),
|
||||
created: timestampSeconds(item.ctime),
|
||||
...(item.type === 'directory' ? { count: item.descendants } : { size: item.size_bytes }),
|
||||
}
|
||||
}
|
||||
|
||||
// Directory listing query
|
||||
const {
|
||||
data: directoryData,
|
||||
isLoading,
|
||||
error: loadError,
|
||||
} = useQuery({
|
||||
queryKey: computed(() => ['files', serverId, currentPath.value]),
|
||||
queryKey: computed(() => ['files', 'v1', worldId.value, currentPath.value]),
|
||||
queryFn: async () => {
|
||||
return client.kyros.files_v0.listDirectory(currentPath.value, 1, 2000)
|
||||
return client.kyros.files_v1.listDescendants(getWorldId(), currentPath.value, 1, 200)
|
||||
},
|
||||
enabled: computed(() => !!worldId.value),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
function isVisibleFileItem(item: Kyros.Files.v0.DirectoryItem) {
|
||||
function isVisibleFileItem(item: FileItem) {
|
||||
return !item.path.split('/').includes('.modrinth-staged')
|
||||
}
|
||||
|
||||
const items = computed<FileItem[]>(() =>
|
||||
(directoryData.value?.items ?? []).filter(isVisibleFileItem),
|
||||
(directoryData.value?.items ?? [])
|
||||
.map(toFileItem)
|
||||
.filter((item): item is FileItem => !!item)
|
||||
.filter(isVisibleFileItem),
|
||||
)
|
||||
|
||||
const filesReadyPending = useReadyState({ isLoading, data: directoryData })
|
||||
|
||||
// Prefetching
|
||||
function prefetchDirectory(path: string) {
|
||||
const id = worldId.value
|
||||
if (!id) return
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['files', serverId, path],
|
||||
queryKey: ['files', 'v1', id, path],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await client.kyros.files_v0.listDirectory(path, 1, 2000)
|
||||
return await client.kyros.files_v1.listDescendants(id, path, 1, 200)
|
||||
} catch {
|
||||
return { items: [], total: 0, current: 1 }
|
||||
return {
|
||||
items: [],
|
||||
page: 1,
|
||||
items_per_page: 200,
|
||||
page_total: 0,
|
||||
items_total: 0,
|
||||
too_many_descendants: false,
|
||||
descendants_limit: 0,
|
||||
}
|
||||
}
|
||||
},
|
||||
staleTime: 30_000,
|
||||
@@ -158,11 +197,14 @@ function prefetchDirectory(path: string) {
|
||||
}
|
||||
|
||||
function prefetchFile(path: string) {
|
||||
const id = worldId.value
|
||||
if (!id) return
|
||||
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['file-content', serverId, path],
|
||||
queryKey: ['file-content', id, path],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const blob = await client.kyros.files_v0.downloadFile(path)
|
||||
const blob = await client.kyros.files_v1.downloadRawFileContents(id, path)
|
||||
return await blob.text()
|
||||
} catch {
|
||||
return null
|
||||
@@ -173,24 +215,24 @@ function prefetchFile(path: string) {
|
||||
}
|
||||
|
||||
function getQueryKey() {
|
||||
return ['files', serverId, currentPath.value]
|
||||
return ['files', 'v1', worldId.value, currentPath.value]
|
||||
}
|
||||
|
||||
function refreshList() {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
queryClient.invalidateQueries({ queryKey: ['files', 'v1', worldId.value] })
|
||||
}
|
||||
|
||||
// Mutations
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: ({ path, recursive }: { path: string; recursive: boolean }) =>
|
||||
client.kyros.files_v0.deleteFileOrFolder(path, recursive),
|
||||
mutationFn: ({ path }: { path: string; recursive: boolean }) =>
|
||||
client.kyros.files_v1.deleteFile(getWorldId(), path),
|
||||
onMutate: async ({ path }) => {
|
||||
const queryKey = getQueryKey()
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previous = queryClient.getQueryData(queryKey)
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v0.DirectoryResponse | undefined) => {
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v1.FileListingResponse | undefined) => {
|
||||
if (!old) return old
|
||||
return { ...old, items: old.items.filter((item) => item.path !== path) }
|
||||
return { ...old, items: old.items.filter((item) => item.full_path !== path) }
|
||||
})
|
||||
return { previous }
|
||||
},
|
||||
@@ -210,27 +252,27 @@ const deleteMutation = useMutation({
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
refreshList()
|
||||
},
|
||||
})
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: ({ path, newName }: { path: string; newName: string }) =>
|
||||
client.kyros.files_v0.renameFileOrFolder(path, newName),
|
||||
client.kyros.files_v1.renameFile(getWorldId(), path, newName),
|
||||
onMutate: async ({ path, newName }) => {
|
||||
const queryKey = getQueryKey()
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previous = queryClient.getQueryData(queryKey)
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v0.DirectoryResponse | undefined) => {
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v1.FileListingResponse | undefined) => {
|
||||
if (!old) return old
|
||||
return {
|
||||
...old,
|
||||
items: old.items.map((item) =>
|
||||
item.path === path
|
||||
item.full_path === path
|
||||
? {
|
||||
...item,
|
||||
name: newName,
|
||||
path: item.path.replace(/[^/]+$/, newName),
|
||||
full_path: item.full_path.replace(/[^/]+$/, newName),
|
||||
}
|
||||
: item,
|
||||
),
|
||||
@@ -250,20 +292,20 @@ const renameMutation = useMutation({
|
||||
addNotification({ title: 'Renamed', text: `Renamed to ${newName}`, type: 'success' })
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
refreshList()
|
||||
},
|
||||
})
|
||||
|
||||
const moveMutation = useMutation({
|
||||
mutationFn: ({ source, destination }: { source: string; destination: string }) =>
|
||||
client.kyros.files_v0.moveFileOrFolder(source, destination),
|
||||
client.kyros.files_v1.moveFile(getWorldId(), source, destination),
|
||||
onMutate: async ({ source }) => {
|
||||
const queryKey = getQueryKey()
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previous = queryClient.getQueryData(queryKey)
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v0.DirectoryResponse | undefined) => {
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v1.FileListingResponse | undefined) => {
|
||||
if (!old) return old
|
||||
return { ...old, items: old.items.filter((item) => item.path !== source) }
|
||||
return { ...old, items: old.items.filter((item) => item.full_path !== source) }
|
||||
})
|
||||
return { previous }
|
||||
},
|
||||
@@ -279,86 +321,50 @@ const moveMutation = useMutation({
|
||||
addNotification({ title: 'Moved', text: `Moved to ${destination}`, type: 'success' })
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
refreshList()
|
||||
},
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: ({ path, type }: { path: string; type: 'file' | 'directory' }) =>
|
||||
client.kyros.files_v0.createFileOrFolder(path, type),
|
||||
onMutate: async ({ path, type }) => {
|
||||
const queryKey = getQueryKey()
|
||||
await queryClient.cancelQueries({ queryKey })
|
||||
const previous = queryClient.getQueryData(queryKey)
|
||||
const name = path.split('/').pop()!
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const newItem: Kyros.Files.v0.DirectoryItem = {
|
||||
name,
|
||||
path,
|
||||
type,
|
||||
modified: now,
|
||||
created: now,
|
||||
...(type === 'directory' ? { count: 0 } : { size: 0 }),
|
||||
}
|
||||
queryClient.setQueryData(queryKey, (old: Kyros.Files.v0.DirectoryResponse | undefined) => {
|
||||
if (!old) return old
|
||||
return { ...old, items: [newItem, ...old.items] }
|
||||
})
|
||||
return { previous }
|
||||
mutationFn: (_vars: { path: string; type: 'file' | 'directory' }) => {
|
||||
// await client.kyros.files_v0.createFileOrFolder(path, type)
|
||||
throw new Error('Creating files or folders is not supported by the v1 world-scoped files API.')
|
||||
},
|
||||
onError: (err: Error, _vars, context) => {
|
||||
queryClient.setQueryData(getQueryKey(), context?.previous)
|
||||
onError: (err: Error) => {
|
||||
addNotification({
|
||||
title: formatMessage(commonMessages.createFailedLabel),
|
||||
text: err.message,
|
||||
type: 'error',
|
||||
})
|
||||
},
|
||||
onSuccess: (_, { path, type }) => {
|
||||
const name = path.split('/').pop()
|
||||
addNotification({
|
||||
title: `${type === 'directory' ? 'Folder' : 'File'} created`,
|
||||
text: `Created ${name}`,
|
||||
type: 'success',
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['files', serverId] })
|
||||
},
|
||||
})
|
||||
|
||||
// Extraction
|
||||
async function extractFile(path: string, override: boolean, dry: boolean) {
|
||||
if (fileWriteDisabled.value) return
|
||||
if (dry) {
|
||||
return await client.kyros.files_v0.extractFile(path, override, true)
|
||||
}
|
||||
await client.kyros.files_v0.extractFile(path, override, false)
|
||||
}
|
||||
|
||||
// File I/O
|
||||
async function readFile(path: string): Promise<string> {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
const cachedContent = queryClient.getQueryData<string>(['file-content', serverId, normalizedPath])
|
||||
const id = getWorldId()
|
||||
const cachedContent = queryClient.getQueryData<string>(['file-content', id, normalizedPath])
|
||||
if (cachedContent) return cachedContent
|
||||
const blob = await client.kyros.files_v0.downloadFile(normalizedPath)
|
||||
const blob = await client.kyros.files_v1.downloadRawFileContents(id, normalizedPath)
|
||||
return await blob.text()
|
||||
}
|
||||
|
||||
async function readFileAsBlob(path: string): Promise<Blob> {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`
|
||||
return await client.kyros.files_v0.downloadFile(normalizedPath)
|
||||
return await client.kyros.files_v1.downloadRawFileContents(getWorldId(), normalizedPath)
|
||||
}
|
||||
|
||||
async function writeFile(path: string, content: string): Promise<void> {
|
||||
if (fileWriteDisabled.value) return
|
||||
await client.kyros.files_v0.updateFile(path, content)
|
||||
queryClient.invalidateQueries({ queryKey: ['servers', 'detail', serverId] })
|
||||
void path
|
||||
void content
|
||||
// await client.kyros.files_v0.updateFile(path, content)
|
||||
throw new Error('Updating file contents is not supported by the v1 world-scoped files API.')
|
||||
}
|
||||
|
||||
async function downloadFile(path: string, fileName: string): Promise<void> {
|
||||
try {
|
||||
const fileData = await client.kyros.files_v0.downloadFile(path)
|
||||
const fileData = await client.kyros.files_v1.downloadRawFileContents(getWorldId(), path)
|
||||
if (fileData) {
|
||||
const blob = new Blob([fileData], { type: 'application/octet-stream' })
|
||||
const link = document.createElement('a')
|
||||
@@ -460,10 +466,10 @@ provideFileManager({
|
||||
isBusy: fileWriteDisabled,
|
||||
busyTooltip: fileWriteDisabledTooltip,
|
||||
busyWarning,
|
||||
extractFile,
|
||||
// extractFile: async (path, override, dry) => client.kyros.files_v0.extractFile(path, override, dry),
|
||||
prefetchDirectory,
|
||||
prefetchFile,
|
||||
showInstallFromUrl: true,
|
||||
showInstallFromUrl: false,
|
||||
canRestart: canUsePowerActions.value,
|
||||
restartServer,
|
||||
canShareToMclogs: true,
|
||||
|
||||
Reference in New Issue
Block a user