@@ -6,22 +6,29 @@ import io.ktor.client.plugins.cache.storage.CacheStorage
66import io.ktor.client.plugins.cache.storage.CachedResponseData
77import io.ktor.http.Url
88import io.ktor.util.collections.ConcurrentMap
9+ import io.ktor.util.logging.KtorSimpleLogger
10+ import io.ktor.util.logging.trace
911import kotlinx.coroutines.CoroutineDispatcher
1012import kotlinx.coroutines.coroutineScope
13+ import kotlinx.coroutines.launch
1114import kotlinx.coroutines.sync.Mutex
1215import kotlinx.coroutines.sync.withLock
1316import kotlinx.coroutines.withContext
1417import kotlinx.serialization.ExperimentalSerializationApi
1518import kotlinx.serialization.cbor.Cbor
19+ import kotlinx.serialization.decodeFromByteArray
1620import kotlinx.serialization.decodeFromHexString
21+ import kotlinx.serialization.encodeToByteArray
1722import kotlinx.serialization.encodeToHexString
1823import okio.ByteString.Companion.encodeUtf8
1924import okio.FileSystem
2025import okio.HashingSink
2126import okio.Path
27+ import okio.Path.Companion.toPath
2228import okio.blackholeSink
2329import okio.buffer
2430import okio.use
31+ import kotlin.collections.emptySet
2532
2633internal val prefix = " fr.frankois944.ktorfilecaching_key"
2734
@@ -41,14 +48,18 @@ internal class FileCacheStorage(
4148) : CacheStorage {
4249 private val mutexes = ConcurrentMap <String , Mutex >()
4350
51+ @Suppress(" ktlint:standard:property-naming" )
52+ private val LOGGER = KtorSimpleLogger (" KtorFileCaching" )
53+
4454 override suspend fun store (
4555 url : Url ,
4656 data : CachedResponseData ,
4757 ): Unit =
4858 withContext(dispatcher) {
4959 val urlHex = key(url)
50- val caches = readCache(urlHex).filterNot { it.varyKeys == data.varyKeys } + data
51- writeCache(urlHex, caches)
60+ updateCache(urlHex) { caches ->
61+ caches.filterNot { it.varyKeys == data.varyKeys } + data
62+ }
5263 }
5364
5465 override suspend fun findAll (url : Url ): Set <CachedResponseData > = readCache(key(url))
@@ -58,7 +69,24 @@ internal class FileCacheStorage(
5869 varyKeys : Map <String , String >,
5970 ): CachedResponseData ? {
6071 val data = readCache(key(url))
61- return data.find { varyKeys.all { (key, value) -> it.varyKeys[key] == value } }
72+ return data.find {
73+ varyKeys.all { (key, value) -> it.varyKeys[key] == value }
74+ }
75+ }
76+
77+ override suspend fun remove (
78+ url : Url ,
79+ varyKeys : Map <String , String >,
80+ ) {
81+ val urlHex = key(url)
82+ updateCache(urlHex) { caches ->
83+ caches.filterNot { it.varyKeys == varyKeys }
84+ }
85+ }
86+
87+ override suspend fun removeAll (url : Url ) {
88+ val urlHex = key(url)
89+ deleteCache(urlHex)
6290 }
6391
6492 private fun key (url : Url ): String {
@@ -69,27 +97,53 @@ internal class FileCacheStorage(
6997 return hashingSink.hash.hex()
7098 }
7199
72- private suspend fun writeCache (
100+ private suspend fun readCache (urlHex : String ): Set <CachedResponseData > {
101+ val mutex = mutexes.computeIfAbsent(urlHex) { Mutex () }
102+ return mutex.withLock { readCacheUnsafe(urlHex) }
103+ }
104+
105+ private suspend inline fun updateCache (
73106 urlHex : String ,
74- caches : List <CachedResponseData >,
75- ) = coroutineScope {
107+ transform : ( Set < CachedResponseData >) -> List <CachedResponseData >,
108+ ) {
76109 val mutex = mutexes.computeIfAbsent(urlHex) { Mutex () }
77- mutex.withLock {
78- val serializedData = Cbor .encodeToHexString(caches.map { SerializableCachedResponseData (it) } )
79- Database .setItem( " ${prefix} _ $ urlHex" , serializedData )
110+ return mutex.withLock {
111+ val caches = readCacheUnsafe(urlHex )
112+ writeCacheUnsafe( urlHex, transform(caches) )
80113 }
81114 }
82115
83- private suspend fun readCache (urlHex : String ): Set < CachedResponseData > {
116+ private suspend fun deleteCache (urlHex : String ) {
84117 val mutex = mutexes.computeIfAbsent(urlHex) { Mutex () }
85- return mutex.withLock {
86- val item = Database .getItem(" ${prefix} _$urlHex " )
87- if (item == null ) return @withLock emptySet()
118+ mutex.withLock {
88119 try {
89- Cbor .decodeFromHexString<Set <SerializableCachedResponseData >>(item).map { it.cachedResponseData }.toSet()
90- } catch (e: Exception ) {
91- emptySet()
120+ if (Database .getItem(" ${prefix} _$urlHex " ) == null ) return @withLock
121+ Database .removeItem(" ${prefix} _$urlHex " )
122+ } catch (cause: Exception ) {
123+ LOGGER .trace { " Exception during cache deletion in a file: ${cause.stackTraceToString()} " }
92124 }
93125 }
94126 }
127+
128+ private suspend fun writeCacheUnsafe (
129+ urlHex : String ,
130+ caches : List <CachedResponseData >,
131+ ) = coroutineScope {
132+ try {
133+ val serializedData = Cbor .encodeToHexString(caches.map { SerializableCachedResponseData (it) })
134+ Database .setItem(" ${prefix} _$urlHex " , serializedData)
135+ } catch (cause: Exception ) {
136+ LOGGER .trace { " Exception during saving a cache to a file: ${cause.stackTraceToString()} " }
137+ }
138+ }
139+
140+ private suspend fun readCacheUnsafe (urlHex : String ): Set <CachedResponseData > {
141+ return try {
142+ val item = Database .getItem(" ${prefix} _$urlHex " ) ? : return emptySet()
143+ Cbor .decodeFromHexString<Set <SerializableCachedResponseData >>(item).map { it.cachedResponseData }.toSet()
144+ } catch (cause: Exception ) {
145+ LOGGER .trace { " Exception during cache lookup in a file: ${cause.stackTraceToString()} " }
146+ emptySet()
147+ }
148+ }
95149}
0 commit comments