[download] implement coil disk caching

unfortunately, in coil disk caching is not generic, but deeply integrated in their NetworkFetcher, so if you need a custom Fetcher then you need to implement disk caching yourself.

Maybe in the future, we can hook into their NetworkFetcher and try to use our mirror logic around it. This or they make disk caching work for all fetchers.
This commit is contained in:
Torsten Grote
2025-10-22 10:13:09 -03:00
parent e99840458e
commit a47423ef01
2 changed files with 123 additions and 22 deletions

View File

@@ -1,43 +1,135 @@
/**
* Contains disk cache related code from https://github.com/coil-kt/coil
* coil-network-core/src/commonMain/kotlin/coil3/network/NetworkFetcher.kt
* under Apache-2.0 license.
*/
package org.fdroid.download.coil
import coil3.ImageLoader
import coil3.annotation.InternalCoilApi
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.disk.DiskCache
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.util.MimeTypeMap
import io.ktor.utils.io.jvm.javaio.toInputStream
import okio.BufferedSource
import okio.FileSystem
import okio.buffer
import okio.source
import org.fdroid.download.DownloadRequest
import org.fdroid.download.HttpManager
import org.fdroid.download.glide.AutoVerifyingInputStream
import org.fdroid.download.glide.getKey
import javax.inject.Inject
public class DownloadRequestFetcher(
private val httpManager: HttpManager,
private val downloadRequest: DownloadRequest,
private val options: Options,
private val diskCache: Lazy<DiskCache?>,
) : Fetcher {
private val fileSystem: FileSystem
get() = diskCache.value?.fileSystem ?: options.fileSystem
private val diskCacheKey: String
get() = options.diskCacheKey ?: downloadRequest.getKey()
@OptIn(InternalCoilApi::class)
private val mimeType: String?
get() = MimeTypeMap.getMimeTypeFromUrl(downloadRequest.indexFile.name)
override suspend fun fetch(): FetchResult? {
// TODO use channel directly and auto-verify hash without InputStream wrapper
// may need https://github.com/Kotlin/kotlinx-io/blob/master/integration/kotlinx-io-okio/Module.md
val inputStream = httpManager.getChannel(downloadRequest).toInputStream()
val sha256 = downloadRequest.indexFile.sha256
val resultStream = if (sha256 == null) {
inputStream
} else {
AutoVerifyingInputStream(inputStream, sha256)
var snapshot = readFromDiskCache()
try {
if (snapshot != null) {
// we have the request cached, so return it right away
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = mimeType,
dataSource = DataSource.DISK,
)
}
// TODO use channel directly and auto-verify hash without InputStream wrapper
// may need https://github.com/Kotlin/kotlinx-io/blob/master/integration/kotlinx-io-okio/Module.md
val inputStream = httpManager.getChannel(downloadRequest).toInputStream()
val sha256 = downloadRequest.indexFile.sha256
val bufferedSource = if (sha256 == null) {
inputStream
} else {
AutoVerifyingInputStream(inputStream, sha256)
}.source().buffer()
snapshot = writeToDiskCache(snapshot, bufferedSource)
if (snapshot == null) {
// we couldn't write the snapshot, so try returning directly
return SourceFetchResult(
source = ImageSource(
source = bufferedSource,
fileSystem = FileSystem.SYSTEM,
metadata = null,
),
mimeType = mimeType,
dataSource = DataSource.NETWORK,
)
}
return SourceFetchResult(
source = snapshot.toImageSource(),
mimeType = mimeType,
dataSource = DataSource.NETWORK,
)
} finally {
snapshot?.close()
}
return SourceFetchResult(
source = ImageSource(
source = resultStream.source().buffer(),
fileSystem = FileSystem.SYSTEM,
metadata = null,
),
mimeType = null,
dataSource = DataSource.NETWORK,
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) {
diskCache.value?.openSnapshot(downloadRequest.getKey())
} else {
null
}
}
private fun writeToDiskCache(
snapshot: DiskCache.Snapshot?,
bufferedSource: BufferedSource,
): DiskCache.Snapshot? {
// Short circuit if we're not allowed to cache this response.
if (!options.diskCachePolicy.writeEnabled) return null
// Open a new editor. Return null if we're unable to write to this entry.
val editor = if (snapshot != null) {
snapshot.closeAndOpenEditor()
} else {
diskCache.value?.openEditor(diskCacheKey)
} ?: return null
return try {
fileSystem.write(editor.data) {
writeAll(bufferedSource)
}
editor.commitAndOpenSnapshot()
} catch (e: Exception) {
try {
editor.abort()
} catch (_: Exception) {
// ignore
}
throw e
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(
file = data,
fileSystem = fileSystem,
diskCacheKey = diskCacheKey,
closeable = this,
)
}
@@ -46,8 +138,13 @@ public class DownloadRequestFetcher(
) : Fetcher.Factory<DownloadRequest> {
override fun create(
data: DownloadRequest,
options: coil3.request.Options,
imageLoader: ImageLoader
): Fetcher? = DownloadRequestFetcher(httpManager, data)
options: Options,
imageLoader: ImageLoader,
): Fetcher? = DownloadRequestFetcher(
httpManager = httpManager,
downloadRequest = data,
options = options,
diskCache = lazy { imageLoader.diskCache },
)
}
}

View File

@@ -24,7 +24,7 @@ public class DownloadRequestLoader(
height: Int,
options: Options,
): LoadData<InputStream> {
return LoadData(downloadRequest.getKey(), HttpFetcher(httpManager, downloadRequest))
return LoadData(downloadRequest.getObjectKey(), HttpFetcher(httpManager, downloadRequest))
}
public class Factory(
@@ -41,6 +41,10 @@ public class DownloadRequestLoader(
}
internal fun DownloadRequest.getKey(): ObjectKey {
return ObjectKey(indexFile.sha256 ?: (mirrors[0].baseUrl + indexFile.name))
internal fun DownloadRequest.getObjectKey(): ObjectKey {
return ObjectKey(getKey())
}
internal fun DownloadRequest.getKey(): String {
return indexFile.sha256 ?: (mirrors[0].baseUrl + indexFile.name)
}