Fix forever installing (#3135)

* Rough draft for fix for Mojang servers being down causing infinite installation

* Add "pack installed" install step

* Allow repairing an instance from Library to recover pack contents

* Allow repair from instance page

* Deduplicate repair code

* Fix lint

* Fix lint (for real this time)

---------

Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Josiah Glosson 2025-01-11 17:27:47 -06:00 committed by GitHub
parent abd679d716
commit 227386bb0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 170 additions and 74 deletions

View File

@ -1,22 +1,22 @@
<script setup> <script setup>
import { computed, ref, onMounted, watch, onUnmounted } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { RouterView, useRouter, useRoute } from 'vue-router' import { RouterView, useRoute, useRouter } from 'vue-router'
import { import {
ArrowBigUpDashIcon, ArrowBigUpDashIcon,
LogInIcon, CompassIcon,
DownloadIcon,
HomeIcon, HomeIcon,
LeftArrowIcon,
LibraryIcon, LibraryIcon,
LogInIcon,
LogOutIcon,
MaximizeIcon,
MinimizeIcon,
PlusIcon, PlusIcon,
RestoreIcon,
RightArrowIcon,
SettingsIcon, SettingsIcon,
XIcon, XIcon,
DownloadIcon,
CompassIcon,
MinimizeIcon,
MaximizeIcon,
RestoreIcon,
LogOutIcon,
RightArrowIcon,
LeftArrowIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui' import { Avatar, Button, ButtonStyled, Notifications, OverflowMenu } from '@modrinth/ui'
import { useLoading, useTheming } from '@/store/state' import { useLoading, useTheming } from '@/store/state'
@ -32,12 +32,12 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js' import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { command_listener, warning_listener } from '@/helpers/events.js'
import { type } from '@tauri-apps/plugin-os' import { type } from '@tauri-apps/plugin-os'
import { isDev, getOS, restartApp } from '@/helpers/utils.js' import { getOS, isDev, restartApp } from '@/helpers/utils.js'
import { initAnalytics, debugAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
import URLConfirmModal from '@/components/ui/URLConfirmModal.vue' import URLConfirmModal from '@/components/ui/URLConfirmModal.vue'
import { install_from_file } from './helpers/pack' import { create_profile_and_install_from_file } from './helpers/pack'
import { useError } from '@/store/error.js' import { useError } from '@/store/error.js'
import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useCheckDisableMouseover } from '@/composables/macCssFix.js'
import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue' import ModInstallModal from '@/components/ui/install_flow/ModInstallModal.vue'
@ -51,7 +51,7 @@ import { renderString } from '@modrinth/utils'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { check } from '@tauri-apps/plugin-updater' import { check } from '@tauri-apps/plugin-updater'
import NavButton from '@/components/ui/NavButton.vue' import NavButton from '@/components/ui/NavButton.vue'
import { get as getCreds, logout, login } from '@/helpers/mr_auth.js' import { get as getCreds, login, logout } from '@/helpers/mr_auth.js'
import { get_user } from '@/helpers/cache.js' import { get_user } from '@/helpers/cache.js'
import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue' import AppSettingsModal from '@/components/ui/modal/AppSettingsModal.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -296,7 +296,7 @@ async function handleCommand(e) {
if (e.event === 'RunMRPack') { if (e.event === 'RunMRPack') {
// RunMRPack should directly install a local mrpack given a path // RunMRPack should directly install a local mrpack given a path
if (e.path.endsWith('.mrpack')) { if (e.path.endsWith('.mrpack')) {
await install_from_file(e.path).catch(handleError) await create_profile_and_install_from_file(e.path).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })

View File

@ -1,10 +1,17 @@
<script setup> <script setup>
import { onUnmounted, ref, computed, onMounted } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { SpinnerIcon, GameIcon, TimerIcon, StopCircleIcon, PlayIcon } from '@modrinth/assets' import {
import { ButtonStyled, Avatar } from '@modrinth/ui' DownloadIcon,
GameIcon,
PlayIcon,
SpinnerIcon,
StopCircleIcon,
TimerIcon,
} from '@modrinth/assets'
import { Avatar, ButtonStyled } from '@modrinth/ui'
import { convertFileSrc } from '@tauri-apps/api/core' import { convertFileSrc } from '@tauri-apps/api/core'
import { kill, run } from '@/helpers/profile' import { finish_install, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process' import { get_by_profile_path } from '@/helpers/process'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'
@ -42,7 +49,8 @@ const modLoading = computed(
currentEvent.value === 'installing' || currentEvent.value === 'installing' ||
(currentEvent.value === 'launched' && !playing.value), (currentEvent.value === 'launched' && !playing.value),
) )
const installing = computed(() => props.instance.install_stage !== 'installed') const installing = computed(() => props.instance.install_stage.includes('installing'))
const installed = computed(() => props.instance.install_stage === 'installed')
const router = useRouter() const router = useRouter()
@ -84,6 +92,12 @@ const stop = async (e, context) => {
}) })
} }
const repair = async (e) => {
e?.stopPropagation()
await finish_install(props.instance)
}
const openFolder = async () => { const openFolder = async () => {
await showProfileInFolder(props.instance.path) await showProfileInFolder(props.instance.path)
} }
@ -195,6 +209,15 @@ onUnmounted(() => unlisten())
class="animate-spin w-8 h-8" class="animate-spin w-8 h-8"
tabindex="-1" tabindex="-1"
/> />
<ButtonStyled v-else-if="!installed" size="large" color="brand" circular>
<button
v-tooltip="'Repair'"
class="transition-all scale-75 group-hover:scale-100 group-focus-within:scale-100 origin-bottom opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 card-shadow"
@click="(e) => repair(e)"
>
<DownloadIcon />
</button>
</ButtonStyled>
<ButtonStyled v-else size="large" color="brand" circular> <ButtonStyled v-else size="large" color="brand" circular>
<button <button
v-tooltip="'Play'" v-tooltip="'Play'"

View File

@ -199,16 +199,16 @@
<script setup> <script setup>
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { import {
PlusIcon,
UploadIcon,
XIcon,
CodeIcon, CodeIcon,
FolderOpenIcon, FolderOpenIcon,
InfoIcon,
FolderSearchIcon, FolderSearchIcon,
InfoIcon,
PlusIcon,
UpdatedIcon, UpdatedIcon,
UploadIcon,
XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { Avatar, Button, Chips, Checkbox } from '@modrinth/ui' import { Avatar, Button, Checkbox, Chips } from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef } from 'vue' import { computed, onUnmounted, ref, shallowRef } from 'vue'
import { get_loaders } from '@/helpers/tags' import { get_loaders } from '@/helpers/tags'
import { create } from '@/helpers/profile' import { create } from '@/helpers/profile'
@ -218,7 +218,7 @@ import { get_game_versions, get_loader_versions } from '@/helpers/metadata'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { install_from_file } from '@/helpers/pack.js' import { create_profile_and_install_from_file } from '@/helpers/pack.js'
import { import {
get_default_launcher_path, get_default_launcher_path,
get_importable_instances, get_importable_instances,
@ -263,7 +263,7 @@ defineExpose({
hide() hide()
const { paths } = event.payload const { paths } = event.payload
if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) { if (paths && paths.length > 0 && paths[0].endsWith('.mrpack')) {
await install_from_file(paths[0]).catch(handleError) await create_profile_and_install_from_file(paths[0]).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileDrop', source: 'CreationModalFileDrop',
}) })
@ -419,7 +419,7 @@ const openFile = async () => {
const newProject = await open({ multiple: false }) const newProject = await open({ multiple: false })
if (!newProject) return if (!newProject) return
hide() hide()
await install_from_file(newProject.path ?? newProject).catch(handleError) await create_profile_and_install_from_file(newProject.path ?? newProject).catch(handleError)
trackEvent('InstanceCreate', { trackEvent('InstanceCreate', {
source: 'CreationModalFileOpen', source: 'CreationModalFileOpen',

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { XIcon, DownloadIcon } from '@modrinth/assets' import { DownloadIcon, XIcon } from '@modrinth/assets'
import { Button } from '@modrinth/ui' import { Button } from '@modrinth/ui'
import { install as pack_install } from '@/helpers/pack' import { create_profile_and_install as pack_install } from '@/helpers/pack'
import { ref } from 'vue' import { ref } from 'vue'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { handleError } from '@/store/state.js' import { handleError } from '@/store/state.js'

View File

@ -7,7 +7,7 @@ import { invoke } from '@tauri-apps/api/core'
import { create } from './profile' import { create } from './profile'
// Installs pack from a version ID // Installs pack from a version ID
export async function install(projectId, versionId, packTitle, iconUrl) { export async function create_profile_and_install(projectId, versionId, packTitle, iconUrl) {
const location = { const location = {
type: 'fromVersionId', type: 'fromVersionId',
project_id: projectId, project_id: projectId,
@ -28,8 +28,18 @@ export async function install(projectId, versionId, packTitle, iconUrl) {
return await invoke('plugin:pack|pack_install', { location, profile }) return await invoke('plugin:pack|pack_install', { location, profile })
} }
export async function install_to_existing_profile(projectId, versionId, title, profilePath) {
const location = {
type: 'fromVersionId',
project_id: projectId,
version_id: versionId,
title,
}
return await invoke('plugin:pack|pack_install', { location, profile: profilePath })
}
// Installs pack from a path // Installs pack from a path
export async function install_from_file(path) { export async function create_profile_and_install_from_file(path) {
const location = { const location = {
type: 'fromFile', type: 'fromFile',
path: path, path: path,

View File

@ -4,6 +4,8 @@
* and deserialized into a usable JS object. * and deserialized into a usable JS object.
*/ */
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import { install_to_existing_profile } from '@/helpers/pack.js'
import { handleError } from '@/store/notifications.js'
/// Add instance /// Add instance
/* /*
@ -186,3 +188,17 @@ export async function edit(path, editProfile) {
export async function edit_icon(path, iconPath) { export async function edit_icon(path, iconPath) {
return await invoke('plugin:profile|profile_edit_icon', { path, iconPath }) return await invoke('plugin:profile|profile_edit_icon', { path, iconPath })
} }
export async function finish_install(instance) {
if (instance.install_stage !== 'pack_installed') {
let linkedData = instance.linked_data
await install_to_existing_profile(
linkedData.project_id,
linkedData.version_id,
instance.name,
instance.path,
).catch(handleError)
} else {
await install(instance.path, false).catch(handleError)
}
}

View File

@ -32,7 +32,12 @@ type GameInstance = {
hooks: Hooks hooks: Hooks
} }
type InstallStage = 'installed' | 'installing' | 'pack_installing' | 'not_installed' type InstallStage =
| 'installed'
| 'minecraft_installing'
| 'pack_installed'
| 'pack_installing'
| 'not_installed'
type LinkedData = { type LinkedData = {
project_id: ModrinthId project_id: ModrinthId

View File

@ -30,9 +30,23 @@
</template> </template>
<template #actions> <template #actions>
<div class="flex gap-2"> <div class="flex gap-2">
<ButtonStyled v-if="instance.install_stage !== 'installed'" color="brand" size="large"> <ButtonStyled
v-if="instance.install_stage.includes('installing')"
color="brand"
size="large"
>
<button disabled>Installing...</button> <button disabled>Installing...</button>
</ButtonStyled> </ButtonStyled>
<ButtonStyled
v-else-if="instance.install_stage !== 'installed'"
color="brand"
size="large"
>
<button @click="repairInstance()">
<DownloadIcon />
Repair
</button>
</ButtonStyled>
<ButtonStyled v-else-if="playing === true" color="red" size="large"> <ButtonStyled v-else-if="playing === true" color="red" size="large">
<button @click="stopInstance('InstancePage')"> <button @click="stopInstance('InstancePage')">
<StopCircleIcon /> <StopCircleIcon />
@ -137,38 +151,39 @@
<script setup> <script setup>
import { import {
Avatar, Avatar,
ContentPageHeader,
ButtonStyled, ButtonStyled,
OverflowMenu, ContentPageHeader,
LoadingIndicator, LoadingIndicator,
OverflowMenu,
} from '@modrinth/ui' } from '@modrinth/ui'
import { import {
UserPlusIcon,
ServerIcon,
PackageIcon,
SettingsIcon,
PlayIcon,
StopCircleIcon,
EditIcon,
FolderOpenIcon,
ClipboardCopyIcon,
PlusIcon,
ExternalIcon,
HashIcon,
GlobeIcon,
EyeIcon,
XIcon,
CheckCircleIcon, CheckCircleIcon,
UpdatedIcon, ClipboardCopyIcon,
MoreVerticalIcon, DownloadIcon,
EditIcon,
ExternalIcon,
EyeIcon,
FolderOpenIcon,
GameIcon, GameIcon,
GlobeIcon,
HashIcon,
MoreVerticalIcon,
PackageIcon,
PlayIcon,
PlusIcon,
ServerIcon,
SettingsIcon,
StopCircleIcon,
TimerIcon, TimerIcon,
UpdatedIcon,
UserPlusIcon,
XIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { get, get_full_path, kill, run } from '@/helpers/profile' import { finish_install, get, get_full_path, kill, run } from '@/helpers/profile'
import { get_by_profile_path } from '@/helpers/process' import { get_by_profile_path } from '@/helpers/process'
import { process_listener, profile_listener } from '@/helpers/events' import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted, computed, watch } from 'vue' import { computed, onUnmounted, ref, watch } from 'vue'
import { handleError, useBreadcrumbs, useLoading } from '@/store/state' import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showProfileInFolder } from '@/helpers/utils.js' import { showProfileInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
@ -294,6 +309,10 @@ const stopInstance = async (context) => {
}) })
} }
const repairInstance = async () => {
await finish_install(instance.value)
}
const handleRightClick = (event) => { const handleRightClick = (event) => {
const baseOptions = [ const baseOptions = [
{ name: 'add_content' }, { name: 'add_content' },

View File

@ -2,14 +2,14 @@ import { defineStore } from 'pinia'
import { import {
add_project_from_version, add_project_from_version,
check_installed, check_installed,
list,
get, get,
get_projects, get_projects,
list,
remove_project, remove_project,
} from '@/helpers/profile.js' } from '@/helpers/profile.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_project, get_version_many } from '@/helpers/cache.js' import { get_project, get_version_many } from '@/helpers/cache.js'
import { install as packInstall } from '@/helpers/pack.js' import { create_profile_and_install as packInstall } from '@/helpers/pack.js'
import { trackEvent } from '@/helpers/analytics.js' import { trackEvent } from '@/helpers/analytics.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'

View File

@ -9,7 +9,7 @@ use crate::pack::install_from::{
}; };
use crate::state::{ use crate::state::{
CacheBehaviour, CachedEntry, Credentials, JavaVersion, ProcessMetadata, CacheBehaviour, CachedEntry, Credentials, JavaVersion, ProcessMetadata,
ProfileFile, ProjectType, SideType, ProfileFile, ProfileInstallStage, ProjectType, SideType,
}; };
use crate::event::{emit::emit_profile, ProfilePayloadType}; use crate::event::{emit::emit_profile, ProfilePayloadType};
@ -225,7 +225,18 @@ pub async fn list() -> crate::Result<Vec<Profile>> {
#[tracing::instrument] #[tracing::instrument]
pub async fn install(path: &str, force: bool) -> crate::Result<()> { pub async fn install(path: &str, force: bool) -> crate::Result<()> {
if let Some(profile) = get(path).await? { if let Some(profile) = get(path).await? {
crate::launcher::install_minecraft(&profile, None, force).await?; let result =
crate::launcher::install_minecraft(&profile, None, force).await;
if result.is_err()
&& profile.install_stage != ProfileInstallStage::Installed
{
edit(path, |prof| {
prof.install_stage = ProfileInstallStage::NotInstalled;
async { Ok(()) }
})
.await?;
}
result?;
} else { } else {
return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string()) return Err(crate::ErrorKind::UnmanagedProfileError(path.to_string())
.as_error()); .as_error());

View File

@ -111,7 +111,7 @@ async fn replace_managed_modrinth(
ignore_lock: bool, ignore_lock: bool,
) -> crate::Result<()> { ) -> crate::Result<()> {
crate::profile::edit(profile_path, |profile| { crate::profile::edit(profile_path, |profile| {
profile.install_stage = ProfileInstallStage::Installing; profile.install_stage = ProfileInstallStage::MinecraftInstalling;
async { Ok(()) } async { Ok(()) }
}) })
.await?; .await?;

View File

@ -7,11 +7,7 @@ use crate::state::{
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage, Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
}; };
use crate::util::io; use crate::util::io;
use crate::{ use crate::{process, state as st, State};
process,
state::{self as st},
State,
};
use chrono::Utc; use chrono::Utc;
use daedalus as d; use daedalus as d;
use daedalus::minecraft::{RuleAction, VersionInfo}; use daedalus::minecraft::{RuleAction, VersionInfo};
@ -199,7 +195,7 @@ pub async fn install_minecraft(
.await?; .await?;
crate::api::profile::edit(&profile.path, |prof| { crate::api::profile::edit(&profile.path, |prof| {
prof.install_stage = ProfileInstallStage::Installing; prof.install_stage = ProfileInstallStage::MinecraftInstalling;
async { Ok(()) } async { Ok(()) }
}) })
@ -431,7 +427,7 @@ pub async fn launch_minecraft(
profile: &Profile, profile: &Profile,
) -> crate::Result<ProcessMetadata> { ) -> crate::Result<ProcessMetadata> {
if profile.install_stage == ProfileInstallStage::PackInstalling if profile.install_stage == ProfileInstallStage::PackInstalling
|| profile.install_stage == ProfileInstallStage::Installing || profile.install_stage == ProfileInstallStage::MinecraftInstalling
{ {
return Err(crate::ErrorKind::LauncherError( return Err(crate::ErrorKind::LauncherError(
"Profile is still installing".to_string(), "Profile is still installing".to_string(),

View File

@ -308,7 +308,7 @@ where
ProfileInstallStage::Installed ProfileInstallStage::Installed
} }
LegacyProfileInstallStage::Installing => { LegacyProfileInstallStage::Installing => {
ProfileInstallStage::Installing ProfileInstallStage::MinecraftInstalling
} }
LegacyProfileInstallStage::PackInstalling => { LegacyProfileInstallStage::PackInstalling => {
ProfileInstallStage::PackInstalling ProfileInstallStage::PackInstalling

View File

@ -53,7 +53,9 @@ pub enum ProfileInstallStage {
/// Profile is installed /// Profile is installed
Installed, Installed,
/// Profile's minecraft game is still installing /// Profile's minecraft game is still installing
Installing, MinecraftInstalling,
/// Pack is installed, but Minecraft installation has not begun
PackInstalled,
/// Profile created for pack, but the pack hasn't been fully installed yet /// Profile created for pack, but the pack hasn't been fully installed yet
PackInstalling, PackInstalling,
/// Profile is not installed /// Profile is not installed
@ -64,7 +66,8 @@ impl ProfileInstallStage {
pub fn as_str(&self) -> &'static str { pub fn as_str(&self) -> &'static str {
match *self { match *self {
Self::Installed => "installed", Self::Installed => "installed",
Self::Installing => "installing", Self::MinecraftInstalling => "minecraft_installing",
Self::PackInstalled => "pack_installed",
Self::PackInstalling => "pack_installing", Self::PackInstalling => "pack_installing",
Self::NotInstalled => "not_installed", Self::NotInstalled => "not_installed",
} }
@ -73,7 +76,9 @@ impl ProfileInstallStage {
pub fn from_str(val: &str) -> Self { pub fn from_str(val: &str) -> Self {
match val { match val {
"installed" => Self::Installed, "installed" => Self::Installed,
"installing" => Self::Installing, "minecraft_installing" => Self::MinecraftInstalling,
"installing" => Self::MinecraftInstalling, // Backwards compatibility
"pack_installed" => Self::PackInstalled,
"pack_installing" => Self::PackInstalling, "pack_installing" => Self::PackInstalling,
"not_installed" => Self::NotInstalled, "not_installed" => Self::NotInstalled,
_ => Self::NotInstalled, _ => Self::NotInstalled,
@ -549,11 +554,11 @@ impl Profile {
pub(crate) async fn refresh_all() -> crate::Result<()> { pub(crate) async fn refresh_all() -> crate::Result<()> {
let state = crate::State::get().await?; let state = crate::State::get().await?;
let all = Self::get_all(&state.pool).await?; let mut all = Self::get_all(&state.pool).await?;
let mut keys = vec![]; let mut keys = vec![];
for profile in &all { for profile in &mut all {
let path = let path =
crate::api::profile::get_full_path(&profile.path).await?; crate::api::profile::get_full_path(&profile.path).await?;
@ -586,6 +591,17 @@ impl Profile {
} }
} }
} }
if profile.install_stage == ProfileInstallStage::MinecraftInstalling
{
profile.install_stage = ProfileInstallStage::PackInstalled;
profile.upsert(&state.pool).await?;
} else if profile.install_stage
== ProfileInstallStage::PackInstalling
{
profile.install_stage = ProfileInstallStage::NotInstalled;
profile.upsert(&state.pool).await?;
}
} }
let file_hashes = CachedEntry::get_file_hash_many( let file_hashes = CachedEntry::get_file_hash_many(