diff --git a/firebase-storage/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt b/firebase-storage/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt new file mode 100644 index 000000000..22d885915 --- /dev/null +++ b/firebase-storage/src/androidInstrumentedTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt @@ -0,0 +1,7 @@ +package dev.gitlive.firebase.storage + +import android.net.Uri + +actual fun createTestData(): Data { + return Data("test".toByteArray()) +} \ No newline at end of file diff --git a/firebase-storage/src/androidMain/kotlin/dev/gitlive/firebase/storage/storage.kt b/firebase-storage/src/androidMain/kotlin/dev/gitlive/firebase/storage/storage.kt index 868d380a5..b1c678d60 100644 --- a/firebase-storage/src/androidMain/kotlin/dev/gitlive/firebase/storage/storage.kt +++ b/firebase-storage/src/androidMain/kotlin/dev/gitlive/firebase/storage/storage.kt @@ -8,8 +8,10 @@ package dev.gitlive.firebase.storage import android.net.Uri import com.google.android.gms.tasks.OnCanceledListener import com.google.android.gms.tasks.OnCompleteListener +import com.google.android.gms.tasks.Task import com.google.firebase.storage.OnPausedListener import com.google.firebase.storage.OnProgressListener +import com.google.firebase.storage.StorageMetadata import com.google.firebase.storage.UploadTask import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp @@ -18,7 +20,10 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await actual val Firebase.storage get() = @@ -57,6 +62,8 @@ actual class StorageReference(val android: com.google.firebase.storage.StorageRe actual val root: StorageReference get() = StorageReference(android.root) actual val storage: FirebaseStorage get() = FirebaseStorage(android.storage) + actual suspend fun getMetadata(): FirebaseStorageMetadata? = android.metadata.await().toFirebaseStorageMetadata() + actual fun child(path: String): StorageReference = StorageReference(android.child(path)) actual suspend fun delete() = android.delete().await().run { Unit } @@ -65,10 +72,28 @@ actual class StorageReference(val android: com.google.firebase.storage.StorageRe actual suspend fun listAll(): ListResult = ListResult(android.listAll().await()) - actual suspend fun putFile(file: File) = android.putFile(file.uri).await().run {} + actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?) { + if (metadata != null) { + android.putFile(file.uri, metadata.toStorageMetadata()).await().run {} + } else { + android.putFile(file.uri).await().run {} + } + } - actual fun putFileResumable(file: File): ProgressFlow { - val android = android.putFile(file.uri) + actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?) { + if (metadata != null) { + android.putBytes(data.data, metadata.toStorageMetadata()).await().run {} + } else { + android.putBytes(data.data).await().run {} + } + } + + actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow { + val android = if (metadata != null) { + android.putFile(file.uri, metadata.toStorageMetadata()) + } else { + android.putFile(file.uri) + } val flow = callbackFlow { val onCanceledListener = OnCanceledListener { cancel() } @@ -104,4 +129,35 @@ actual class ListResult(android: com.google.firebase.storage.ListResult) { actual class File(val uri: Uri) +actual class Data(val data: ByteArray) + actual typealias FirebaseStorageException = com.google.firebase.storage.StorageException + +fun FirebaseStorageMetadata.toStorageMetadata(): StorageMetadata { + return StorageMetadata.Builder() + .setCacheControl(this.cacheControl) + .setContentDisposition(this.contentDisposition) + .setContentEncoding(this.contentEncoding) + .setContentLanguage(this.contentLanguage) + .setContentType(this.contentType) + .apply { + customMetadata.entries.forEach { + (key, value) -> setCustomMetadata(key, value) + } + }.build() +} + +fun StorageMetadata.toFirebaseStorageMetadata(): FirebaseStorageMetadata { + val sdkMetadata = this + return storageMetadata { + md5Hash = sdkMetadata.md5Hash + cacheControl = sdkMetadata.cacheControl + contentDisposition = sdkMetadata.contentDisposition + contentEncoding = sdkMetadata.contentEncoding + contentLanguage = sdkMetadata.contentLanguage + contentType = sdkMetadata.contentType + sdkMetadata.customMetadataKeys.forEach { + setCustomMetadata(it, sdkMetadata.getCustomMetadata(it)) + } + } +} \ No newline at end of file diff --git a/firebase-storage/src/androidUnitTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt b/firebase-storage/src/androidUnitTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt new file mode 100644 index 000000000..22d885915 --- /dev/null +++ b/firebase-storage/src/androidUnitTest/kotlin/dev/gitlive/firebase/storage/storage.android.kt @@ -0,0 +1,7 @@ +package dev.gitlive.firebase.storage + +import android.net.Uri + +actual fun createTestData(): Data { + return Data("test".toByteArray()) +} \ No newline at end of file diff --git a/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt b/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt index ada31266b..3f0037ff3 100644 --- a/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt +++ b/firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt @@ -4,6 +4,9 @@ import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseApp import dev.gitlive.firebase.FirebaseException import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch /** Returns the [FirebaseStorage] instance of the default [FirebaseApp]. */ expect val Firebase.storage: FirebaseStorage @@ -32,6 +35,8 @@ expect class StorageReference { val root: StorageReference val storage: FirebaseStorage + suspend fun getMetadata(): FirebaseStorageMetadata? + fun child(path: String): StorageReference suspend fun delete() @@ -40,9 +45,11 @@ expect class StorageReference { suspend fun listAll(): ListResult - suspend fun putFile(file: File) + suspend fun putFile(file: File, metadata: FirebaseStorageMetadata? = null) + + suspend fun putData(data: Data, metadata: FirebaseStorageMetadata? = null) - fun putFileResumable(file: File): ProgressFlow + fun putFileResumable(file: File, metadata: FirebaseStorageMetadata? = null): ProgressFlow } expect class ListResult { @@ -53,6 +60,8 @@ expect class ListResult { expect class File +expect class Data + sealed class Progress(val bytesTransferred: Number, val totalByteCount: Number) { class Running internal constructor(bytesTransferred: Number, totalByteCount: Number): Progress(bytesTransferred, totalByteCount) class Paused internal constructor(bytesTransferred: Number, totalByteCount: Number): Progress(bytesTransferred, totalByteCount) @@ -64,5 +73,26 @@ interface ProgressFlow : Flow { fun cancel() } - expect class FirebaseStorageException : FirebaseException + +data class FirebaseStorageMetadata( + var md5Hash: String? = null, + var cacheControl: String? = null, + var contentDisposition: String? = null, + var contentEncoding: String? = null, + var contentLanguage: String? = null, + var contentType: String? = null, + var customMetadata: MutableMap = mutableMapOf() +) { + fun setCustomMetadata(key: String, value: String?) { + value?.let { + customMetadata[key] = it + } + } +} + +fun storageMetadata(init: FirebaseStorageMetadata.() -> Unit): FirebaseStorageMetadata { + val metadata = FirebaseStorageMetadata() + metadata.init() + return metadata +} \ No newline at end of file diff --git a/firebase-storage/src/commonTest/kotlin/dev/gitlive/firebase/storage/storage.kt b/firebase-storage/src/commonTest/kotlin/dev/gitlive/firebase/storage/storage.kt index 0b96512af..cf5fbc127 100644 --- a/firebase-storage/src/commonTest/kotlin/dev/gitlive/firebase/storage/storage.kt +++ b/firebase-storage/src/commonTest/kotlin/dev/gitlive/firebase/storage/storage.kt @@ -8,7 +8,16 @@ import dev.gitlive.firebase.Firebase import dev.gitlive.firebase.FirebaseOptions import dev.gitlive.firebase.apps import dev.gitlive.firebase.initialize +import dev.gitlive.firebase.runBlockingTest +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull expect val emulatorHost: String expect val context: Any @@ -35,6 +44,61 @@ class FirebaseStorageTest { storage = Firebase.storage(app).apply { useEmulator(emulatorHost, 9199) + setMaxOperationRetryTimeMillis(10000) + setMaxUploadRetryTimeMillis(10000) } } -} \ No newline at end of file + + @AfterTest + fun deinitializeFirebase() = runBlockingTest { + Firebase.apps(context).forEach { + it.delete() + } + } + + @Test + fun testStorageNotNull() { + assertNotNull(storage) + } + + @Test + fun testUploadShouldNotCrash() = runBlockingTest { + val data = createTestData() + val ref = storage.reference("test").child("testFile.txt") + ref.putData(data) + } + + @Test + fun testUploadMetadata() = runBlockingTest { + val data = createTestData() + val ref = storage.reference("test").child("testFile.txt") + val metadata = storageMetadata { + contentType = "text/plain" + } + ref.putData(data, metadata) + + val metadataResult = ref.getMetadata() + + assertNotNull(metadataResult) + assertNotNull(metadataResult.contentType) + assertEquals(metadataResult.contentType, metadata.contentType) + } + + @Test + fun testUploadCustomMetadata() = runBlockingTest { + val data = createTestData() + val ref = storage.reference("test").child("testFile.txt") + val metadata = storageMetadata { + contentType = "text/plain" + setCustomMetadata("key", "value") + } + ref.putData(data, metadata) + + val metadataResult = ref.getMetadata() + + assertNotNull(metadataResult) + assertEquals(metadataResult.customMetadata["key"], metadata.customMetadata["key"]) + } +} + +expect fun createTestData(): Data \ No newline at end of file diff --git a/firebase-storage/src/iosMain/kotlin/dev/gitlive/firebase/storage/storage.kt b/firebase-storage/src/iosMain/kotlin/dev/gitlive/firebase/storage/storage.kt index 3a988926e..610f72e66 100644 --- a/firebase-storage/src/iosMain/kotlin/dev/gitlive/firebase/storage/storage.kt +++ b/firebase-storage/src/iosMain/kotlin/dev/gitlive/firebase/storage/storage.kt @@ -6,6 +6,7 @@ package dev.gitlive.firebase.storage import cocoapods.FirebaseStorage.FIRStorage import cocoapods.FirebaseStorage.FIRStorageListResult +import cocoapods.FirebaseStorage.FIRStorageMetadata import cocoapods.FirebaseStorage.FIRStorageReference import cocoapods.FirebaseStorage.FIRStorageTaskStatusFailure import cocoapods.FirebaseStorage.FIRStorageTaskStatusPause @@ -22,6 +23,7 @@ import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.emitAll +import platform.Foundation.NSData import platform.Foundation.NSError import platform.Foundation.NSURL @@ -64,6 +66,16 @@ actual class StorageReference(val ios: FIRStorageReference) { actual fun child(path: String): StorageReference = StorageReference(ios.child(path)) + actual suspend fun getMetadata(): FirebaseStorageMetadata? = ios.awaitResult { + metadataWithCompletion { metadata, error -> + if (error == null) { + it.invoke(metadata?.toFirebaseStorageMetadata(), null) + } else { + it.invoke(null, error) + } + } + } + actual suspend fun delete() = await { ios.deleteWithCompletion(it) } actual suspend fun getDownloadUrl(): String = ios.awaitResult { @@ -76,10 +88,16 @@ actual class StorageReference(val ios: FIRStorageReference) { } } - actual suspend fun putFile(file: File) = ios.awaitResult { putFile(file.url, null, completion = it) }.run {} + actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?) = ios.awaitResult { callback -> + putFile(file.url, metadata?.toFIRMetadata(), callback) + }.run {} + + actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?) = ios.awaitResult { callback -> + putData(data.data, metadata?.toFIRMetadata(), callback) + }.run {} - actual fun putFileResumable(file: File): ProgressFlow { - val ios = ios.putFile(file.url) + actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow { + val ios = ios.putFile(file.url, metadata?.toFIRMetadata()) val flow = callbackFlow { ios.observeStatus(FIRStorageTaskStatusProgress) { @@ -122,6 +140,8 @@ actual class ListResult(ios: FIRStorageListResult) { actual class File(val url: NSURL) +actual class Data(val data: NSData) + actual class FirebaseStorageException(message: String): FirebaseException(message) suspend inline fun T.await(function: T.(callback: (NSError?) -> Unit) -> Unit) { @@ -147,3 +167,32 @@ suspend inline fun T.awaitResult(function: T.(callback: (R?, NSEr } return job.await() as R } + +fun FirebaseStorageMetadata.toFIRMetadata(): FIRStorageMetadata { + val metadata = FIRStorageMetadata() + val mappedMetadata: Map = this.customMetadata.map { + it.key to it.value + }.toMap() + metadata.setCustomMetadata(mappedMetadata) + metadata.setCacheControl(this.cacheControl) + metadata.setContentDisposition(this.contentDisposition) + metadata.setContentEncoding(this.contentEncoding) + metadata.setContentLanguage(this.contentLanguage) + metadata.setContentType(this.contentType) + return metadata +} + +fun FIRStorageMetadata.toFirebaseStorageMetadata(): FirebaseStorageMetadata { + val sdkMetadata = this + return storageMetadata { + md5Hash = sdkMetadata.md5Hash() + cacheControl = sdkMetadata.cacheControl() + contentDisposition = sdkMetadata.contentDisposition() + contentEncoding = sdkMetadata.contentEncoding() + contentLanguage = sdkMetadata.contentLanguage() + contentType = sdkMetadata.contentType() + sdkMetadata.customMetadata()?.forEach { + setCustomMetadata(it.key.toString(), it.value.toString()) + } + } +} \ No newline at end of file diff --git a/firebase-storage/src/iosTest/kotlin/dev/gitlive/firebase/storage/storage.ios.kt b/firebase-storage/src/iosTest/kotlin/dev/gitlive/firebase/storage/storage.ios.kt new file mode 100644 index 000000000..b2da088d5 --- /dev/null +++ b/firebase-storage/src/iosTest/kotlin/dev/gitlive/firebase/storage/storage.ios.kt @@ -0,0 +1,20 @@ +package dev.gitlive.firebase.storage + +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.utf8 +import platform.Foundation.NSCoder +import platform.Foundation.NSData +import platform.Foundation.NSSearchPathDirectory +import platform.Foundation.NSSearchPathDomainMask +import platform.Foundation.NSSearchPathForDirectoriesInDomains +import platform.Foundation.NSString +import platform.Foundation.NSURL +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.create +import platform.Foundation.dataUsingEncoding + +@OptIn(BetaInteropApi::class) +actual fun createTestData(): Data { + val value = NSString.create(string = "test") + return Data(value.dataUsingEncoding(NSUTF8StringEncoding, false)!!) +} \ No newline at end of file diff --git a/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/externals/storage.kt b/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/externals/storage.kt index dbd7768d4..52fca0864 100644 --- a/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/externals/storage.kt +++ b/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/externals/storage.kt @@ -13,15 +13,17 @@ external fun ref(ref: StorageReference, url: String? = definedExternally): Stora external fun getDownloadURL(ref: StorageReference): Promise -external fun uploadBytes(ref: StorageReference, file: dynamic): Promise +external fun getMetadata(ref: StorageReference): Promise -external fun uploadBytesResumable(ref: StorageReference, data: dynamic): UploadTask +external fun uploadBytes(ref: StorageReference, file: dynamic, metadata: StorageMetadata?): Promise + +external fun uploadBytesResumable(ref: StorageReference, data: dynamic, metadata: StorageMetadata?): UploadTask external fun deleteObject(ref: StorageReference): Promise; external fun listAll(ref: StorageReference): Promise; -external fun connectFirestoreEmulator( +external fun connectStorageEmulator( storage: FirebaseStorage, host: String, port: Double, @@ -58,6 +60,25 @@ external interface UploadTaskSnapshot { val totalBytes: Number } +external class StorageMetadata { + val bucket: String? + var cacheControl: String? + var contentDisposition: String? + var contentEncoding: String? + var contentLanguage: String? + var contentType: String? + var customMetadata: Map? + val fullPath: String? + val generation: String? + val md5Hash: String? + val metageneration: String? + val name: String? + val size: Number? + val timeCreated: String? + val updated: String? + +} + external class UploadTask : Promise { fun cancel(): Boolean; fun on(event: String, next: (snapshot: UploadTaskSnapshot) -> Unit, error: (a: StorageError) -> Unit, complete: () -> Unit): () -> Unit diff --git a/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/storage.kt b/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/storage.kt index 9ba0380d5..1ab30ebb7 100644 --- a/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/storage.kt +++ b/firebase-storage/src/jsMain/kotlin/dev/gitlive/firebase/storage/storage.kt @@ -34,7 +34,7 @@ actual class FirebaseStorage(val js: dev.gitlive.firebase.storage.externals.Fire } actual fun useEmulator(host: String, port: Int) { - connectFirestoreEmulator(js, host, port.toDouble()) + connectStorageEmulator(js, host, port.toDouble()) } actual val reference: StorageReference get() = StorageReference(ref(js)) @@ -51,6 +51,8 @@ actual class StorageReference(val js: dev.gitlive.firebase.storage.externals.Sto actual val root: StorageReference get() = StorageReference(js.root) actual val storage: FirebaseStorage get() = FirebaseStorage(js.storage) + actual suspend fun getMetadata(): FirebaseStorageMetadata? = rethrow { getMetadata(js).await().toFirebaseStorageMetadata() } + actual fun child(path: String): StorageReference = StorageReference(ref(js, path)) actual suspend fun delete() = rethrow { deleteObject(js).await() } @@ -59,10 +61,12 @@ actual class StorageReference(val js: dev.gitlive.firebase.storage.externals.Sto actual suspend fun listAll(): ListResult = rethrow { ListResult(listAll(js).await()) } - actual suspend fun putFile(file: File): Unit = rethrow { uploadBytes(js, file).await() } + actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?): Unit = rethrow { uploadBytes(js, file, metadata?.toStorageMetadata()).await() } + + actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?): Unit = rethrow { uploadBytes(js, data.data, metadata?.toStorageMetadata()).await() } - actual fun putFileResumable(file: File): ProgressFlow = rethrow { - val uploadTask = uploadBytesResumable(js, file) + actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow = rethrow { + val uploadTask = uploadBytesResumable(js, file, metadata?.toStorageMetadata()) val flow = callbackFlow { val unsubscribe = uploadTask.on( @@ -99,6 +103,7 @@ actual class ListResult(js: dev.gitlive.firebase.storage.externals.ListResult) { } actual typealias File = org.w3c.files.File +actual class Data(val data: org.khronos.webgl.Uint8Array) actual open class FirebaseStorageException(code: String, cause: Throwable) : FirebaseException(code, cause) @@ -123,4 +128,31 @@ internal fun errorToException(error: dynamic) = (error?.code ?: error?.message ? FirebaseStorageException(code, error) } } - } \ No newline at end of file + } + + +fun StorageMetadata.toFirebaseStorageMetadata(): FirebaseStorageMetadata { + val sdkMetadata = this + return storageMetadata { + md5Hash = sdkMetadata.md5Hash + cacheControl = sdkMetadata.cacheControl + contentDisposition = sdkMetadata.contentDisposition + contentEncoding = sdkMetadata.contentEncoding + contentLanguage = sdkMetadata.contentLanguage + contentType = sdkMetadata.contentType + sdkMetadata.customMetadata?.entries?.forEach { + setCustomMetadata(it.key, it.value) + } + } +} + +fun FirebaseStorageMetadata.toStorageMetadata(): StorageMetadata { + val metadata = StorageMetadata() + metadata.cacheControl = cacheControl + metadata.contentDisposition = contentDisposition + metadata.contentEncoding = contentEncoding + metadata.contentLanguage = contentLanguage + metadata.contentType = contentType + metadata.customMetadata = customMetadata + return metadata +} \ No newline at end of file diff --git a/firebase-storage/src/jsTest/kotlin/dev/gitlive/firebase/storage/storage.js.kt b/firebase-storage/src/jsTest/kotlin/dev/gitlive/firebase/storage/storage.js.kt new file mode 100644 index 000000000..8a8c266b6 --- /dev/null +++ b/firebase-storage/src/jsTest/kotlin/dev/gitlive/firebase/storage/storage.js.kt @@ -0,0 +1,5 @@ +package dev.gitlive.firebase.storage + +actual fun createTestData(): Data { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/firebase-storage/src/jvmMain/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt b/firebase-storage/src/jvmMain/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt index cc61e378b..ef187c0d2 100644 --- a/firebase-storage/src/jvmMain/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt +++ b/firebase-storage/src/jvmMain/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt @@ -51,6 +51,10 @@ actual class StorageReference { actual val storage: FirebaseStorage get() = TODO("Not yet implemented") + actual suspend fun getMetadata(): FirebaseStorageMetadata? { + TODO("Not yet implemented") + } + actual fun child(path: String): StorageReference { TODO("Not yet implemented") } @@ -66,13 +70,15 @@ actual class StorageReference { TODO("Not yet implemented") } - actual fun putFileResumable(file: File): ProgressFlow { + actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow { TODO("Not yet implemented") } - actual suspend fun putFile(file: File) { + actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?) { } + actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?) { + } } actual class ListResult { @@ -85,4 +91,5 @@ actual class ListResult { } actual class File -actual class FirebaseStorageException internal constructor(message: String) : FirebaseException(message) \ No newline at end of file +actual class FirebaseStorageException internal constructor(message: String) : FirebaseException(message) +actual class Data \ No newline at end of file diff --git a/firebase-storage/src/jvmTest/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt b/firebase-storage/src/jvmTest/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt new file mode 100644 index 000000000..8a8c266b6 --- /dev/null +++ b/firebase-storage/src/jvmTest/kotlin/dev/gitlive/firebase/storage/storage.jvm.kt @@ -0,0 +1,5 @@ +package dev.gitlive.firebase.storage + +actual fun createTestData(): Data { + TODO("Not yet implemented") +} \ No newline at end of file