Skip to content

Commit bb61386

Browse files
authored
Merge pull request #514 from deBasMan21/feat/firebaseStorageMetadata
Add metadata to Firebase Storage
2 parents 54740d7 + 3186355 commit bb61386

File tree

12 files changed

+324
-21
lines changed

12 files changed

+324
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.gitlive.firebase.storage
2+
3+
import android.net.Uri
4+
5+
actual fun createTestData(): Data {
6+
return Data("test".toByteArray())
7+
}

firebase-storage/src/androidMain/kotlin/dev/gitlive/firebase/storage/storage.kt

+59-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package dev.gitlive.firebase.storage
88
import android.net.Uri
99
import com.google.android.gms.tasks.OnCanceledListener
1010
import com.google.android.gms.tasks.OnCompleteListener
11+
import com.google.android.gms.tasks.Task
1112
import com.google.firebase.storage.OnPausedListener
1213
import com.google.firebase.storage.OnProgressListener
14+
import com.google.firebase.storage.StorageMetadata
1315
import com.google.firebase.storage.UploadTask
1416
import dev.gitlive.firebase.Firebase
1517
import dev.gitlive.firebase.FirebaseApp
@@ -18,7 +20,10 @@ import kotlinx.coroutines.channels.awaitClose
1820
import kotlinx.coroutines.channels.trySendBlocking
1921
import kotlinx.coroutines.flow.FlowCollector
2022
import kotlinx.coroutines.flow.callbackFlow
23+
import kotlinx.coroutines.flow.channelFlow
2124
import kotlinx.coroutines.flow.emitAll
25+
import kotlinx.coroutines.flow.first
26+
import kotlinx.coroutines.launch
2227
import kotlinx.coroutines.tasks.await
2328

