Skip to content

Commit

Permalink
Merge pull request #694 from supabase-community/storage-metadata
Browse files Browse the repository at this point in the history
Add support for file metadata, `info` and `exists`
  • Loading branch information
jan-tennert authored Jan 15, 2025
1 parent c84967a commit 34ce185
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,25 @@ sealed interface BucketApi {
filter: BucketListFilter.() -> Unit = {}
): List<FileObject>

/**
* Returns information about the file under [path]
* @param path The path to get information about
* @return The file object
* @throws RestException or one of its subclasses if receiving an error response
* @throws HttpRequestTimeoutException if the request timed out
* @throws HttpRequestException on network related issues
*/
suspend fun info(path: String): FileObjectV2

/**
* Checks if a file exists under [path]
* @return true if the file exists, false otherwise
* @throws RestException or one of its subclasses if receiving an error response
* @throws HttpRequestTimeoutException if the request timed out
* @throws HttpRequestException on network related issues
*/
suspend fun exists(path: String): Boolean

/**
* Changes the bucket's public status to [public]
* @throws RestException or one of its subclasses if receiving an error response
Expand Down Expand Up @@ -304,6 +323,11 @@ sealed interface BucketApi {
*/
const val UPSERT_HEADER = "x-upsert"

/**
* The header to use for the user metadata
*/
const val METADATA_HEADER = "x-metadata"

}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.jan.supabase.storage

import io.github.jan.supabase.exceptions.RestException
import io.github.jan.supabase.putJsonObject
import io.github.jan.supabase.safeBody
import io.github.jan.supabase.storage.BucketApi.Companion.UPSERT_HEADER
Expand All @@ -14,6 +15,7 @@ import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.Url
import io.ktor.http.content.OutgoingContent
import io.ktor.http.defaultForFilePath
Expand All @@ -27,6 +29,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.time.Duration

internal class BucketApiImpl(override val bucketId: String, val storage: StorageImpl, resumableCache: ResumableCache) : BucketApi {
Expand Down Expand Up @@ -222,6 +226,23 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
}).safeBody()
}

override suspend fun info(path: String): FileObjectV2 {
val response = storage.api.get("object/info/$bucketId/$path")
return response.safeBody<FileObjectV2>().copy(serializer = storage.serializer)
}

override suspend fun exists(path: String): Boolean {
try {
storage.api.request("object/$bucketId/$path") {
method = HttpMethod.Head
}
return true
} catch (e: RestException) {
if (e.statusCode in listOf(HttpStatusCode.NotFound.value, HttpStatusCode.BadRequest.value)) return false
throw e
}
}

private fun defaultUploadUrl(path: String) = "object/$bucketId/$path"

private fun uploadToSignedUrlUrl(path: String, token: String) = "object/upload/sign/$bucketId/$path?token=$token"
Expand All @@ -246,6 +267,7 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
return FileUploadResponse(id, path, key)
}

@OptIn(ExperimentalEncodingApi::class)
private fun HttpRequestBuilder.defaultUploadRequest(
path: String,
data: UploadData,
Expand All @@ -258,6 +280,9 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
})
header(HttpHeaders.ContentType, optionBuilder.contentType ?: ContentType.defaultForFilePath(path))
header(UPSERT_HEADER, optionBuilder.upsert.toString())
optionBuilder.userMetadata?.let {
header(BucketApi.METADATA_HEADER, Base64.encode(it.toString().encodeToByteArray()))
}
}

override suspend fun changePublicStatusTo(public: Boolean) = storage.updateBucket(bucketId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package io.github.jan.supabase.storage

import io.github.jan.supabase.SupabaseSerializer
import io.github.jan.supabase.decode
import io.github.jan.supabase.serializer.KotlinXSerializer
import io.ktor.http.ContentType
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.JsonObject

/**
Expand All @@ -25,4 +30,60 @@ data class FileObject(
@SerialName("last_accessed_at")
val lastAccessedAt: Instant?,
val metadata: JsonObject?
)
)

/**
* Represents a file or a folder in a bucket. If the item is a folder, everything except [name] is null.
* @param name The name of the item
* @param id The id of the item
* @param version The version of the item
* @param bucketId The bucket id of the item
* @param updatedAt The last update date of the item
* @param createdAt The creation date of the item
* @param lastAccessedAt The last access date of the item
* @param metadata The metadata of the item
* @param size The size of the item
* @param rawContentType The content type of the item
* @param etag The etag of the item
* @param lastModified The last modified date of the item
* @param cacheControl The cache control of the item
* @param serializer The serializer to use for decoding the metadata
*/
@Serializable
data class FileObjectV2(
val name: String,
val id: String?,
val version: String,
@SerialName("bucket_id")
val bucketId: String? = null,
@SerialName("updated_at")
val updatedAt: Instant? = null,
@SerialName("created_at")
val createdAt: Instant,
@SerialName("last_accessed_at")
val lastAccessedAt: Instant? = null,
val metadata: JsonObject? = null,
val size: Long,
@SerialName("content_type")
val rawContentType: String,
val etag: String? = null,
@SerialName("last_modified")
val lastModified: Instant? = null,
@SerialName("cache_control")
val cacheControl: String? = null,
@Transient @PublishedApi internal val serializer: SupabaseSerializer = KotlinXSerializer()
) {

/**
* The content type of the file
*/
val contentType by lazy {
ContentType.parse(rawContentType)
}

/**
* Decodes the metadata using the [serializer]
*/
inline fun <reified T> decodeMetadata(): T? = metadata?.let { serializer.decode(it.toString()) }

}
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
package io.github.jan.supabase.storage

import io.github.jan.supabase.SupabaseSerializer
import io.github.jan.supabase.encodeToJsonElement
import io.github.jan.supabase.network.HttpRequestOverride
import io.ktor.http.ContentType
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject

/**
* Builder for uploading files with additional options
* @param serializer The serializer to use for encoding the metadata
* @param upsert Whether to update the file if it already exists
* @param userMetadata The user metadata to upload with the file
* @param contentType The content type of the file. If null, the content type will be inferred from the file extension
*/
class UploadOptionBuilder(
@PublishedApi internal val serializer: SupabaseSerializer,
var upsert: Boolean = false,
var userMetadata: JsonObject? = null,
var contentType: ContentType? = null,
internal val httpRequestOverrides: MutableList<HttpRequestOverride> = mutableListOf()
) {

/**
* Overrides the HTTP request
* Adds an [HttpRequestOverride] to the upload request
* @param override The override to add
*/
fun httpOverride(override: HttpRequestOverride) {
httpRequestOverrides.add(override)
}

/**
* Sets the user metadata to upload with the file
* @param data The data to upload. Must be serializable by the [serializer]
*/
inline fun <reified T : Any> userMetadata(data: T) {
userMetadata = serializer.encodeToJsonElement(data).jsonObject
}

/**
* Sets the user metadata to upload with the file
* @param builder The builder for the metadata
*/
inline fun userMetadata(builder: JsonObjectBuilder.() -> Unit) {
userMetadata = buildJsonObject(builder)
}

}
Loading

0 comments on commit 34ce185

Please sign in to comment.