improve resilience of downloadStream

This commit is contained in:
Chloe committed 2026-02-24 04:13:59 -07:00
1 parent d1fdb26d44
commit 3202851303
2 files changed
+77 -31

No files matched your search

@@ -19,6 +19,7 @@ import kotlinx.coroutines.launch
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okio.buffer import okio.buffer
import okio.sink
import okio.source import okio.source
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@@ -149,12 +150,15 @@ class ReleaseManager private constructor(
try { try {
DownloadUtils.downloadStream( DownloadUtils.downloadStream(
httpClient, httpClient,
download.url download.url,
).use { fileStream -> onResponse = { body ->
outputFile.outputStream().use { body.source().use { source ->
fileStream.copyTo(it) outputFile.sink().buffer().use { sink ->
sink.writeAll(source)
}
}
} }
} )
} catch (e: Exception) { } catch (e: Exception) {
return Result.failure(e) return Result.failure(e)
} }
@@ -179,37 +183,49 @@ class ReleaseManager private constructor(
val tempFilePath = outputFile.path val tempFilePath = outputFile.path
try { try {
val fileStream = DownloadUtils.downloadStream(
httpClient,
releaseAsset.url
) { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
}
outputFile.parentFile?.mkdirs()
val geodeFile = getGeodeOutputPath() val geodeFile = getGeodeOutputPath()
DownloadUtils.extractFileFromZipStream( val geodeParent = geodeFile.parentFile
fileStream, if (geodeParent != null && !geodeParent.exists()) {
outputFile.outputStream(), geodeParent.mkdirs()
geodeFile.name
)
// work around a permission issue from adb push
if (geodeFile.exists()) {
geodeFile.delete()
} }
outputFile.renameTo(geodeFile) DownloadUtils.downloadStream(
httpClient,
releaseAsset.url,
onProgress = { progress, outOf ->
_uiState.value = ReleaseManagerState.InDownload(progress, outOf)
},
onResponse = { body ->
DownloadUtils.extractFileFromZipStream(
body.byteStream(),
outputFile.outputStream(),
geodeFile.name
)
// work around a permission issue from adb push
if (geodeFile.exists()) {
geodeFile.delete()
}
val renameSuccessful = runCatching {
outputFile.renameTo(geodeFile)
}.getOrDefault(false)
if (!renameSuccessful) {
// attempt a manual copy if rename fails for whatever reason
DownloadUtils.copyFile(outputFile.inputStream(), geodeFile.outputStream())
}
}
)
} catch (e: Exception) { } catch (e: Exception) {
sendError(e) sendError(e)
return return
} finally { } finally {
val tempFileClone = File(tempFilePath) val tempFileClone = File(tempFilePath)
try { runCatching {
if (tempFileClone.exists()) tempFileClone.delete() if (tempFileClone.exists()) tempFileClone.delete()
} catch (_: Exception) { } }
} }
// extraction performed // extraction performed
@@ -2,7 +2,9 @@ package com.geode.launcher.utils
import android.os.Build import android.os.Build
import android.os.FileUtils import android.os.FileUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -12,11 +14,14 @@ import okhttp3.coroutines.executeAsync
import okio.Buffer import okio.Buffer
import okio.BufferedSink import okio.BufferedSink
import okio.BufferedSource import okio.BufferedSource
import okio.FileSystem
import okio.ForwardingSink import okio.ForwardingSink
import okio.ForwardingSource import okio.ForwardingSource
import okio.Path.Companion.toOkioPath
import okio.Sink import okio.Sink
import okio.Source import okio.Source
import okio.buffer import okio.buffer
import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@@ -30,8 +35,9 @@ object DownloadUtils {
suspend fun downloadStream( suspend fun downloadStream(
httpClient: OkHttpClient, httpClient: OkHttpClient,
url: String, url: String,
onProgress: ProgressCallback? = null onProgress: ProgressCallback? = null,
): InputStream { onResponse: suspend (ResponseBody) -> Unit,
) {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.build() .build()
@@ -58,11 +64,19 @@ object DownloadUtils {
val progressClient = progressClientBuilder.build() val progressClient = progressClientBuilder.build()
val call = progressClient.newCall(request) val call = progressClient.newCall(request)
val response = call.executeAsync() call.executeAsync().use { response ->
when (response.code) {
return response.body.byteStream() 200 -> onResponse(response.body)
else -> throw IOException("unexpected response ${response.code}")
}
}
} }
/**
* Extracts a file named by zipPath from inputStream and copies it to outputStream
*
* inputStream and outputStream are both closed after this function executes
*/
suspend fun extractFileFromZipStream(inputStream: InputStream, outputStream: OutputStream, zipPath: String) { suspend fun extractFileFromZipStream(inputStream: InputStream, outputStream: OutputStream, zipPath: String) {
// note to self: ZipInputStreams are a little silly // note to self: ZipInputStreams are a little silly
// (runInterruptible allows it to cancel, otherwise it waits for the stream to finish) // (runInterruptible allows it to cancel, otherwise it waits for the stream to finish)
@@ -90,6 +104,22 @@ object DownloadUtils {
} }
} }
suspend fun copyFile(from: File, to: File) {
val fs = FileSystem.SYSTEM
val fromPath = from.toOkioPath()
val toPath = to.toOkioPath()
withContext(Dispatchers.IO) {
fs.copy(fromPath, toPath)
}
}
/**
* Copies data from inputStream to outputStream
*
* Both streams are closed upon the conclusion of this function.
*/
fun copyFile(inputStream: InputStream, outputStream: OutputStream) { fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
// gotta love copying // gotta love copying
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {