Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
7232395387
commit
50a0f30d8f
4 files changed
+70
-8
No files matched your search
@@ -34,6 +34,7 @@ import kotlin.coroutines.coroutineContext
|
|||||||
*/
|
*/
|
||||||
object GatewayFileClient {
|
object GatewayFileClient {
|
||||||
private const val DOWNLOAD_PATH = "/api/files/download"
|
private const val DOWNLOAD_PATH = "/api/files/download"
|
||||||
|
private const val STREAM_PATH = "/api/files/stream"
|
||||||
|
|
||||||
/** Downloads are reused for this long before a fresh fetch is made. */
|
/** Downloads are reused for this long before a fresh fetch is made. */
|
||||||
private const val CACHE_REUSE_TTL_MS = 10 * 60 * 1000L
|
private const val CACHE_REUSE_TTL_MS = 10 * 60 * 1000L
|
||||||
@@ -46,13 +47,52 @@ object GatewayFileClient {
|
|||||||
/**
|
/**
|
||||||
* Pure builder for the authenticated download URL.
|
* Pure builder for the authenticated download URL.
|
||||||
*
|
*
|
||||||
* @return the full URL, or `null` if [baseUrl]/[token] are blank or
|
* @return the full URL, or `null` if [baseUrl] is blank or [path] is not an
|
||||||
* [path] is not an absolute (or `~/`) host path.
|
* absolute (or `~/`) host path.
|
||||||
*/
|
*/
|
||||||
fun buildDownloadUrl(
|
fun buildDownloadUrl(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
token: String,
|
token: String,
|
||||||
path: String,
|
path: String,
|
||||||
|
): String? = buildUrl(baseUrl, token, path, DOWNLOAD_PATH)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure builder for the authenticated media streaming URL (`/api/files/stream`).
|
||||||
|
*
|
||||||
|
* Supports HTTP Range requests and inline content disposition for seekable audio & video (issue #920).
|
||||||
|
*/
|
||||||
|
fun buildStreamUrl(
|
||||||
|
baseUrl: String,
|
||||||
|
token: String,
|
||||||
|
path: String,
|
||||||
|
): String? = buildUrl(baseUrl, token, path, STREAM_PATH)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the best URL for a gateway host file: `/api/files/stream` for seekable audio/video
|
||||||
|
* with Range support (issue #920), and `/api/files/download` for images, documents, and other files.
|
||||||
|
*/
|
||||||
|
fun buildMediaUrl(
|
||||||
|
baseUrl: String,
|
||||||
|
token: String,
|
||||||
|
path: String,
|
||||||
|
): String? {
|
||||||
|
val kind = com.m57.hermescontrol.ui.chat.mediaKindForPath(path)
|
||||||
|
val endpoint =
|
||||||
|
if (kind == com.m57.hermescontrol.ui.chat.MediaKind.AUDIO ||
|
||||||
|
kind == com.m57.hermescontrol.ui.chat.MediaKind.VIDEO
|
||||||
|
) {
|
||||||
|
STREAM_PATH
|
||||||
|
} else {
|
||||||
|
DOWNLOAD_PATH
|
||||||
|
}
|
||||||
|
return buildUrl(baseUrl, token, path, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildUrl(
|
||||||
|
baseUrl: String,
|
||||||
|
token: String,
|
||||||
|
path: String,
|
||||||
|
endpointPath: String,
|
||||||
): String? {
|
): String? {
|
||||||
val trimmedBase = baseUrl.trimEnd('/')
|
val trimmedBase = baseUrl.trimEnd('/')
|
||||||
if (trimmedBase.isBlank()) return null
|
if (trimmedBase.isBlank()) return null
|
||||||
@@ -71,7 +111,7 @@ object GatewayFileClient {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
return "$trimmedBase$DOWNLOAD_PATH?path=$encPath$authQuery"
|
return "$trimmedBase$endpointPath?path=$encPath$authQuery"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,7 +238,7 @@ object GatewayFileClient {
|
|||||||
cacheDir: File,
|
cacheDir: File,
|
||||||
cacheName: String,
|
cacheName: String,
|
||||||
): File? {
|
): File? {
|
||||||
val body = resp.body ?: return null
|
val body = resp.body
|
||||||
val dir = cacheDir.also { it.mkdirs() }
|
val dir = cacheDir.also { it.mkdirs() }
|
||||||
sweepOldCacheFiles(dir)
|
sweepOldCacheFiles(dir)
|
||||||
val target = File(dir, cacheName)
|
val target = File(dir, cacheName)
|
||||||
|
|||||||
@@ -2867,7 +2867,7 @@ class ChatViewModel(
|
|||||||
items
|
items
|
||||||
.mapNotNull { item ->
|
.mapNotNull { item ->
|
||||||
val url =
|
val url =
|
||||||
GatewayFileClient.buildDownloadUrl(
|
GatewayFileClient.buildMediaUrl(
|
||||||
baseUrl,
|
baseUrl,
|
||||||
token,
|
token,
|
||||||
item.path,
|
item.path,
|
||||||
@@ -2985,7 +2985,8 @@ class ChatViewModel(
|
|||||||
.toSet()
|
.toSet()
|
||||||
val newAttachments =
|
val newAttachments =
|
||||||
items.mapNotNull { item ->
|
items.mapNotNull { item ->
|
||||||
val url = GatewayFileClient.buildDownloadUrl(baseUrl, "", item.path) ?: return@mapNotNull null
|
val token = AuthManager.getToken().orEmpty()
|
||||||
|
val url = GatewayFileClient.buildMediaUrl(baseUrl, token, item.path) ?: return@mapNotNull null
|
||||||
if (url in existingUrls) return@mapNotNull null
|
if (url in existingUrls) return@mapNotNull null
|
||||||
Attachment(
|
Attachment(
|
||||||
uri = url,
|
uri = url,
|
||||||
|
|||||||
@@ -1062,8 +1062,8 @@ private fun resolveImageUrl(uri: String): String {
|
|||||||
}
|
}
|
||||||
val baseUrl = runCatching { AuthManager.getBaseUrl() }.getOrDefault("")
|
val baseUrl = runCatching { AuthManager.getBaseUrl() }.getOrDefault("")
|
||||||
val token = runCatching { AuthManager.getToken() }.getOrNull().orEmpty()
|
val token = runCatching { AuthManager.getToken() }.getOrNull().orEmpty()
|
||||||
val downloadUrl = GatewayFileClient.buildDownloadUrl(baseUrl, token, trimmed)
|
val mediaUrl = GatewayFileClient.buildMediaUrl(baseUrl, token, trimmed)
|
||||||
if (downloadUrl != null) return downloadUrl
|
if (mediaUrl != null) return mediaUrl
|
||||||
|
|
||||||
if (baseUrl.isNotBlank() && (trimmed.startsWith("/api/") || trimmed.startsWith("api/"))) {
|
if (baseUrl.isNotBlank() && (trimmed.startsWith("/api/") || trimmed.startsWith("api/"))) {
|
||||||
val cleanPath = if (trimmed.startsWith("/")) trimmed else "/$trimmed"
|
val cleanPath = if (trimmed.startsWith("/")) trimmed else "/$trimmed"
|
||||||
|
|||||||
@@ -76,6 +76,27 @@ class GatewayFileClientTest {
|
|||||||
assertFalse(url.contains("MEDIA:"))
|
assertFalse(url.contains("MEDIA:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildStreamUrl encodes path and token with stream endpoint`() {
|
||||||
|
val url = GatewayFileClient.buildStreamUrl(base, tok, "/tmp/video.mp4")!!
|
||||||
|
assertTrue(url.startsWith("$base/api/files/stream?"))
|
||||||
|
assertTrue(url.contains("path=%2Ftmp%2Fvideo.mp4"))
|
||||||
|
assertTrue(url.contains("token="))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `buildMediaUrl routes audio and video to stream and others to download`() {
|
||||||
|
val videoUrl = GatewayFileClient.buildMediaUrl(base, tok, "/tmp/clip.mp4")!!
|
||||||
|
val audioUrl = GatewayFileClient.buildMediaUrl(base, tok, "/tmp/voice.ogg")!!
|
||||||
|
val imageUrl = GatewayFileClient.buildMediaUrl(base, tok, "/tmp/photo.jpg")!!
|
||||||
|
val docUrl = GatewayFileClient.buildMediaUrl(base, tok, "/tmp/report.pdf")!!
|
||||||
|
|
||||||
|
assertTrue(videoUrl.startsWith("$base/api/files/stream?"))
|
||||||
|
assertTrue(audioUrl.startsWith("$base/api/files/stream?"))
|
||||||
|
assertTrue(imageUrl.startsWith("$base/api/files/download?"))
|
||||||
|
assertTrue(docUrl.startsWith("$base/api/files/download?"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `normalizePath expands tilde`() {
|
fun `normalizePath expands tilde`() {
|
||||||
val home = System.getenv("HOME") ?: "/home/test"
|
val home = System.getenv("HOME") ?: "/home/test"
|
||||||
|
|||||||
Reference in new issue
Block a user