2429
actual val Firebase.storage get() =
@@ -57,6 +62,8 @@ actual class StorageReference(val android: com.google.firebase.storage.StorageRe
5762
actual val root: StorageReference get() = StorageReference(android.root)
5863
actual val storage: FirebaseStorage get() = FirebaseStorage(android.storage)
5964

65+
actual suspend fun getMetadata(): FirebaseStorageMetadata? = android.metadata.await().toFirebaseStorageMetadata()
66+
6067
actual fun child(path: String): StorageReference = StorageReference(android.child(path))
6168

6269
actual suspend fun delete() = android.delete().await().run { Unit }
@@ -65,10 +72,28 @@ actual class StorageReference(val android: com.google.firebase.storage.StorageRe
6572

6673
actual suspend fun listAll(): ListResult = ListResult(android.listAll().await())
6774

68-
actual suspend fun putFile(file: File) = android.putFile(file.uri).await().run {}
75+
actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?) {
76+
if (metadata != null) {
77+
android.putFile(file.uri, metadata.toStorageMetadata()).await().run {}
78+
} else {
79+
android.putFile(file.uri).await().run {}
80+
}
81+
}
6982

70-
actual fun putFileResumable(file: File): ProgressFlow {
71-
val android = android.putFile(file.uri)
83+
actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?) {
84+
if (metadata != null) {
85+
android.putBytes(data.data, metadata.toStorageMetadata()).await().run {}
86+
} else {
87+
android.putBytes(data.data).await().run {}
88+
}
89+
}
90+
91+
actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow {
92+
val android = if (metadata != null) {
93+
android.putFile(file.uri, metadata.toStorageMetadata())
94+
} else {
95+
android.putFile(file.uri)
96+
}
7297

7398
val flow = callbackFlow {
7499
val onCanceledListener = OnCanceledListener { cancel() }
@@ -104,4 +129,35 @@ actual class ListResult(android: com.google.firebase.storage.ListResult) {
104129

105130
actual class File(val uri: Uri)
106131

132+
actual class Data(val data: ByteArray)
133+
107134
actual typealias FirebaseStorageException = com.google.firebase.storage.StorageException
135+
136+
fun FirebaseStorageMetadata.toStorageMetadata(): StorageMetadata {
137+
return StorageMetadata.Builder()
138+
.setCacheControl(this.cacheControl)
139+
.setContentDisposition(this.contentDisposition)
140+
.setContentEncoding(this.contentEncoding)
141+
.setContentLanguage(this.contentLanguage)
142+
.setContentType(this.contentType)
143+
.apply {
144+
customMetadata.entries.forEach {
145+
(key, value) -> setCustomMetadata(key, value)
146+
}
147+
}.build()
148+
}
149+
150+
fun StorageMetadata.toFirebaseStorageMetadata(): FirebaseStorageMetadata {
151+
val sdkMetadata = this
152+
return storageMetadata {
153+
md5Hash = sdkMetadata.md5Hash
154+
cacheControl = sdkMetadata.cacheControl
155+
contentDisposition = sdkMetadata.contentDisposition
156+
contentEncoding = sdkMetadata.contentEncoding
157+
contentLanguage = sdkMetadata.contentLanguage
158+
contentType = sdkMetadata.contentType
159+
sdkMetadata.customMetadataKeys.forEach {
160+
setCustomMetadata(it, sdkMetadata.getCustomMetadata(it))
161+
}
162+
}
163+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.gitlive.firebase.storage
2+
3+
import android.net.Uri
4+
5+
actual fun createTestData(): Data {
6+
return Data("test".toByteArray())
7+
}

firebase-storage/src/commonMain/kotlin/dev/gitlive/firebase/storage/storage.kt

+33-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import dev.gitlive.firebase.Firebase
44
import dev.gitlive.firebase.FirebaseApp
55
import dev.gitlive.firebase.FirebaseException
66
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.flow.channelFlow
8+
import kotlinx.coroutines.flow.first
9+
import kotlinx.coroutines.launch
710

811
/** Returns the [FirebaseStorage] instance of the default [FirebaseApp]. */
912
expect val Firebase.storage: FirebaseStorage
@@ -32,6 +35,8 @@ expect class StorageReference {
3235
val root: StorageReference
3336
val storage: FirebaseStorage
3437

38+
suspend fun getMetadata(): FirebaseStorageMetadata?
39+
3540
fun child(path: String): StorageReference
3641

3742
suspend fun delete()
@@ -40,9 +45,11 @@ expect class StorageReference {
4045

4146
suspend fun listAll(): ListResult
4247

43-
suspend fun putFile(file: File)
48+
suspend fun putFile(file: File, metadata: FirebaseStorageMetadata? = null)
49+
50+
suspend fun putData(data: Data, metadata: FirebaseStorageMetadata? = null)
4451

45-
fun putFileResumable(file: File): ProgressFlow
52+
fun putFileResumable(file: File, metadata: FirebaseStorageMetadata? = null): ProgressFlow
4653
}
4754

4855
expect class ListResult {
@@ -53,6 +60,8 @@ expect class ListResult {
5360

5461
expect class File
5562

63+
expect class Data
64+
5665
sealed class Progress(val bytesTransferred: Number, val totalByteCount: Number) {
5766
class Running internal constructor(bytesTransferred: Number, totalByteCount: Number): Progress(bytesTransferred, totalByteCount)
5867
class Paused internal constructor(bytesTransferred: Number, totalByteCount: Number): Progress(bytesTransferred, totalByteCount)
@@ -64,5 +73,26 @@ interface ProgressFlow : Flow<Progress> {
6473
fun cancel()
6574
}
6675

67-
6876
expect class FirebaseStorageException : FirebaseException
77+
78+
data class FirebaseStorageMetadata(
79+
var md5Hash: String? = null,
80+
var cacheControl: String? = null,
81+
var contentDisposition: String? = null,
82+
var contentEncoding: String? = null,
83+
var contentLanguage: String? = null,
84+
var contentType: String? = null,
85+
var customMetadata: MutableMap<String, String> = mutableMapOf()
86+
) {
87+
fun setCustomMetadata(key: String, value: String?) {
88+
value?.let {
89+
customMetadata[key] = it
90+
}
91+
}
92+
}
93+
94+
fun storageMetadata(init: FirebaseStorageMetadata.() -> Unit): FirebaseStorageMetadata {
95+
val metadata = FirebaseStorageMetadata()
96+
metadata.init()
97+
return metadata
98+
}

firebase-storage/src/commonTest/kotlin/dev/gitlive/firebase/storage/storage.kt

+65-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ import dev.gitlive.firebase.Firebase
88
import dev.gitlive.firebase.FirebaseOptions
99
import dev.gitlive.firebase.apps
1010
import dev.gitlive.firebase.initialize
11+
import dev.gitlive.firebase.runBlockingTest
12+
import kotlinx.coroutines.flow.channelFlow
13+
import kotlinx.coroutines.flow.first
14+
import kotlinx.coroutines.launch
15+
import kotlin.test.AfterTest
1116
import kotlin.test.BeforeTest
17+
import kotlin.test.Test
18+
import kotlin.test.assertContentEquals
19+
import kotlin.test.assertEquals
20+
import kotlin.test.assertNotNull
1221

1322
expect val emulatorHost: String
1423
expect val context: Any
@@ -35,6 +44,61 @@ class FirebaseStorageTest {
3544

3645
storage = Firebase.storage(app).apply {
3746
useEmulator(emulatorHost, 9199)
47+
setMaxOperationRetryTimeMillis(10000)
48+
setMaxUploadRetryTimeMillis(10000)
3849
}
3950
}
40-
}
51+
52+
@AfterTest
53+
fun deinitializeFirebase() = runBlockingTest {
54+
Firebase.apps(context).forEach {
55+
it.delete()
56+
}
57+
}
58+
59+
@Test
60+
fun testStorageNotNull() {
61+
assertNotNull(storage)
62+
}
63+
64+
@Test
65+
fun testUploadShouldNotCrash() = runBlockingTest {
66+
val data = createTestData()
67+
val ref = storage.reference("test").child("testFile.txt")
68+
ref.putData(data)
69+
}
70+
71+
@Test
72+
fun testUploadMetadata() = runBlockingTest {
73+
val data = createTestData()
74+
val ref = storage.reference("test").child("testFile.txt")
75+
val metadata = storageMetadata {
76+
contentType = "text/plain"
77+
}
78+
ref.putData(data, metadata)
79+
80+
val metadataResult = ref.getMetadata()
81+
82+
assertNotNull(metadataResult)
83+
assertNotNull(metadataResult.contentType)
84+
assertEquals(metadataResult.contentType, metadata.contentType)
85+
}
86+
87+
@Test
88+
fun testUploadCustomMetadata() = runBlockingTest {
89+
val data = createTestData()
90+
val ref = storage.reference("test").child("testFile.txt")
91+
val metadata = storageMetadata {
92+
contentType = "text/plain"
93+
setCustomMetadata("key", "value")
94+
}
95+
ref.putData(data, metadata)
96+
97+
val metadataResult = ref.getMetadata()
98+
99+
assertNotNull(metadataResult)
100+
assertEquals(metadataResult.customMetadata["key"], metadata.customMetadata["key"])
101+
}
102+
}
103+
104+
expect fun createTestData(): Data

firebase-storage/src/iosMain/kotlin/dev/gitlive/firebase/storage/storage.kt

+52-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package dev.gitlive.firebase.storage
66

77
import cocoapods.FirebaseStorage.FIRStorage
88
import cocoapods.FirebaseStorage.FIRStorageListResult
9+
import cocoapods.FirebaseStorage.FIRStorageMetadata
910
import cocoapods.FirebaseStorage.FIRStorageReference
1011
import cocoapods.FirebaseStorage.FIRStorageTaskStatusFailure
1112
import cocoapods.FirebaseStorage.FIRStorageTaskStatusPause
@@ -22,6 +23,7 @@ import kotlinx.coroutines.channels.trySendBlocking
2223
import kotlinx.coroutines.flow.FlowCollector
2324
import kotlinx.coroutines.flow.callbackFlow
2425
import kotlinx.coroutines.flow.emitAll
26+
import platform.Foundation.NSData
2527
import platform.Foundation.NSError
2628
import platform.Foundation.NSURL
2729

@@ -64,6 +66,16 @@ actual class StorageReference(val ios: FIRStorageReference) {
6466

6567
actual fun child(path: String): StorageReference = StorageReference(ios.child(path))
6668

69+
actual suspend fun getMetadata(): FirebaseStorageMetadata? = ios.awaitResult {
70+
metadataWithCompletion { metadata, error ->
71+
if (error == null) {
72+
it.invoke(metadata?.toFirebaseStorageMetadata(), null)
73+
} else {
74+
it.invoke(null, error)
75+
}
76+
}
77+
}
78+
6779
actual suspend fun delete() = await { ios.deleteWithCompletion(it) }
6880

6981
actual suspend fun getDownloadUrl(): String = ios.awaitResult {
@@ -76,10 +88,16 @@ actual class StorageReference(val ios: FIRStorageReference) {
7688
}
7789
}
7890

79-
actual suspend fun putFile(file: File) = ios.awaitResult { putFile(file.url, null, completion = it) }.run {}
91+
actual suspend fun putFile(file: File, metadata: FirebaseStorageMetadata?) = ios.awaitResult { callback ->
92+
putFile(file.url, metadata?.toFIRMetadata(), callback)
93+
}.run {}
94+
95+
actual suspend fun putData(data: Data, metadata: FirebaseStorageMetadata?) = ios.awaitResult { callback ->
96+
putData(data.data, metadata?.toFIRMetadata(), callback)
97+
}.run {}
8098

81-
actual fun putFileResumable(file: File): ProgressFlow {
82-
val ios = ios.putFile(file.url)
99+
actual fun putFileResumable(file: File, metadata: FirebaseStorageMetadata?): ProgressFlow {
100+
val ios = ios.putFile(file.url, metadata?.toFIRMetadata())
83101

84102
val flow = callbackFlow {
85103
ios.observeStatus(FIRStorageTaskStatusProgress) {
@@ -122,6 +140,8 @@ actual class ListResult(ios: FIRStorageListResult) {
122140

123141
actual class File(val url: NSURL)
124142

143+
actual class Data(val data: NSData)
144+
125145
actual class FirebaseStorageException(message: String): FirebaseException(message)
126146

127147
suspend inline fun <T> T.await(function: T.(callback: (NSError?) -> Unit) -> Unit) {
@@ -147,3 +167,32 @@ suspend inline fun <T, reified R> T.awaitResult(function: T.(callback: (R?, NSEr
147167
}
148168
return job.await() as R
149169
}
170+
171+
fun FirebaseStorageMetadata.toFIRMetadata(): FIRStorageMetadata {
172+
val metadata = FIRStorageMetadata()
173+
val mappedMetadata: Map<Any?, String> = this.customMetadata.map {
174+
it.key to it.value
175+
}.toMap()
176+
metadata.setCustomMetadata(mappedMetadata)
177+
metadata.setCacheControl(this.cacheControl)
178+
metadata.setContentDisposition(this.contentDisposition)
179+
metadata.setContentEncoding(this.contentEncoding)
180+
metadata.setContentLanguage(this.contentLanguage)
181+
metadata.setContentType(this.contentType)
182+
return metadata
183+
}
184+
185+
fun FIRStorageMetadata.toFirebaseStorageMetadata(): FirebaseStorageMetadata {
186+
val sdkMetadata = this
187+
return storageMetadata {
188+
md5Hash = sdkMetadata.md5Hash()
189+
cacheControl = sdkMetadata.cacheControl()
190+
contentDisposition = sdkMetadata.contentDisposition()
191+
contentEncoding = sdkMetadata.contentEncoding()
192+
contentLanguage = sdkMetadata.contentLanguage()
193+
contentType = sdkMetadata.contentType()
194+
sdkMetadata.customMetadata()?.forEach {
195+
setCustomMetadata(it.key.toString(), it.value.toString())
196+
}
197+
}
198+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package dev.gitlive.firebase.storage
2+
3+
import kotlinx.cinterop.BetaInteropApi
4+
import kotlinx.cinterop.utf8
5+
import platform.Foundation.NSCoder
6+
import platform.Foundation.NSData
7+
import platform.Foundation.NSSearchPathDirectory
8+
import platform.Foundation.NSSearchPathDomainMask
9+
import platform.Foundation.NSSearchPathForDirectoriesInDomains
10+
import platform.Foundation.NSString
11+
import platform.Foundation.NSURL
12+
import platform.Foundation.NSUTF8StringEncoding
13+
import platform.Foundation.create
14+
import platform.Foundation.dataUsingEncoding
15+
16+
@OptIn(BetaInteropApi::class)
17+
actual fun createTestData(): Data {
18+
val value = NSString.create(string = "test")
19+
return Data(value.dataUsingEncoding(NSUTF8StringEncoding, false)!!)
20+
}

0 commit comments

Comments
 (0)