refactor: promote desktop upload classes to commons/jvmMain
Moves five JVM-only upload helpers from desktopApp into commons so
the CLI can reuse the same Blossom upload + AES-GCM encryption path
without depending on :desktopApp:
desktop/service/upload/DesktopBlossomAuth.kt → commons/service/upload/BlossomAuth.kt
desktop/service/upload/DesktopBlossomClient.kt → commons/service/upload/BlossomClient.kt
desktop/service/upload/DesktopMediaCompressor.kt → commons/service/upload/MediaCompressor.kt
desktop/service/upload/DesktopMediaMetadata.kt → commons/service/upload/MediaMetadata.kt
desktop/service/upload/DesktopUploadOrchestrator.kt → commons/service/upload/UploadOrchestrator.kt
API tweaks:
- Drop the `Desktop` prefix on every class.
- Rename the `DesktopMediaMetadata` object to `MediaMetadataReader`
to avoid colliding with the `MediaMetadata` data class in the same
package.
- BlossomClient no longer reaches into desktop's `DesktopHttpClient`.
Callers pass an `OkHttpClient` (or use the default one-shot client);
desktop continues to inject its Tor-aware client.
- Tighten BlossomClient's response handling — the OkHttp version on
commons' classpath has a nullable `Response.body`, so explicitly
null-check it.
Mechanical updates to keep desktop building:
- ComposeNoteDialog / ChatPane / DesktopUploadTracker switch to the
new commons imports.
- DimensionTag-from-MediaMetadata: cross-module smart cast no longer
works on the public `width`/`height` properties — replace with
`?.let { … }` chains.
- commons/jvmMain now declares `commons-imaging` (used only by
MediaCompressor's EXIF stripping).
Tests come along too: file names follow the renamed classes
(spotless ktlint enforces this).
Next commit will add `amy dm send-file --file PATH --server URL`,
which calls `UploadOrchestrator.uploadEncrypted(...)` directly.
This commit is contained in:
@@ -104,6 +104,9 @@ kotlin {
|
||||
|
||||
// Secure key storage via OS keychain (macOS/Windows/Linux)
|
||||
implementation(libs.java.keyring)
|
||||
|
||||
// EXIF stripping for image uploads (used by service/upload/MediaCompressor).
|
||||
implementation(libs.commons.imaging)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -18,14 +18,14 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent
|
||||
import java.util.Base64
|
||||
|
||||
object DesktopBlossomAuth {
|
||||
object BlossomAuth {
|
||||
suspend fun createUploadAuth(
|
||||
hash: HexKey,
|
||||
size: Long,
|
||||
+14
-8
@@ -18,9 +18,8 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -34,11 +33,16 @@ import okio.BufferedSink
|
||||
import okio.source
|
||||
import java.io.File
|
||||
|
||||
class DesktopBlossomClient(
|
||||
private val clientOverride: OkHttpClient? = null,
|
||||
/**
|
||||
* Blossom HTTP client for JVM consumers (desktop + CLI). Owns no global
|
||||
* state — pass a configured [OkHttpClient] (e.g. desktop's Tor-aware
|
||||
* `DesktopHttpClient.currentClient()`) for proxying / connection pooling.
|
||||
* The default constructor uses a fresh OkHttpClient — fine for one-shot
|
||||
* uses such as the CLI.
|
||||
*/
|
||||
class BlossomClient(
|
||||
private val okHttpClient: OkHttpClient = OkHttpClient(),
|
||||
) {
|
||||
private val okHttpClient: OkHttpClient get() = clientOverride ?: DesktopHttpClient.currentClient()
|
||||
|
||||
suspend fun upload(
|
||||
file: File,
|
||||
contentType: String,
|
||||
@@ -72,7 +76,8 @@ class DesktopBlossomClient(
|
||||
val reason = it.headers["X-Reason"] ?: it.code.toString()
|
||||
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
|
||||
}
|
||||
JsonMapper.fromJson<BlossomUploadResult>(it.body.string())
|
||||
val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body")
|
||||
JsonMapper.fromJson<BlossomUploadResult>(body.string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +108,8 @@ class DesktopBlossomClient(
|
||||
val reason = it.headers["X-Reason"] ?: it.code.toString()
|
||||
throw RuntimeException("Upload failed ($serverBaseUrl): $reason")
|
||||
}
|
||||
JsonMapper.fromJson<BlossomUploadResult>(it.body.string())
|
||||
val body = it.body ?: throw RuntimeException("Upload to $serverBaseUrl returned no body")
|
||||
JsonMapper.fromJson<BlossomUploadResult>(body.string())
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -18,14 +18,14 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import org.apache.commons.imaging.Imaging
|
||||
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
|
||||
object DesktopMediaCompressor {
|
||||
object MediaCompressor {
|
||||
fun stripExif(file: File): File {
|
||||
if (!file.name.lowercase().let { it.endsWith(".jpg") || it.endsWith(".jpeg") }) {
|
||||
return file
|
||||
+7
-2
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage
|
||||
@@ -38,7 +38,12 @@ data class MediaMetadata(
|
||||
val thumbhash: String? = null,
|
||||
)
|
||||
|
||||
object DesktopMediaMetadata {
|
||||
/**
|
||||
* Reads media metadata (sha256, size, mime-type, dimensions, blurhash, thumbhash)
|
||||
* from a JVM [File]. Named to avoid colliding with the [MediaMetadata] data class
|
||||
* in the same package.
|
||||
*/
|
||||
object MediaMetadataReader {
|
||||
fun compute(file: File): MediaMetadata {
|
||||
val bytes = file.readBytes()
|
||||
val hash = sha256(bytes).toHexKey()
|
||||
+8
-8
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
package com.vitorpamplona.amethyst.commons.service.upload
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
@@ -39,8 +39,8 @@ data class EncryptedUploadResult(
|
||||
val encryptedSize: Int,
|
||||
)
|
||||
|
||||
class DesktopUploadOrchestrator(
|
||||
private val client: DesktopBlossomClient = DesktopBlossomClient(),
|
||||
class UploadOrchestrator(
|
||||
private val client: BlossomClient = BlossomClient(),
|
||||
) {
|
||||
suspend fun upload(
|
||||
file: File,
|
||||
@@ -52,17 +52,17 @@ class DesktopUploadOrchestrator(
|
||||
// 1. Strip EXIF if requested (JPEG only)
|
||||
val processedFile =
|
||||
if (stripExif) {
|
||||
DesktopMediaCompressor.stripExif(file)
|
||||
MediaCompressor.stripExif(file)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
|
||||
// 2. Compute metadata (hash, dimensions, blurhash)
|
||||
val metadata = DesktopMediaMetadata.compute(processedFile)
|
||||
val metadata = MediaMetadataReader.compute(processedFile)
|
||||
|
||||
// 3. Create auth header
|
||||
val authHeader =
|
||||
DesktopBlossomAuth.createUploadAuth(
|
||||
BlossomAuth.createUploadAuth(
|
||||
hash = metadata.sha256,
|
||||
size = metadata.size,
|
||||
alt = alt ?: "Uploading ${file.name}",
|
||||
@@ -98,7 +98,7 @@ class DesktopUploadOrchestrator(
|
||||
signer: NostrSigner,
|
||||
): EncryptedUploadResult {
|
||||
// 1. Compute pre-encryption metadata (dimensions, blurhash, mime, originalHash)
|
||||
val metadata = DesktopMediaMetadata.compute(file)
|
||||
val metadata = MediaMetadataReader.compute(file)
|
||||
|
||||
// 2. Read file bytes and encrypt
|
||||
val plaintext = file.readBytes()
|
||||
@@ -110,7 +110,7 @@ class DesktopUploadOrchestrator(
|
||||
|
||||
// 4. Create Blossom auth with encrypted hash and size
|
||||
val authHeader =
|
||||
DesktopBlossomAuth.createUploadAuth(
|
||||
BlossomAuth.createUploadAuth(
|
||||
hash = encryptedHash,
|
||||
size = encryptedSize.toLong(),
|
||||
alt = "Encrypted upload",
|
||||
+1
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadResult
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
|
||||
+8
-8
@@ -51,12 +51,12 @@ import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTarget
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadResult
|
||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.account.AccountState
|
||||
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
|
||||
import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadTracker
|
||||
import com.vitorpamplona.amethyst.desktop.service.upload.UploadResult
|
||||
import com.vitorpamplona.amethyst.desktop.ui.compose.ComposeRelayPicker
|
||||
import com.vitorpamplona.amethyst.desktop.ui.compose.RelayPickerState
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.ClipboardPasteHandler
|
||||
@@ -103,7 +103,7 @@ fun ComposeNoteDialog(
|
||||
val attachedFiles = remember { mutableStateListOf<File>() }
|
||||
val uploadTracker = remember { DesktopUploadTracker() }
|
||||
val uploadState by uploadTracker.state.collectAsState()
|
||||
val orchestrator = remember { DesktopUploadOrchestrator() }
|
||||
val orchestrator = remember { UploadOrchestrator() }
|
||||
var selectedServer by remember { mutableStateOf(DesktopPreferences.preferredBlossomServer) }
|
||||
var postAsPicture by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -482,11 +482,11 @@ private fun buildPictureMetas(results: List<UploadResult>): List<com.vitorpamplo
|
||||
mimeType = meta.mimeType,
|
||||
blurhash = meta.blurhash,
|
||||
dimension =
|
||||
if (meta.width != null && meta.height != null) {
|
||||
com.vitorpamplona.quartz.nip94FileMetadata.tags
|
||||
.DimensionTag(meta.width, meta.height)
|
||||
} else {
|
||||
null
|
||||
meta.width?.let { w ->
|
||||
meta.height?.let { h ->
|
||||
com.vitorpamplona.quartz.nip94FileMetadata.tags
|
||||
.DimensionTag(w, h)
|
||||
}
|
||||
},
|
||||
hash = meta.sha256,
|
||||
size = meta.size.toInt(),
|
||||
|
||||
+4
-6
@@ -86,6 +86,7 @@ import androidx.compose.ui.window.PopupProperties
|
||||
import com.vitorpamplona.amethyst.commons.model.IAccount
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.commons.ui.chat.ChatMessageCompose
|
||||
import com.vitorpamplona.amethyst.commons.ui.chat.ChatroomHeader
|
||||
import com.vitorpamplona.amethyst.commons.ui.chat.DmBroadcastBanner
|
||||
@@ -97,7 +98,6 @@ import com.vitorpamplona.amethyst.commons.util.toTimeAgo
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ChatNewMessageState
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ChatroomFeedViewModel
|
||||
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
|
||||
import com.vitorpamplona.amethyst.desktop.service.upload.DesktopUploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.DesktopFilePicker
|
||||
import com.vitorpamplona.amethyst.desktop.ui.media.MediaAttachmentRow
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
@@ -801,7 +801,7 @@ private suspend fun sendEncryptedFiles(
|
||||
account: IAccount,
|
||||
cacheProvider: ICacheProvider,
|
||||
) {
|
||||
val orchestrator = DesktopUploadOrchestrator()
|
||||
val orchestrator = UploadOrchestrator()
|
||||
val server = DesktopPreferences.preferredBlossomServer
|
||||
val recipients = roomKey.users.mapNotNull { cacheProvider.getUserIfExists(it) }.map { it.toPTag() }
|
||||
|
||||
@@ -819,10 +819,8 @@ private suspend fun sendEncryptedFiles(
|
||||
hash = result.encryptedHash,
|
||||
size = result.encryptedSize,
|
||||
dimension =
|
||||
if (result.metadata.width != null && result.metadata.height != null) {
|
||||
DimensionTag(result.metadata.width, result.metadata.height)
|
||||
} else {
|
||||
null
|
||||
result.metadata.width?.let { w ->
|
||||
result.metadata.height?.let { h -> DimensionTag(w, h) }
|
||||
},
|
||||
blurhash = result.metadata.blurhash,
|
||||
thumbhash = result.metadata.thumbhash,
|
||||
|
||||
+6
-6
@@ -37,7 +37,7 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopBlossomClientTest {
|
||||
class BlossomClientTest {
|
||||
private fun mockOkHttp(
|
||||
responseCode: Int,
|
||||
body: String = "",
|
||||
@@ -67,7 +67,7 @@ class DesktopBlossomClientTest {
|
||||
runTest {
|
||||
val json =
|
||||
"""{"url":"https://blossom.example.com/abc123.png","sha256":"abc123","size":1024}"""
|
||||
val client = DesktopBlossomClient(mockOkHttp(200, json))
|
||||
val client = BlossomClient(mockOkHttp(200, json))
|
||||
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
@@ -94,7 +94,7 @@ class DesktopBlossomClientTest {
|
||||
fun uploadFailureThrowsException() =
|
||||
runTest {
|
||||
val headers = Headers.headersOf("X-Reason", "File too large")
|
||||
val client = DesktopBlossomClient(mockOkHttp(413, "", headers))
|
||||
val client = BlossomClient(mockOkHttp(413, "", headers))
|
||||
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
@@ -119,7 +119,7 @@ class DesktopBlossomClientTest {
|
||||
@Test
|
||||
fun uploadFailureUsesStatusCodeWhenNoXReason() =
|
||||
runTest {
|
||||
val client = DesktopBlossomClient(mockOkHttp(500))
|
||||
val client = BlossomClient(mockOkHttp(500))
|
||||
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
@@ -159,7 +159,7 @@ class DesktopBlossomClientTest {
|
||||
.body("""{"url":"https://example.com/hash"}""".toResponseBody())
|
||||
.build()
|
||||
|
||||
val client = DesktopBlossomClient(mockClient)
|
||||
val client = BlossomClient(mockClient)
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
file.writeBytes(byteArrayOf(1))
|
||||
@@ -199,7 +199,7 @@ class DesktopBlossomClientTest {
|
||||
.body("""{"url":"https://example.com/hash"}""".toResponseBody())
|
||||
.build()
|
||||
|
||||
val client = DesktopBlossomClient(mockClient)
|
||||
val client = BlossomClient(mockClient)
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
file.writeBytes(byteArrayOf(1))
|
||||
+2
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.service.upload
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.MediaMetadata
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.UploadResult
|
||||
import com.vitorpamplona.quartz.nipB7Blossom.BlossomUploadResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
||||
+7
-7
@@ -27,7 +27,7 @@ import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopMediaCompressorTest {
|
||||
class MediaCompressorTest {
|
||||
@Test
|
||||
fun stripExifReturnsSameFileForPng() {
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
@@ -35,7 +35,7 @@ class DesktopMediaCompressorTest {
|
||||
val img = BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB)
|
||||
ImageIO.write(img, "png", file)
|
||||
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
|
||||
// Should return the same file object since it's not JPEG
|
||||
assertEquals(file, result)
|
||||
@@ -48,7 +48,7 @@ class DesktopMediaCompressorTest {
|
||||
file.deleteOnExit()
|
||||
file.writeText("not a jpeg")
|
||||
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
|
||||
assertEquals(file, result)
|
||||
file.delete()
|
||||
@@ -60,7 +60,7 @@ class DesktopMediaCompressorTest {
|
||||
file.deleteOnExit()
|
||||
file.writeBytes(byteArrayOf(0, 0, 0))
|
||||
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
|
||||
assertEquals(file, result)
|
||||
file.delete()
|
||||
@@ -71,7 +71,7 @@ class DesktopMediaCompressorTest {
|
||||
// Create a minimal JPEG without EXIF
|
||||
val file = createMinimalJpeg()
|
||||
try {
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
// Should return the same file since there's no EXIF to strip
|
||||
assertEquals(file, result)
|
||||
} finally {
|
||||
@@ -87,7 +87,7 @@ class DesktopMediaCompressorTest {
|
||||
val img = BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB)
|
||||
ImageIO.write(img, "jpg", file)
|
||||
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
|
||||
// Result should be a valid file regardless
|
||||
assertTrue(result.exists())
|
||||
@@ -107,7 +107,7 @@ class DesktopMediaCompressorTest {
|
||||
val img = BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB)
|
||||
ImageIO.write(img, "jpg", file)
|
||||
|
||||
val result = DesktopMediaCompressor.stripExif(file)
|
||||
val result = MediaCompressor.stripExif(file)
|
||||
|
||||
assertTrue(result.exists())
|
||||
if (result != file) result.delete()
|
||||
+23
-23
@@ -29,60 +29,60 @@ import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopMediaMetadataTest {
|
||||
class MediaMetadataReaderTest {
|
||||
// --- guessMimeType ---
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForJpeg() {
|
||||
assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.jpg")))
|
||||
assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.jpeg")))
|
||||
assertEquals("image/jpeg", DesktopMediaMetadata.guessMimeType(File("photo.JPEG")))
|
||||
assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.jpg")))
|
||||
assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.jpeg")))
|
||||
assertEquals("image/jpeg", MediaMetadataReader.guessMimeType(File("photo.JPEG")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForPng() {
|
||||
assertEquals("image/png", DesktopMediaMetadata.guessMimeType(File("image.png")))
|
||||
assertEquals("image/png", MediaMetadataReader.guessMimeType(File("image.png")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForGif() {
|
||||
assertEquals("image/gif", DesktopMediaMetadata.guessMimeType(File("anim.gif")))
|
||||
assertEquals("image/gif", MediaMetadataReader.guessMimeType(File("anim.gif")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForWebp() {
|
||||
assertEquals("image/webp", DesktopMediaMetadata.guessMimeType(File("image.webp")))
|
||||
assertEquals("image/webp", MediaMetadataReader.guessMimeType(File("image.webp")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForSvg() {
|
||||
assertEquals("image/svg+xml", DesktopMediaMetadata.guessMimeType(File("icon.svg")))
|
||||
assertEquals("image/svg+xml", MediaMetadataReader.guessMimeType(File("icon.svg")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForAvif() {
|
||||
assertEquals("image/avif", DesktopMediaMetadata.guessMimeType(File("photo.avif")))
|
||||
assertEquals("image/avif", MediaMetadataReader.guessMimeType(File("photo.avif")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForVideoFormats() {
|
||||
assertEquals("video/mp4", DesktopMediaMetadata.guessMimeType(File("clip.mp4")))
|
||||
assertEquals("video/webm", DesktopMediaMetadata.guessMimeType(File("clip.webm")))
|
||||
assertEquals("video/quicktime", DesktopMediaMetadata.guessMimeType(File("clip.mov")))
|
||||
assertEquals("video/mp4", MediaMetadataReader.guessMimeType(File("clip.mp4")))
|
||||
assertEquals("video/webm", MediaMetadataReader.guessMimeType(File("clip.webm")))
|
||||
assertEquals("video/quicktime", MediaMetadataReader.guessMimeType(File("clip.mov")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForAudioFormats() {
|
||||
assertEquals("audio/mpeg", DesktopMediaMetadata.guessMimeType(File("song.mp3")))
|
||||
assertEquals("audio/ogg", DesktopMediaMetadata.guessMimeType(File("track.ogg")))
|
||||
assertEquals("audio/wav", DesktopMediaMetadata.guessMimeType(File("sound.wav")))
|
||||
assertEquals("audio/flac", DesktopMediaMetadata.guessMimeType(File("lossless.flac")))
|
||||
assertEquals("audio/mpeg", MediaMetadataReader.guessMimeType(File("song.mp3")))
|
||||
assertEquals("audio/ogg", MediaMetadataReader.guessMimeType(File("track.ogg")))
|
||||
assertEquals("audio/wav", MediaMetadataReader.guessMimeType(File("sound.wav")))
|
||||
assertEquals("audio/flac", MediaMetadataReader.guessMimeType(File("lossless.flac")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun guessMimeTypeForUnknownExtension() {
|
||||
assertEquals("application/octet-stream", DesktopMediaMetadata.guessMimeType(File("data.xyz")))
|
||||
assertEquals("application/octet-stream", DesktopMediaMetadata.guessMimeType(File("noext")))
|
||||
assertEquals("application/octet-stream", MediaMetadataReader.guessMimeType(File("data.xyz")))
|
||||
assertEquals("application/octet-stream", MediaMetadataReader.guessMimeType(File("noext")))
|
||||
}
|
||||
|
||||
// --- compute ---
|
||||
@@ -91,7 +91,7 @@ class DesktopMediaMetadataTest {
|
||||
fun computeForPngImage() {
|
||||
val file = createTempPng(width = 10, height = 5)
|
||||
try {
|
||||
val meta = DesktopMediaMetadata.compute(file)
|
||||
val meta = MediaMetadataReader.compute(file)
|
||||
|
||||
assertEquals("image/png", meta.mimeType)
|
||||
assertTrue(meta.size > 0)
|
||||
@@ -110,7 +110,7 @@ class DesktopMediaMetadataTest {
|
||||
file.deleteOnExit()
|
||||
file.writeText("hello world")
|
||||
try {
|
||||
val meta = DesktopMediaMetadata.compute(file)
|
||||
val meta = MediaMetadataReader.compute(file)
|
||||
|
||||
assertEquals("application/octet-stream", meta.mimeType)
|
||||
assertEquals(11L, meta.size)
|
||||
@@ -129,8 +129,8 @@ class DesktopMediaMetadataTest {
|
||||
file.deleteOnExit()
|
||||
file.writeBytes(byteArrayOf(1, 2, 3, 4, 5))
|
||||
try {
|
||||
val meta1 = DesktopMediaMetadata.compute(file)
|
||||
val meta2 = DesktopMediaMetadata.compute(file)
|
||||
val meta1 = MediaMetadataReader.compute(file)
|
||||
val meta2 = MediaMetadataReader.compute(file)
|
||||
assertEquals(meta1.sha256, meta2.sha256)
|
||||
} finally {
|
||||
file.delete()
|
||||
@@ -143,7 +143,7 @@ class DesktopMediaMetadataTest {
|
||||
file.deleteOnExit()
|
||||
file.writeBytes(byteArrayOf(0, 0, 0))
|
||||
try {
|
||||
val meta = DesktopMediaMetadata.compute(file)
|
||||
val meta = MediaMetadataReader.compute(file)
|
||||
assertEquals("video/mp4", meta.mimeType)
|
||||
assertNull(meta.width)
|
||||
assertNull(meta.height)
|
||||
+12
-12
@@ -36,24 +36,24 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class DesktopUploadOrchestratorTest {
|
||||
class UploadOrchestratorTest {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
mockkObject(DesktopBlossomAuth)
|
||||
mockkObject(BlossomAuth)
|
||||
coEvery {
|
||||
DesktopBlossomAuth.createUploadAuth(any(), any(), any(), any())
|
||||
BlossomAuth.createUploadAuth(any(), any(), any(), any())
|
||||
} returns "Nostr fakeAuthToken"
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun teardown() {
|
||||
unmockkObject(DesktopBlossomAuth)
|
||||
unmockkObject(BlossomAuth)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uploadCallsClientWithCorrectParameters() =
|
||||
runTest {
|
||||
val mockClient = mockk<DesktopBlossomClient>()
|
||||
val mockClient = mockk<BlossomClient>()
|
||||
val fileSlot = slot<File>()
|
||||
val contentTypeSlot = slot<String>()
|
||||
val urlSlot = slot<String>()
|
||||
@@ -72,7 +72,7 @@ class DesktopUploadOrchestratorTest {
|
||||
size = 100,
|
||||
)
|
||||
|
||||
val orchestrator = DesktopUploadOrchestrator(mockClient)
|
||||
val orchestrator = UploadOrchestrator(mockClient)
|
||||
|
||||
val file = File.createTempFile("test_", ".png")
|
||||
file.deleteOnExit()
|
||||
@@ -112,7 +112,7 @@ class DesktopUploadOrchestratorTest {
|
||||
@Test
|
||||
fun uploadPassesSameFileWhenNoStripExif() =
|
||||
runTest {
|
||||
val mockClient = mockk<DesktopBlossomClient>()
|
||||
val mockClient = mockk<BlossomClient>()
|
||||
val fileSlot = slot<File>()
|
||||
|
||||
coEvery {
|
||||
@@ -124,7 +124,7 @@ class DesktopUploadOrchestratorTest {
|
||||
)
|
||||
} returns BlossomUploadResult(url = "https://example.com/hash")
|
||||
|
||||
val orchestrator = DesktopUploadOrchestrator(mockClient)
|
||||
val orchestrator = UploadOrchestrator(mockClient)
|
||||
|
||||
val file = File.createTempFile("test_", ".txt")
|
||||
file.deleteOnExit()
|
||||
@@ -150,13 +150,13 @@ class DesktopUploadOrchestratorTest {
|
||||
@Test
|
||||
fun uploadComputesMetadata() =
|
||||
runTest {
|
||||
val mockClient = mockk<DesktopBlossomClient>()
|
||||
val mockClient = mockk<BlossomClient>()
|
||||
|
||||
coEvery {
|
||||
mockClient.upload(any<java.io.File>(), any<String>(), any<String>(), any())
|
||||
} returns BlossomUploadResult(url = "https://example.com/hash")
|
||||
|
||||
val orchestrator = DesktopUploadOrchestrator(mockClient)
|
||||
val orchestrator = UploadOrchestrator(mockClient)
|
||||
|
||||
val file = File.createTempFile("test_", ".txt")
|
||||
file.deleteOnExit()
|
||||
@@ -185,7 +185,7 @@ class DesktopUploadOrchestratorTest {
|
||||
@Test
|
||||
fun uploadPassesAuthHeaderToClient() =
|
||||
runTest {
|
||||
val mockClient = mockk<DesktopBlossomClient>()
|
||||
val mockClient = mockk<BlossomClient>()
|
||||
val authSlot = slot<String?>()
|
||||
|
||||
coEvery {
|
||||
@@ -197,7 +197,7 @@ class DesktopUploadOrchestratorTest {
|
||||
)
|
||||
} returns BlossomUploadResult(url = "https://example.com/hash")
|
||||
|
||||
val orchestrator = DesktopUploadOrchestrator(mockClient)
|
||||
val orchestrator = UploadOrchestrator(mockClient)
|
||||
|
||||
val file = File.createTempFile("test_", ".txt")
|
||||
file.deleteOnExit()
|
||||
Reference in New Issue
Block a user