Skip to content

feat: MOD-300 Servers modpack upload progress feedback #3711

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions apps/frontend/src/components/ui/servers/PlatformMrpackModal.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
<template>
<NewModal ref="mrpackModal" header="Uploading mrpack" @hide="onHide" @show="onShow">
<div class="flex flex-col gap-4 md:w-[600px]">
<Transition
enter-active-class="transition-all duration-300 ease-out"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-20"
leave-active-class="transition-all duration-200 ease-in"
leave-from-class="opacity-100 max-h-20"
leave-to-class="opacity-0 max-h-0"
>
<div v-if="isLoading" class="w-full">
<div class="mb-2 flex justify-between text-sm">
<span class="font-medium text-contrast">Uploading...</span>
<span class="text-secondary">{{ Math.round(uploadProgress) }}%</span>
</div>
<div class="h-2 w-full rounded-full bg-divider">
<div
class="h-2 rounded-full bg-brand transition-all duration-300 ease-out"
:style="{ width: `${uploadProgress}%` }"
></div>
</div>
</div>
</Transition>
<p
v-if="isMrpackModalSecondPhase"
:style="{
Expand Down Expand Up @@ -134,6 +155,7 @@ const hardReset = ref(false);
const isLoading = ref(false);
const loadingServerCheck = ref(false);
const mrpackFile = ref<File | null>(null);
const uploadProgress = ref(0);

const isDangerous = computed(() => hardReset.value);
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value);
Expand All @@ -152,18 +174,30 @@ const handleReinstall = async () => {
return;
}

if (!mrpackFile.value) {
addNotification({
group: "server",
title: "No file selected",
text: "Choose a .mrpack file before installing.",
type: "error",
});
return;
}

isLoading.value = true;
uploadProgress.value = 0;

try {
if (!mrpackFile.value) {
throw new Error("No mrpack file selected");
}
const { onProgress, promise } = props.server.general!.reinstallFromMrpack(
mrpackFile.value,
hardReset.value,
);

const mrpack = new File([mrpackFile.value], mrpackFile.value.name, {
type: mrpackFile.value.type,
});
onProgress(({ progress }) => {
uploadProgress.value = progress;
});

await props.server.general?.reinstallFromMrpack(mrpack, hardReset.value);
try {
await promise;

emit("reinstall", {
loader: "mrpack",
Expand All @@ -185,7 +219,7 @@ const handleReinstall = async () => {
} else {
addNotification({
group: "server",
title: "Reinstall Failed",
title: "Reinstall failed",
text: "An unexpected error occurred while reinstalling. Please try again later.",
type: "error",
});
Expand All @@ -194,7 +228,6 @@ const handleReinstall = async () => {
isLoading.value = false;
}
};

const onShow = () => {
hardReset.value = false;
isMrpackModalSecondPhase.value = false;
Expand Down
84 changes: 59 additions & 25 deletions apps/frontend/src/composables/pyroServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,35 +567,63 @@ const reinstallServer = async (
}
};

const reinstallFromMrpack = async (mrpack: File, hardReset: boolean = false) => {
const reinstallFromMrpack = (mrpack: File, hardReset: boolean = false) => {
const hardResetParam = hardReset ? "true" : "false";
try {
const auth = await PyroFetch<JWTAuth>(
`servers/${internalServerReference.value.serverId}/reinstallFromMrpack`,
);

const formData = new FormData();
formData.append("file", mrpack);
const progressSubject = new EventTarget();

const response = await fetch(
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${auth.token}`,
},
body: formData,
signal: AbortSignal.timeout(30 * 60 * 1000),
},
);
const uploadPromise = (async () => {
try {
const auth = await PyroFetch<JWTAuth>(
`servers/${internalServerReference.value.serverId}/reinstallFromMrpack`,
);

await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();

xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
progressSubject.dispatchEvent(
new CustomEvent("progress", {
detail: {
loaded: e.loaded,
total: e.total,
progress: (e.loaded / e.total) * 100,
},
}),
);
}
});

xhr.onload = () =>
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`));

if (!response.ok) {
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
xhr.onerror = () => reject(new Error("[pyroservers] .mrpack upload failed"));
xhr.onabort = () => reject(new Error("[pyroservers] .mrpack upload cancelled"));
xhr.ontimeout = () => reject(new Error("[pyroservers] .mrpack upload timed out"));
xhr.timeout = 30 * 60 * 1000;

xhr.open("POST", `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`);
xhr.setRequestHeader("Authorization", `Bearer ${auth.token}`);

const formData = new FormData();
formData.append("file", mrpack);
xhr.send(formData);
});
} catch (err) {
console.error("Error reinstalling from mrpack:", err);
throw err;
}
} catch (error) {
console.error("Error reinstalling from mrpack:", error);
throw error;
}
})();

return {
promise: uploadPromise,
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
progressSubject.addEventListener("progress", ((e: CustomEvent) =>
cb(e.detail)) as EventListener),
};
};

const suspendServer = async (status: boolean) => {
Expand Down Expand Up @@ -1428,7 +1456,13 @@ type GeneralFunctions = {
* @param mrpack - The mrpack file.
* @param hardReset - Whether to perform a hard reset.
*/
reinstallFromMrpack: (mrpack: File, hardReset?: boolean) => Promise<void>;
reinstallFromMrpack: (
mrpack: File,
hardReset?: boolean,
) => {
promise: Promise<void>;
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void;
};

/**
* Suspends or resumes the server.
Expand Down
Loading