perform resources download within launcher

This commit is contained in:
Chloe committed 2026-03-31 22:14:04 -07:00
1 parent acaff15d4d
commit 23ed997640
4 files changed
+150 -11

No files matched your search

@@ -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
)
}
}
@@ -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.
*/
@@ -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
*
@@ -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()) {