code simplification and deduplication

This commit is contained in:
davotoula
2025-11-02 20:37:20 +01:00
parent 76be7b0d58
commit 942de50148
4 changed files with 70 additions and 93 deletions
@@ -50,64 +50,54 @@ class ImageDownloader {
* Stream download and calculate hash for verification without loading entire file into memory. * Stream download and calculate hash for verification without loading entire file into memory.
* This is memory-efficient for large files (videos, high-res images, etc.) * This is memory-efficient for large files (videos, high-res images, etc.)
*/ */
suspend fun waitAndVerifyStream( private suspend fun <T> retryWithDelay(
imageUrl: String, maxAttempts: Int = 15,
okHttpClient: (url: String) -> OkHttpClient, delayMs: Long = 1000,
): StreamVerification? = operation: suspend () -> T?,
): T? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var verification: StreamVerification? = null var result: T? = null
var tentatives = 0 var tentatives = 0
// Servers are usually not ready, so tries to download it for 15 times/seconds. // Servers are usually not ready, so tries to download it for 15 times/seconds.
while (verification == null && tentatives < 15) { while (result == null && tentatives < maxAttempts) {
verification = result =
try { try {
tryStreamAndVerify(imageUrl, okHttpClient) operation()
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
null null
} }
if (verification == null) { if (result == null) {
tentatives++ tentatives++
delay(1000) delay(delayMs)
} }
} }
return@withContext verification return@withContext result
} }
suspend fun waitAndVerifyStream(
imageUrl: String,
okHttpClient: (url: String) -> OkHttpClient,
): StreamVerification? = retryWithDelay { tryStreamAndVerify(imageUrl, okHttpClient) }
suspend fun waitAndGetImage( suspend fun waitAndGetImage(
imageUrl: String, imageUrl: String,
okHttpClient: (url: String) -> OkHttpClient, okHttpClient: (url: String) -> OkHttpClient,
): Blob? = ): Blob? = retryWithDelay { tryGetTheImage(imageUrl, okHttpClient) }
withContext(Dispatchers.IO) {
var imageData: Blob? = null
var tentatives = 0
// Servers are usually not ready, so tries to download it for 15 times/seconds. private data class HttpConnection(
while (imageData == null && tentatives < 15) { val connection: HttpURLConnection,
imageData = val responseCode: Int,
try { val contentType: String?,
tryGetTheImage(imageUrl, okHttpClient) )
} catch (e: Exception) {
if (e is CancellationException) throw e
null
}
if (imageData == null) { private suspend fun openHttpConnection(
tentatives++
delay(1000)
}
}
return@withContext imageData
}
private suspend fun tryStreamAndVerify(
imageUrl: String, imageUrl: String,
okHttpClient: (url: String) -> OkHttpClient, okHttpClient: (url: String) -> OkHttpClient,
): StreamVerification? = ): HttpConnection =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// TODO: Migrate to OkHttp // TODO: Migrate to OkHttp
HttpURLConnection.setFollowRedirects(true) HttpURLConnection.setFollowRedirects(true)
@@ -122,6 +112,7 @@ class ImageDownloader {
huc.instanceFollowRedirects = true huc.instanceFollowRedirects = true
var responseCode = huc.responseCode var responseCode = huc.responseCode
// Handle redirects
if (responseCode in 300..400) { if (responseCode in 300..400) {
val newUrl: String = huc.getHeaderField("Location") val newUrl: String = huc.getHeaderField("Location")
@@ -137,24 +128,38 @@ class ImageDownloader {
responseCode = huc.responseCode responseCode = huc.responseCode
} }
return@withContext HttpConnection(
connection = huc,
responseCode = responseCode,
contentType = huc.headerFields.get("Content-Type")?.firstOrNull(),
)
}
private suspend fun tryStreamAndVerify(
imageUrl: String,
okHttpClient: (url: String) -> OkHttpClient,
): StreamVerification? =
withContext(Dispatchers.IO) {
val httpConn = openHttpConnection(imageUrl, okHttpClient)
return@withContext try { return@withContext try {
if (responseCode in 200..300) { if (httpConn.responseCode in 200..300) {
val (hash, totalBytes) = val (hash, totalBytes) =
huc.inputStream.use { httpConn.connection.inputStream.use {
sha256StreamWithCount(it) sha256StreamWithCount(it)
} }
StreamVerification( StreamVerification(
hash = hash.toHexKey(), hash = hash.toHexKey(),
size = totalBytes, size = totalBytes,
contentType = huc.headerFields.get("Content-Type")?.firstOrNull(), contentType = httpConn.contentType,
) )
} else { } else {
null null
} }
} finally { } finally {
// Always disconnect to release connection resources // Always disconnect to release connection resources
huc.disconnect() httpConn.connection.disconnect()
} }
} }
@@ -163,41 +168,19 @@ class ImageDownloader {
okHttpClient: (url: String) -> OkHttpClient, okHttpClient: (url: String) -> OkHttpClient,
): Blob? = ): Blob? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// TODO: Migrate to OkHttp val httpConn = openHttpConnection(imageUrl, okHttpClient)
HttpURLConnection.setFollowRedirects(true)
var url = URL(imageUrl)
var clientProxy = okHttpClient(imageUrl).proxy
var huc =
if (clientProxy != null) {
url.openConnection(clientProxy) as HttpURLConnection
} else {
url.openConnection() as HttpURLConnection
}
huc.instanceFollowRedirects = true
var responseCode = huc.responseCode
if (responseCode in 300..400) { return@withContext try {
val newUrl: String = huc.getHeaderField("Location") if (httpConn.responseCode in 200..300) {
// open the new connnection again
url = URL(newUrl)
clientProxy = okHttpClient(newUrl).proxy
huc =
if (clientProxy != null) {
url.openConnection(clientProxy) as HttpURLConnection
} else {
url.openConnection() as HttpURLConnection
}
responseCode = huc.responseCode
}
return@withContext if (responseCode in 200..300) {
Blob( Blob(
huc.inputStream.use { it.readBytes() }, httpConn.connection.inputStream.use { it.readBytes() },
huc.headerFields.get("Content-Type")?.firstOrNull(), httpConn.contentType,
) )
} else { } else {
null null
} }
} finally {
httpConn.connection.disconnect()
}
} }
} }
@@ -45,4 +45,12 @@ data class MediaUploadResult(
val ipfs: String? = null, val ipfs: String? = null,
// blurhash value for previews // blurhash value for previews
val blurHash: BlurhashWrapper? = null, val blurHash: BlurhashWrapper? = null,
) ) {
fun mergeLocalMetadata(localMetadata: Pair<BlurhashWrapper?, DimensionTag?>?): MediaUploadResult =
localMetadata?.let { (blur, dim) ->
copy(
dimension = dim ?: dimension,
blurHash = blur ?: blurHash,
)
} ?: this
}
@@ -97,8 +97,8 @@ class BlossomUploader {
val imageInputStream = contentResolver.openInputStream(uri) val imageInputStream = contentResolver.openInputStream(uri)
checkNotNull(imageInputStream) { "Can't open the image input stream" } checkNotNull(imageInputStream) { "Can't open the image input stream" }
val serverResult = return imageInputStream
imageInputStream.use { stream -> .use { stream ->
upload( upload(
stream, stream,
hash, hash,
@@ -112,14 +112,7 @@ class BlossomUploader {
httpAuth, httpAuth,
context, context,
) )
} }.mergeLocalMetadata(localMetadata)
return localMetadata?.let { (blur, dim) ->
serverResult.copy(
dimension = dim ?: serverResult.dimension,
blurHash = blur ?: serverResult.blurHash,
)
} ?: serverResult
} }
fun encodeAuth(event: BlossomAuthorizationEvent): String { fun encodeAuth(event: BlossomAuthorizationEvent): String {
@@ -112,8 +112,8 @@ class Nip96Uploader {
checkNotNull(imageInputStream) { "Can't open the image input stream" } checkNotNull(imageInputStream) { "Can't open the image input stream" }
val serverResult = return imageInputStream
imageInputStream.use { stream -> .use { stream ->
upload( upload(
stream, stream,
length, length,
@@ -126,14 +126,7 @@ class Nip96Uploader {
httpAuth, httpAuth,
context, context,
) )
} }.mergeLocalMetadata(localMetadata)
return localMetadata?.let { (blur, dim) ->
serverResult.copy(
dimension = dim ?: serverResult.dimension,
blurHash = blur ?: serverResult.blurHash,
)
} ?: serverResult
} }
suspend fun upload( suspend fun upload(