Skip to content

Commit

Permalink
Merge pull request #514 from deBasMan21/feat/firebaseStorageMetadata
Browse files Browse the repository at this point in the history
Add metadata to Firebase Storage
  • Loading branch information
nbransby authored Jun 14, 2024
2 parents 54740d7 + 3186355 commit bb61386
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.gitlive.firebase.storage

import android.net.Uri

actual fun createTestData(): Data {
return Data("test".toByteArray())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() =
Expand Down Expand Up @@ -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 }
Expand All @@ -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() }
Expand Down Expand Up @@ -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))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.gitlive.firebase.storage

import android.net.Uri

actual fun createTestData(): Data {
return Data("test".toByteArray())
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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 {
Expand All @@ -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)
Expand All @@ -64,5 +73,26 @@ interface ProgressFlow : Flow<Progress> {
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<String, String> = 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -35,6 +44,61 @@ class FirebaseStorageTest {

storage = Firebase.storage(app).apply {
useEmulator(emulatorHost, 9199)
setMaxOperationRetryTimeMillis(10000)
setMaxUploadRetryTimeMillis(10000)
}
}
}

@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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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> T.await(function: T.(callback: (NSError?) -> Unit) -> Unit) {
Expand All @@ -147,3 +167,32 @@ suspend inline fun <T, reified R> T.awaitResult(function: T.(callback: (R?, NSEr
}
return job.await() as R
}

fun FirebaseStorageMetadata.toFIRMetadata(): FIRStorageMetadata {
val metadata = FIRStorageMetadata()
val mappedMetadata: Map<Any?, String> = 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())
}
}
}
Original file line number Diff line number Diff line change
@@ -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)!!)
}
Loading

0 comments on commit bb61386

Please sign in to comment.