From 23ed99764070fff618281258846aa88e407548d8 Mon Sep 17 00:00:00 2001 From: Chloe Date: Tue, 31 Mar 2026 22:14:04 -0700 Subject: [PATCH] perform resources download within launcher --- .../com/geode/launcher/updater/Release.kt | 34 ++++++- .../geode/launcher/updater/ReleaseManager.kt | 93 +++++++++++++++++-- .../com/geode/launcher/utils/DownloadUtils.kt | 29 ++++++ .../com/geode/launcher/utils/LaunchUtils.kt | 5 + 4 files changed, 150 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/geode/launcher/updater/Release.kt b/app/src/main/java/com/geode/launcher/updater/Release.kt index 27c77499..dba1e878 100644 --- a/app/src/main/java/com/geode/launcher/updater/Release.kt +++ b/app/src/main/java/com/geode/launcher/updater/Release.kt @@ -62,6 +62,11 @@ abstract class Downloadable { * Release download link for the current platform */ abstract fun getDownload(): DownloadableAsset? + + /** + * Resources download link for current platform. + */ + abstract fun getResourcesDownload(): DownloadableAsset? } class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadable() { @@ -101,6 +106,18 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab size = download.size.toLong() ) } + + override fun getResourcesDownload(): DownloadableAsset? { + val download = release.assets.find { + it.name == "resources.zip" + } ?: return null + + return DownloadableAsset( + url = download.browserDownloadUrl, + filename = download.name, + size = download.size.toLong() + ) + } } class DownloadableLauncherRelease(val release: Release) : Downloadable() { @@ -126,8 +143,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() { * 0 | 0 | 1 * surprise! it's an xnor */ - - (asset.name.contains("android32")) == use32BitPlatform + asset.name.endsWith("apk") && asset.name.contains("android32") == use32BitPlatform } } @@ -139,6 +155,10 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() { size = download.size.toLong() ) } + + override fun getResourcesDownload(): DownloadableAsset? { + return null + } } class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadable() { @@ -151,11 +171,19 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab return version.createdAt.epochSeconds } - override fun getDownload(): DownloadableAsset? { + override fun getDownload(): DownloadableAsset { val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip" return DownloadableAsset( url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename", filename = filename ) } + + override fun getResourcesDownload(): DownloadableAsset { + val filename = "resources.zip" + return DownloadableAsset( + url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip", + filename = filename + ) + } } diff --git a/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt b/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt index 281266a2..a4f5f26e 100644 --- a/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt +++ b/app/src/main/java/com/geode/launcher/updater/ReleaseManager.kt @@ -25,7 +25,9 @@ import okio.sink import okio.source import java.io.File import java.io.FileNotFoundException +import java.io.IOException import java.io.InterruptedIOException +import kotlin.math.max import kotlin.time.Clock import kotlin.time.Duration.Companion.minutes @@ -168,6 +170,53 @@ class ReleaseManager private constructor( return Result.success(outputFile) } + private suspend fun performResourceDownload(resourceAsset: DownloadableAsset, initialSize: Long) { + val guessSize = if (resourceAsset.size == null) null else initialSize + resourceAsset.size + val guessInitial = if (guessSize == null) 0 else initialSize + _uiState.value = ReleaseManagerState.InDownload(guessInitial, guessSize) + + val outputDir = getTempResourcesDirectory() + val outputPath = outputDir.path + + val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext) + + try { + if (!outputDir.exists()) { + outputDir.mkdirs() + } + + DownloadUtils.downloadStream( + httpClient, + resourceAsset.url, + onProgress = { progress, outOf -> + _uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize) + }, + onResponse = { body -> + DownloadUtils.copyZipStreamToDirectory( + body.byteStream(), + outputDir + ) + + if (finalDir.exists()) { + finalDir.deleteRecursively() + } + + if (!outputDir.renameTo(finalDir)) { + throw IOException("failed to rename resources output directory") + } + } + ) + } catch (e: Exception) { + sendError(e) + return + } finally { + val tempPathClone = File(outputPath) + runCatching { + if (tempPathClone.exists()) tempPathClone.deleteRecursively() + } + } + } + private suspend fun performUpdate(release: Downloadable) { val releaseAsset = release.getDownload() if (releaseAsset == null) { @@ -177,8 +226,14 @@ class ReleaseManager private constructor( return } + val resourcesAsset = release.getResourcesDownload() + + var releaseSize = releaseAsset.size ?: 0L + val resourcesSize = resourcesAsset?.size ?: 0L + // set an initial download size - _uiState.value = ReleaseManagerState.InDownload(0, releaseAsset.size) + val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize + _uiState.value = ReleaseManagerState.InDownload(0, initialSize) val outputFile = getTempFile() // clone the file instance as renameTo may move the original file @@ -197,7 +252,9 @@ class ReleaseManager private constructor( httpClient, releaseAsset.url, onProgress = { progress, outOf -> - _uiState.value = ReleaseManagerState.InDownload(progress, outOf) + _uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize) + + releaseSize = max(outOf, releaseSize) }, onResponse = { body -> DownloadUtils.extractFileFromZipStream( @@ -231,6 +288,10 @@ class ReleaseManager private constructor( } } + if (resourcesAsset != null) { + performResourceDownload(resourcesAsset, releaseSize) + } + // extraction performed updatePreferences(release) @@ -251,7 +312,7 @@ class ReleaseManager private constructor( val sharedPreferences = PreferenceUtils.get(applicationContext) val originalFileHash = sharedPreferences.getString(PreferenceUtils.Key.CURRENT_RELEASE_MODIFIED) - ?: return false + ?: return true val currentFileHash = computeFileHash(geodeFile) return originalFileHash != currentFileHash @@ -401,6 +462,13 @@ class ReleaseManager private constructor( return File(geodeDirectory, geodeName) } + private fun createRandomString(): String { + val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return buildString(8) { + repeat(8) { append(alphabet.random()) } + } + } + private fun getTempFile(): File { val geodeName = LaunchUtils.geodeFilename val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext) @@ -408,11 +476,7 @@ class ReleaseManager private constructor( // warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it // so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay) - val alphabet = ('A'..'Z') + ('a'..'z') + ('0'..'9') - val suffix = buildString(8) { - repeat(8) { append(alphabet.random()) } - } - + val suffix = createRandomString() val tmpName = "tmp-$suffix.$geodeName" val tempFile = File(geodeDirectory, tmpName) @@ -420,6 +484,19 @@ class ReleaseManager private constructor( return tempFile } + private fun getTempResourcesDirectory(): File { + val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext) + val suffix = createRandomString() + val tempDirName = "tmp-$suffix-geode.loader" + + val parentDir = finalDir.parentFile + return if (parentDir != null) { + File(finalDir.parentFile, tempDirName) + } else { + File(tempDirName) + } + } + /** * Cancels the current update job. */ diff --git a/app/src/main/java/com/geode/launcher/utils/DownloadUtils.kt b/app/src/main/java/com/geode/launcher/utils/DownloadUtils.kt index 7a341a7c..f1a082ff 100644 --- a/app/src/main/java/com/geode/launcher/utils/DownloadUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/DownloadUtils.kt @@ -72,6 +72,35 @@ object DownloadUtils { } } + suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible { + ZipInputStream(inputStream).use { zipStream -> + if (output.exists()) output.deleteRecursively() + output.mkdirs() + + val canonicalOutput = output.canonicalPath + + var entry = zipStream.nextEntry + while (entry != null) { + val destination = File(output, entry.name) + if (!destination.canonicalPath.startsWith(canonicalOutput)) { + throw IOException("attempted copy to outside of output directory: ${entry.name}") + } + + if (destination.isDirectory) { + destination.mkdirs() + } else { + destination.parentFile?.mkdirs() + + destination.outputStream().use { destinationStream -> + zipStream.copyTo(destinationStream) + } + } + + entry = zipStream.nextEntry + } + } + } + /** * Extracts a file named by zipPath from inputStream and copies it to outputStream * diff --git a/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt b/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt index 1c507dbc..3c02bfc0 100644 --- a/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt +++ b/app/src/main/java/com/geode/launcher/utils/LaunchUtils.kt @@ -85,6 +85,11 @@ object LaunchUtils { return File(base, "game/geode/logs/") } + fun getGeodeResourcesDirectory(context: Context): File { + val base = getBaseDirectory(context) + return File(base, "game/geode/resources/geode.loader/") + } + fun getLastCrash(context: Context): File? { val crashDirectory = getCrashDirectory(context) if (!crashDirectory.exists()) {