mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 09:04:55 +00:00
feat: content upload session impl (#6992)
* feat: content upload session impl * fix: fixes * fix: prepr
This commit is contained in:
@@ -7,6 +7,10 @@ export type UploadSessionFile = {
|
||||
filename: string
|
||||
}
|
||||
|
||||
function getUploadSessionPath(scope: Kyros.UploadSessions.v1.Scope, worldId: string) {
|
||||
return `/worlds/${worldId}/${scope}/upload-session`
|
||||
}
|
||||
|
||||
export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_upload_sessions_v1'
|
||||
@@ -17,7 +21,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
getUploadSessionPath(scope, worldId),
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -32,7 +36,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.GetUploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.GetUploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
getUploadSessionPath(scope, worldId),
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -58,7 +62,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
}
|
||||
|
||||
return this.client.upload<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/files`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}/files`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -76,7 +80,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/finalize`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}/finalize`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
@@ -92,7 +96,7 @@ export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}`,
|
||||
`${getUploadSessionPath(scope, worldId)}/${uploadId}`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
|
||||
@@ -68,6 +68,7 @@ useQuery({
|
||||
from_modpack: false,
|
||||
}),
|
||||
enabled: computed(() => !!worldId.value),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const serverSettingsTabComponentMap = {
|
||||
|
||||
@@ -199,7 +199,6 @@ export function useServerPanelSync(options: UseServerPanelSyncOptions) {
|
||||
}
|
||||
: current,
|
||||
)
|
||||
void queryClient.invalidateQueries({ queryKey: contentListKey(serverId) })
|
||||
}
|
||||
|
||||
function handleWorldContentBaseUpdate(
|
||||
|
||||
@@ -31,6 +31,11 @@ export interface BrowseInstallTarget {
|
||||
loader?: string | null
|
||||
}
|
||||
|
||||
export interface BrowseResolvedInstallContent {
|
||||
projectId: string
|
||||
versionId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal project shape needed by shared install resolution.
|
||||
*/
|
||||
@@ -59,6 +64,7 @@ export interface BrowseInstallPlan<TProject extends BrowseInstallProject = Brows
|
||||
contentType: BrowseInstallContentType
|
||||
preferences: BrowseInstallPreferences
|
||||
source: BrowseInstallPlanSource
|
||||
resolvedContent?: BrowseResolvedInstallContent[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,6 +341,15 @@ export interface FlushStoredServerAddonInstallQueueOptions<TProject extends Brow
|
||||
onQueueChange?: (plans: Map<string, BrowseInstallPlan<TProject>>) => void
|
||||
}
|
||||
|
||||
export interface ResolveServerAddonInstallPlansOptions<TProject extends BrowseInstallProject> {
|
||||
plans: readonly BrowseInstallPlan<TProject>[]
|
||||
existingProjectIds: Iterable<string>
|
||||
resolvePlan: (
|
||||
plan: BrowseInstallPlan<TProject>,
|
||||
existingProjectIds: string[],
|
||||
) => Promise<BrowseResolvedInstallContent[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a queue flush. Failed plans are also written back to the queue.
|
||||
*/
|
||||
@@ -646,6 +661,62 @@ export function getStoredServerAddonInstallQueue<
|
||||
return addonPlans
|
||||
}
|
||||
|
||||
export function getServerAddonInstallPlanProjectIds<TProject extends BrowseInstallProject>(
|
||||
plans: Iterable<BrowseInstallPlan<TProject>>,
|
||||
) {
|
||||
const projectIds = new Set<string>()
|
||||
for (const plan of plans) {
|
||||
projectIds.add(plan.projectId)
|
||||
for (const item of plan.resolvedContent ?? []) {
|
||||
projectIds.add(item.projectId)
|
||||
}
|
||||
}
|
||||
return projectIds
|
||||
}
|
||||
|
||||
export async function resolveServerAddonInstallPlans<TProject extends BrowseInstallProject>({
|
||||
plans,
|
||||
existingProjectIds,
|
||||
resolvePlan,
|
||||
}: ResolveServerAddonInstallPlansOptions<TProject>) {
|
||||
const installedProjectIds = new Set(existingProjectIds)
|
||||
const resolutionExistingProjectIds = Array.from(installedProjectIds)
|
||||
const resolvedPlans = await Promise.all(
|
||||
plans.map(
|
||||
async (plan) =>
|
||||
plan.resolvedContent ?? (await resolvePlan(plan, resolutionExistingProjectIds)),
|
||||
),
|
||||
)
|
||||
const explicitProjectIds = new Set(plans.map((plan) => plan.projectId))
|
||||
const addons = new Map<string, { project_id: string; version_id: string }>()
|
||||
|
||||
for (const plan of plans) {
|
||||
if (installedProjectIds.has(plan.projectId)) continue
|
||||
addons.set(plan.projectId, {
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
})
|
||||
}
|
||||
|
||||
for (const content of resolvedPlans) {
|
||||
for (const item of content) {
|
||||
if (
|
||||
installedProjectIds.has(item.projectId) ||
|
||||
explicitProjectIds.has(item.projectId) ||
|
||||
addons.has(item.projectId)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
addons.set(item.projectId, {
|
||||
project_id: item.projectId,
|
||||
version_id: item.versionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(addons.values())
|
||||
}
|
||||
|
||||
export async function flushStoredServerAddonInstallQueue<TProject extends BrowseInstallProject>({
|
||||
serverId,
|
||||
worldId,
|
||||
@@ -869,7 +940,22 @@ function isStoredBrowseInstallPlan(
|
||||
typeof record.versionId === 'string' &&
|
||||
isStoredBrowseInstallContentType(record.contentType) &&
|
||||
isStoredBrowseInstallPreferences(record.preferences) &&
|
||||
(record.source === 'filtered' || record.source === 'target')
|
||||
(record.source === 'filtered' || record.source === 'target') &&
|
||||
isOptionalStoredResolvedContent(record.resolvedContent)
|
||||
)
|
||||
}
|
||||
|
||||
function isOptionalStoredResolvedContent(value: unknown) {
|
||||
return (
|
||||
value === undefined ||
|
||||
(Array.isArray(value) &&
|
||||
value.every(
|
||||
(item) =>
|
||||
!!item &&
|
||||
typeof item === 'object' &&
|
||||
typeof (item as Record<string, unknown>).projectId === 'string' &&
|
||||
typeof (item as Record<string, unknown>).versionId === 'string',
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,7 @@ const addonsQuery = useQuery({
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const modpack = computed(() => addonsQuery.data.value?.modpack ?? null)
|
||||
@@ -411,6 +412,9 @@ async function uploadLocalModpackWithSoftOverride() {
|
||||
|
||||
provideInstallationSettings({
|
||||
closeSettings: serverSettings.closeModal,
|
||||
afterSave: async () => {
|
||||
serverSettings.closeModal?.()
|
||||
},
|
||||
onGameVersionHover: handleGameVersionHover,
|
||||
loading: computed(() => !server.value || addonsQuery.isLoading.value),
|
||||
installationInfo: computed(() => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
flushStoredServerAddonInstallQueue,
|
||||
getStoredServerAddonInstallQueue,
|
||||
getTargetInstallPreferences,
|
||||
resolveServerAddonInstallPlans,
|
||||
} from '../../../shared/browse-tab/composables/install-logic'
|
||||
import ManagedContentModal from '../../../shared/content-tab/components/managed-content-modal/index.vue'
|
||||
import ConfirmModpackUpdateModal from '../../../shared/content-tab/components/modals/ConfirmModpackUpdateModal.vue'
|
||||
@@ -157,7 +158,7 @@ const contentQuery = useQuery({
|
||||
queryFn: () =>
|
||||
client.archon.content_v1.getAddons(serverId, worldId.value!, { from_modpack: false }),
|
||||
enabled: computed(() => worldId.value !== null),
|
||||
staleTime: 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const isModpackContentModalOpen = ref(false)
|
||||
@@ -168,7 +169,7 @@ const modpackContentQuery = useQuery({
|
||||
from_modpack: true,
|
||||
}),
|
||||
enabled: computed(() => worldId.value !== null && !!contentQuery.data.value?.modpack),
|
||||
staleTime: 0,
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const setupActionDisabled = computed(() => !canSetup.value || busyReasons.value.length > 0)
|
||||
@@ -395,37 +396,32 @@ function toResolvePreferences(
|
||||
}
|
||||
|
||||
async function resolveStoredServerAddonPlans(plans: BrowseInstallPlan[]) {
|
||||
const existingProjectIds = getInstalledProjectIds()
|
||||
const resolvedAddons: Array<{ project_id: string; version_id: string }> = []
|
||||
|
||||
for (const plan of plans) {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: server.value?.mc_version,
|
||||
loader: server.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: Array.from(existingProjectIds),
|
||||
})
|
||||
|
||||
for (const item of [resolved.primary, ...resolved.dependencies]) {
|
||||
if (existingProjectIds.has(item.project_id)) continue
|
||||
existingProjectIds.add(item.project_id)
|
||||
resolvedAddons.push({
|
||||
project_id: item.project_id,
|
||||
version_id: item.version_id,
|
||||
return await resolveServerAddonInstallPlans({
|
||||
plans,
|
||||
existingProjectIds: getInstalledProjectIds(),
|
||||
resolvePlan: async (plan, existingProjectIds) => {
|
||||
const target = getTargetInstallPreferences(
|
||||
{
|
||||
gameVersion: server.value?.mc_version,
|
||||
loader: server.value?.loader,
|
||||
},
|
||||
plan.contentType,
|
||||
)
|
||||
const resolved = await client.labrinth.content_v3.resolve({
|
||||
project_id: plan.projectId,
|
||||
version_id: plan.versionId,
|
||||
content_type: plan.contentType as Labrinth.Content.v3.ContentType,
|
||||
selected: toResolvePreferences(plan.preferences),
|
||||
target: toResolvePreferences(target),
|
||||
existing_project_ids: existingProjectIds,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedAddons
|
||||
return [resolved.primary, ...resolved.dependencies].map((item) => ({
|
||||
projectId: item.project_id,
|
||||
versionId: item.version_id,
|
||||
}))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function addonMatchesPendingInstall(
|
||||
|
||||
@@ -1124,8 +1124,13 @@ const handleFilesystemOps = (data: Archon.Websocket.v0.WSFilesystemOpsEvent) =>
|
||||
)
|
||||
}
|
||||
|
||||
let newModInvalidateTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const handleNewMod = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
if (newModInvalidateTimer) clearTimeout(newModInvalidateTimer)
|
||||
newModInvalidateTimer = setTimeout(() => {
|
||||
newModInvalidateTimer = null
|
||||
void queryClient.invalidateQueries({ queryKey: ['content', 'list'] })
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallationResultEvent) => {
|
||||
@@ -1520,6 +1525,10 @@ function initializeServer() {
|
||||
|
||||
const cleanup = () => {
|
||||
isMounted.value = false
|
||||
if (newModInvalidateTimer) {
|
||||
clearTimeout(newModInvalidateTimer)
|
||||
newModInvalidateTimer = null
|
||||
}
|
||||
|
||||
saveWsStateToCache()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user