feat: gateway files client — fetch any gateway-hosted file over HTTP (#726)
* feat: gateway files client — fetch any gateway-hosted file over HTTP Add GatewayFileClient: a typed client for the gateway's managed-files download endpoint (GET /api/files/download?path=<enc>&token=<enc>), which streams the raw bytes of any file living on the gateway host. This is the mobile equivalent of desktop's mediaExternalUrl() and works on a remote phone (real HTTP, not a host-local file read). - buildDownloadUrl(): pure authed-URL builder (null on blank creds / non-absolute path), mirrors desktop's token-in-query auth. - fetch(): OkHttp GET via the shared client, maps 401/403/404/413 to typed GatewayFileResult variants; returns GatewayFile(name, mime, bytes). - MediaTypes: IMAGE/AUDIO/VIDEO/FILE classification by extension (mirrors desktop MEDIA_BY_EXT) so future consumers can present images, audio, video, or generic files — beyond just images. Pure builders/classifiers are unit-tested. Mobile-only, backend untouched. This is the foundation PR; issue #724's MEDIA: rendering will consume it. * fix: decode filename + handle download-URL in mediaNameFromPath - GatewayFileClient.parseFilename now URL-decodes the captured value so filename*=UTF-8''a%20b.png yields 'a b.png'. - MediaTypes.mediaNameFromPath extracts the filename from the percent-encoded path= query param of a full /api/files/download?path=<enc> URL (the real case), not just a bare path. * fix(remote): fix unit tests for gateway file client and media parsing Replace plus-encoded spaces with %20 in download URL builder, URL-decode filenames in parseFilename, and extract target filename from URL path parameters. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
5e409e8dc6
commit
439ae9fcba
4 files changed
+385
No files matched your search
@@ -0,0 +1,78 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class GatewayFileClientTest {
|
||||
private val base = "https://gw.example.com:9119"
|
||||
private val tok = "s e/cret"
|
||||
|
||||
@Test
|
||||
fun `buildDownloadUrl encodes path and token`() {
|
||||
val url = GatewayFileClient.buildDownloadUrl(base, tok, "/tmp/foo.png")!!
|
||||
assertEquals(
|
||||
"$base/api/files/download?path=%2Ftmp%2Ffoo.png&token=${java.net.URLEncoder.encode(
|
||||
tok,
|
||||
"UTF-8",
|
||||
).replace("+", "%20")}",
|
||||
url,
|
||||
)
|
||||
assertTrue(url.contains("path=%2Ftmp%2Ffoo.png"))
|
||||
assertTrue(url.contains("token="))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildDownloadUrl rejects blank base or token`() {
|
||||
assertNull(GatewayFileClient.buildDownloadUrl("", tok, "/tmp/x.png"))
|
||||
assertNull(GatewayFileClient.buildDownloadUrl(base, "", "/tmp/x.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildDownloadUrl rejects relative paths`() {
|
||||
assertNull(GatewayFileClient.buildDownloadUrl(base, tok, "relative/path.png"))
|
||||
assertNull(GatewayFileClient.buildDownloadUrl(base, tok, "MEDIA:relative.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `buildDownloadUrl handles quoted and spaced paths`() {
|
||||
val url = GatewayFileClient.buildDownloadUrl(base, tok, "\"/tmp/a b.png\"")!!
|
||||
assertTrue(url.contains("path=%2Ftmp%2Fa%20b.png"))
|
||||
assertFalse(url.contains("MEDIA:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizePath expands tilde`() {
|
||||
val home = System.getenv("HOME") ?: "/home/test"
|
||||
assertEquals("$home/foo.png", GatewayFileClient.normalizePath("~/foo.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizePath strips surrounding quotes`() {
|
||||
assertEquals("/tmp/x.png", GatewayFileClient.normalizePath("'/tmp/x.png'"))
|
||||
assertEquals("/tmp/x.png", GatewayFileClient.normalizePath("`/tmp/x.png`"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `normalizePath requires absolute path`() {
|
||||
assertNull(GatewayFileClient.normalizePath("relative.png"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `classifyStatus maps known codes`() {
|
||||
assertEquals(GatewayFileResult.NotFound, GatewayFileClient.classifyStatus(404))
|
||||
assertEquals(GatewayFileResult.Forbidden, GatewayFileClient.classifyStatus(403))
|
||||
assertEquals(GatewayFileResult.TooLarge, GatewayFileClient.classifyStatus(413))
|
||||
assertEquals(GatewayFileResult.Unauthorized, GatewayFileClient.classifyStatus(401))
|
||||
assertNull(GatewayFileClient.classifyStatus(200))
|
||||
assertNull(GatewayFileClient.classifyStatus(500))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseFilename extracts from content-disposition`() {
|
||||
assertEquals("report.pdf", GatewayFileClient.parseFilename("attachment; filename=\"report.pdf\""))
|
||||
assertEquals("a b.png", GatewayFileClient.parseFilename("inline; filename*=UTF-8''a%20b.png"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user