fix: reuse cached gateway downloads within a 10-minute TTL (#915)

Every tap on a gateway attachment re-downloaded the whole file: the
cache write used a random UUID prefix, so no two fetches ever produced
the same filename and old copies were never looked up.

Cache files are now named deterministically from the gateway path
(UUID3 hash + filename), so repeat opens within the TTL (10 min) reuse
the existing download and skip the network entirely. Stale entries fall
through to a fresh fetch that overwrites the cache via temp-file +
rename, so a failed re-download never destroys a previously-good copy.
Content types are remembered in-memory per path so cache hits still
hand the viewer the right MIME.

Also adds isFreshCacheFile()/cacheFileNameFor() unit tests.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-15 23:05:10 +03:00
1 parent b653903fa8
commit d8be45b8fc
2 files changed
+83 -12

No files matched your search

@@ -13,6 +13,7 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -108,6 +109,31 @@ class GatewayFileClientTest {
assertEquals("a b.png", GatewayFileClient.parseFilename("inline; filename*=UTF-8''a%20b.png"))
}
@Test
fun `cacheFileNameFor is deterministic per path`() {
val first = GatewayFileClient.cacheFileNameFor("/tmp/report.pdf")
val second = GatewayFileClient.cacheFileNameFor("/tmp/report.pdf")
val other = GatewayFileClient.cacheFileNameFor("/tmp/other.pdf")
assertEquals(first, second)
assertNotEquals(first, other)
assertTrue(first.endsWith("-report.pdf"))
}
@Test
fun `isFreshCacheFile respects the TTL`() {
val dir = java.io.File(System.getProperty("java.io.tmpdir"), "gwcache_${System.nanoTime()}").apply { mkdirs() }
try {
val fresh = java.io.File(dir, "fresh").apply { writeText("x") }
fresh.setLastModified(System.currentTimeMillis() - 60_000L) // 1 min old
val stale = java.io.File(dir, "stale").apply { writeText("x") }
stale.setLastModified(System.currentTimeMillis() - 60 * 60 * 1000L) // 1 h old
assertTrue(GatewayFileClient.isFreshCacheFile(fresh))
assertFalse(GatewayFileClient.isFreshCacheFile(stale))
} finally {
dir.deleteRecursively()
}
}
@Test
fun `copyChunked copies input to output byte-for-byte`() =
runTest {