forked from geode-sdk/android-launcher
implement new release api features (#86)
* implement new release api features * make hash nullable
This commit is contained in:
1 parent
ef44d48ed0
commit
3b4951def1
5 files changed
+162
-90
No files matched your search
@@ -716,6 +716,9 @@ fun GeodeUpdateIndicator(snackbarHostState: SnackbarHostState, onRetry: () -> Un
|
|||||||
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
ReleaseManager.UpdateException.Reason.EXTERNAL_FILE_IN_USE -> stringResource(
|
||||||
R.string.release_fetch_external_in_use_short
|
R.string.release_fetch_external_in_use_short
|
||||||
)
|
)
|
||||||
|
ReleaseManager.UpdateException.Reason.HASH_VALIDATION_FAILED -> stringResource(
|
||||||
|
R.string.release_fetch_hash_validation_failed_short
|
||||||
|
)
|
||||||
else -> stringResource(R.string.release_fetch_generic_short)
|
else -> stringResource(R.string.release_fetch_generic_short)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ package com.geode.launcher.updater
|
|||||||
import com.geode.launcher.utils.LaunchUtils
|
import com.geode.launcher.utils.LaunchUtils
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Asset @OptIn(ExperimentalTime::class) constructor(
|
data class Asset(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -14,10 +13,11 @@ data class Asset @OptIn(ExperimentalTime::class) constructor(
|
|||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val updatedAt: Instant,
|
val updatedAt: Instant,
|
||||||
val browserDownloadUrl: String,
|
val browserDownloadUrl: String,
|
||||||
|
val digest: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Release @OptIn(ExperimentalTime::class) constructor(
|
data class Release(
|
||||||
val url: String,
|
val url: String,
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val targetCommitish: String,
|
val targetCommitish: String,
|
||||||
@@ -31,12 +31,29 @@ data class Release @OptIn(ExperimentalTime::class) constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoaderVersion @OptIn(ExperimentalTime::class) constructor(
|
data class LoaderPlatformDownload(
|
||||||
|
val url: String,
|
||||||
|
val hash: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoaderDownload(
|
||||||
|
val win: LoaderPlatformDownload,
|
||||||
|
val mac: LoaderPlatformDownload,
|
||||||
|
val android32: LoaderPlatformDownload,
|
||||||
|
val android64: LoaderPlatformDownload,
|
||||||
|
val ios: LoaderPlatformDownload,
|
||||||
|
val resources: LoaderPlatformDownload,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoaderVersion(
|
||||||
val tag: String,
|
val tag: String,
|
||||||
val version: String,
|
val version: String,
|
||||||
val createdAt: Instant,
|
val createdAt: Instant,
|
||||||
val commitHash: String,
|
val commitHash: String,
|
||||||
val prerelease: Boolean
|
val prerelease: Boolean,
|
||||||
|
val downloads: LoaderDownload,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -45,7 +62,25 @@ data class LoaderPayload<T>(
|
|||||||
val error: String
|
val error: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DownloadableAsset(val url: String, val filename: String, val size: Long? = null)
|
data class DownloadableAsset(
|
||||||
|
val url: String,
|
||||||
|
val filename: String,
|
||||||
|
val size: Long? = null,
|
||||||
|
val hash: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun mapDownload(download: Asset): DownloadableAsset {
|
||||||
|
val hash = if (download.digest?.startsWith("sha256:") == true)
|
||||||
|
download.digest.removePrefix("sha256:")
|
||||||
|
else null
|
||||||
|
|
||||||
|
return DownloadableAsset(
|
||||||
|
url = download.browserDownloadUrl,
|
||||||
|
filename = download.name,
|
||||||
|
size = download.size.toLong(),
|
||||||
|
hash = hash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
abstract class Downloadable {
|
abstract class Downloadable {
|
||||||
/**
|
/**
|
||||||
@@ -83,7 +118,6 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -100,11 +134,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
|
|
||||||
override fun getDownload(): DownloadableAsset? {
|
override fun getDownload(): DownloadableAsset? {
|
||||||
val download = getGitHubDownload() ?: return null
|
val download = getGitHubDownload() ?: return null
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset? {
|
override fun getResourcesDownload(): DownloadableAsset? {
|
||||||
@@ -112,11 +142,7 @@ class DownloadableGitHubLoaderRelease(private val release: Release) : Downloadab
|
|||||||
it.name == "resources.zip"
|
it.name == "resources.zip"
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +151,6 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
|||||||
return release.tagName
|
return release.tagName
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return release.createdAt.epochSeconds
|
return release.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
@@ -149,11 +174,7 @@ class DownloadableLauncherRelease(val release: Release) : Downloadable() {
|
|||||||
|
|
||||||
override fun getDownload(): DownloadableAsset? {
|
override fun getDownload(): DownloadableAsset? {
|
||||||
val download = getGitHubDownload() ?: return null
|
val download = getGitHubDownload() ?: return null
|
||||||
return DownloadableAsset(
|
return mapDownload(download)
|
||||||
url = download.browserDownloadUrl,
|
|
||||||
filename = download.name,
|
|
||||||
size = download.size.toLong()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset? {
|
override fun getResourcesDownload(): DownloadableAsset? {
|
||||||
@@ -166,24 +187,27 @@ class DownloadableLoaderRelease(private val version: LoaderVersion) : Downloadab
|
|||||||
return version.tag
|
return version.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
override fun getDescriptor(): Long {
|
override fun getDescriptor(): Long {
|
||||||
return version.createdAt.epochSeconds
|
return version.createdAt.epochSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDownload(): DownloadableAsset {
|
override fun getDownload(): DownloadableAsset {
|
||||||
|
val data = if (LaunchUtils.is64bit) version.downloads.android64 else version.downloads.android32
|
||||||
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
val filename = "geode-${version.tag}-${LaunchUtils.platformName}.zip"
|
||||||
return DownloadableAsset(
|
return DownloadableAsset(
|
||||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/$filename",
|
url = data.url,
|
||||||
filename = filename
|
filename = filename,
|
||||||
|
hash = data.hash,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getResourcesDownload(): DownloadableAsset {
|
override fun getResourcesDownload(): DownloadableAsset {
|
||||||
|
val data = version.downloads.resources
|
||||||
val filename = "resources.zip"
|
val filename = "resources.zip"
|
||||||
return DownloadableAsset(
|
return DownloadableAsset(
|
||||||
url = "https://github.com/geode-sdk/geode/releases/download/${version.tag}/resources.zip",
|
url = data.url,
|
||||||
filename = filename
|
filename = filename,
|
||||||
|
hash = data.hash,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,8 @@ class ReleaseManager private constructor(
|
|||||||
|
|
||||||
class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) {
|
class UpdateException(reason: Reason? = null, cause: Throwable? = null) : Exception(reason?.name, cause) {
|
||||||
enum class Reason {
|
enum class Reason {
|
||||||
EXTERNAL_FILE_IN_USE
|
EXTERNAL_FILE_IN_USE,
|
||||||
|
HASH_VALIDATION_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
var reason: Reason? = reason
|
var reason: Reason? = reason
|
||||||
@@ -155,17 +156,17 @@ class ReleaseManager private constructor(
|
|||||||
val outputFile = File(outputDirectory, download.filename)
|
val outputFile = File(outputDirectory, download.filename)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
download.url,
|
download.url,
|
||||||
onResponse = { body ->
|
outputFile
|
||||||
body.source().use { source ->
|
|
||||||
outputFile.sink().buffer().use { sink ->
|
|
||||||
sink.writeAll(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (download.hash != null && download.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "downloadLauncherUpdate failed: found $hash but expected ${download.hash}")
|
||||||
|
outputFile.delete()
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return Result.failure(e)
|
return Result.failure(e)
|
||||||
}
|
}
|
||||||
@@ -183,42 +184,52 @@ class ReleaseManager private constructor(
|
|||||||
|
|
||||||
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
|
val finalDir = LaunchUtils.getGeodeResourcesDirectory(applicationContext)
|
||||||
|
|
||||||
|
val downloadFile = getTempFile("resources.zip")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!outputDir.exists()) {
|
if (!outputDir.exists()) {
|
||||||
outputDir.mkdirs()
|
outputDir.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
resourceAsset.url,
|
resourceAsset.url,
|
||||||
|
downloadFile,
|
||||||
onProgress = { progress, outOf ->
|
onProgress = { progress, outOf ->
|
||||||
if (!skipStateUpdate)
|
if (!skipStateUpdate)
|
||||||
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
|
_uiState.value = ReleaseManagerState.InDownload(initialSize + progress, outOf + initialSize)
|
||||||
},
|
},
|
||||||
onResponse = { body ->
|
|
||||||
DownloadUtils.copyZipStreamToDirectory(
|
|
||||||
body.byteStream(),
|
|
||||||
outputDir
|
|
||||||
)
|
|
||||||
|
|
||||||
if (finalDir.exists()) {
|
|
||||||
finalDir.deleteRecursively()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!outputDir.renameTo(finalDir)) {
|
|
||||||
println("Failed to rename temporary directory!")
|
|
||||||
DownloadUtils.copyDirectory(outputDir, finalDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (resourceAsset.hash != null && resourceAsset.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "performResourceDownload failed: found $hash but expected ${resourceAsset.hash}")
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile.inputStream().use { zip ->
|
||||||
|
DownloadUtils.copyZipStreamToDirectory(
|
||||||
|
zip,
|
||||||
|
outputDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDir.exists()) {
|
||||||
|
finalDir.deleteRecursively()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!outputDir.renameTo(finalDir)) {
|
||||||
|
println("Failed to rename temporary directory!")
|
||||||
|
DownloadUtils.copyDirectory(outputDir, finalDir)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sendError(e)
|
sendError(e)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
val tempPathClone = File(outputPath)
|
val tempPathClone = File(outputPath)
|
||||||
runCatching {
|
try {
|
||||||
|
if (downloadFile.exists()) downloadFile.delete()
|
||||||
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
|
if (tempPathClone.exists()) tempPathClone.deleteRecursively()
|
||||||
}
|
} catch (_: IOException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -244,12 +255,14 @@ class ReleaseManager private constructor(
|
|||||||
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
|
val initialSize = if (releaseAsset.size == null && resourcesAsset?.size == null) null else releaseSize + resourcesSize
|
||||||
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
|
_uiState.value = ReleaseManagerState.InDownload(0, initialSize)
|
||||||
|
|
||||||
val outputFile = getTempFile()
|
val outputFile = getTempFile(LaunchUtils.geodeFilename)
|
||||||
// clone the file instance as renameTo may move the original file
|
// clone the file instance as renameTo may move the original file
|
||||||
val tempFilePath = outputFile.path
|
val tempFilePath = outputFile.path
|
||||||
|
|
||||||
val geodeFile = getGeodeOutputPath()
|
val geodeFile = getGeodeOutputPath()
|
||||||
|
|
||||||
|
val downloadFile = getTempFile("geode.zip")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// tempFile should be in same path as geodeFile
|
// tempFile should be in same path as geodeFile
|
||||||
val geodeParent = geodeFile.parentFile
|
val geodeParent = geodeFile.parentFile
|
||||||
@@ -257,45 +270,53 @@ class ReleaseManager private constructor(
|
|||||||
geodeParent.mkdirs()
|
geodeParent.mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadUtils.downloadStream(
|
val hash = DownloadUtils.downloadFile(
|
||||||
httpClient,
|
httpClient,
|
||||||
releaseAsset.url,
|
releaseAsset.url,
|
||||||
|
downloadFile,
|
||||||
onProgress = { progress, outOf ->
|
onProgress = { progress, outOf ->
|
||||||
if (!skipStateUpdate)
|
if (!skipStateUpdate)
|
||||||
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
|
_uiState.value = ReleaseManagerState.InDownload(progress, outOf + resourcesSize)
|
||||||
|
|
||||||
releaseSize = max(outOf, releaseSize)
|
releaseSize = max(outOf, releaseSize)
|
||||||
},
|
|
||||||
onResponse = { body ->
|
|
||||||
DownloadUtils.extractFileFromZipStream(
|
|
||||||
body.byteStream(),
|
|
||||||
outputFile,
|
|
||||||
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, geodeFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (releaseAsset.hash != null && releaseAsset.hash != hash) {
|
||||||
|
Log.w("ReleaseManager", "performUpdate failed: found $hash but expected ${releaseAsset.hash}")
|
||||||
|
throw UpdateException(UpdateException.Reason.HASH_VALIDATION_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile.inputStream().use { zip ->
|
||||||
|
DownloadUtils.extractFileFromZipStream(
|
||||||
|
zip,
|
||||||
|
outputFile,
|
||||||
|
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, geodeFile)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sendError(e)
|
sendError(e)
|
||||||
return
|
return
|
||||||
} finally {
|
} finally {
|
||||||
val tempFileClone = File(tempFilePath)
|
val tempFileClone = File(tempFilePath)
|
||||||
runCatching {
|
try {
|
||||||
|
if (downloadFile.exists()) downloadFile.delete()
|
||||||
if (tempFileClone.exists()) tempFileClone.delete()
|
if (tempFileClone.exists()) tempFileClone.delete()
|
||||||
}
|
} catch (_: IOException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourcesAsset != null) {
|
if (resourcesAsset != null) {
|
||||||
@@ -496,15 +517,14 @@ class ReleaseManager private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTempFile(): File {
|
private fun getTempFile(filename: String): File {
|
||||||
val geodeName = LaunchUtils.geodeFilename
|
|
||||||
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
val geodeDirectory = LaunchUtils.getBaseDirectory(applicationContext)
|
||||||
|
|
||||||
// warning!! while File::createTempFile may look tempting, a certain brand of phones has a messed up implementation of it
|
// 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)
|
// so we're making a temp file manually (as long as it doesn't collide with the geode download, it's okay)
|
||||||
|
|
||||||
val suffix = createRandomString()
|
val suffix = createRandomString()
|
||||||
val tmpName = "tmp-$suffix.$geodeName"
|
val tmpName = "tmp-$suffix.$filename"
|
||||||
|
|
||||||
val tempFile = File(geodeDirectory, tmpName)
|
val tempFile = File(geodeDirectory, tmpName)
|
||||||
|
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ import okio.BufferedSource
|
|||||||
import okio.FileSystem
|
import okio.FileSystem
|
||||||
import okio.ForwardingSink
|
import okio.ForwardingSink
|
||||||
import okio.ForwardingSource
|
import okio.ForwardingSource
|
||||||
|
import okio.HashingSink
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.Sink
|
import okio.Sink
|
||||||
import okio.Source
|
import okio.Source
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -32,12 +34,31 @@ import java.util.zip.ZipInputStream
|
|||||||
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
||||||
|
|
||||||
object DownloadUtils {
|
object DownloadUtils {
|
||||||
|
suspend fun downloadFile(
|
||||||
|
httpClient: OkHttpClient,
|
||||||
|
url: String,
|
||||||
|
outputFile: File,
|
||||||
|
onProgress: ProgressCallback? = null,
|
||||||
|
): String = downloadStream(
|
||||||
|
httpClient,
|
||||||
|
url,
|
||||||
|
onProgress = onProgress
|
||||||
|
).use { body ->
|
||||||
|
val hashingSink = HashingSink.sha256(outputFile.sink().buffer())
|
||||||
|
hashingSink.use { sink ->
|
||||||
|
body.source().use { source ->
|
||||||
|
source.readAll(sink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashingSink.hash.hex()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun downloadStream(
|
suspend fun downloadStream(
|
||||||
httpClient: OkHttpClient,
|
httpClient: OkHttpClient,
|
||||||
url: String,
|
url: String,
|
||||||
onProgress: ProgressCallback? = null,
|
onProgress: ProgressCallback? = null,
|
||||||
onResponse: suspend (ResponseBody) -> Unit,
|
): ResponseBody {
|
||||||
) {
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.build()
|
.build()
|
||||||
@@ -64,12 +85,15 @@ object DownloadUtils {
|
|||||||
val progressClient = progressClientBuilder.build()
|
val progressClient = progressClientBuilder.build()
|
||||||
|
|
||||||
val call = progressClient.newCall(request)
|
val call = progressClient.newCall(request)
|
||||||
call.executeAsync().use { response ->
|
val response = call.executeAsync()
|
||||||
when (response.code) {
|
|
||||||
200 -> onResponse(response.body)
|
val responseCode = response.code
|
||||||
else -> throw IOException("unexpected response ${response.code}")
|
if (responseCode != 200) {
|
||||||
}
|
response.close()
|
||||||
|
throw IOException("unexpected response ${response.code}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response.body
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
|
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
<string name="release_fetch_manual_check_required">Externally managed library in use, please manually check for updates if you want to overwrite it.</string>
|
||||||
|
|
||||||
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
|
<string name="release_fetch_external_in_use_short">Update failed. External library in use.</string>
|
||||||
|
<string name="release_fetch_hash_validation_failed_short">Update failed. Integrity check failed.</string>
|
||||||
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
|
<string name="release_fetch_no_internet_short">Update failed. Internet not connected.</string>
|
||||||
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
|
<string name="release_fetch_generic_short">Update failed. Please try again later.</string>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user