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:
M57andM57 authored and GitHub committed 2026-07-26 21:02:26 +03:00
1 parent 5e409e8dc6
commit 439ae9fcba
4 files changed
+385

No files matched your search

@@ -0,0 +1,38 @@
package com.m57.hermescontrol.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Test
class MediaTypesTest {
@Test
fun `classifies known image extensions`() {
assertEquals(MediaKind.IMAGE, mediaKindForPath("/tmp/foo.png"))
assertEquals(MediaKind.IMAGE, mediaKindForPath("a/b.jpg"))
assertEquals(MediaKind.IMAGE, mediaKindForPath("x.WEBP"))
}
@Test
fun `classifies audio and video`() {
assertEquals(MediaKind.AUDIO, mediaKindForPath("/tmp/clip.mp3"))
assertEquals(MediaKind.VIDEO, mediaKindForPath("/tmp/mov.mkv"))
}
@Test
fun `unknown extension falls back to file`() {
assertEquals(MediaKind.FILE, mediaKindForPath("/tmp/notes.txt"))
assertEquals(MediaKind.FILE, mediaKindForPath("/tmp/noext"))
}
@Test
fun `mime lookup matches kind`() {
assertEquals("image/png", mediaMimeForPath("/tmp/x.png"))
assertEquals("audio/mpeg", mediaMimeForPath("/tmp/x.mp3"))
assertEquals("application/octet-stream", mediaMimeForPath("/tmp/x.weird"))
}
@Test
fun `name extraction handles urls and paths`() {
assertEquals("report.pdf", mediaNameFromPath("https://gw/api/files/download?path=%2Ftmp%2Freport.pdf"))
assertEquals("a.png", mediaNameFromPath("/home/u/a.png"))
}
}