Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for file metadata, info and exists #694

Merged
merged 16 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,86 +34,84 @@ sealed interface BucketApi {
* Uploads a file in [bucketId] under [path]
* @param path The path to upload the file to
* @param data The data to upload
* @param upsert Whether to overwrite an existing file
* @return the key to the uploaded file
* @throws IllegalArgumentException if data to upload is empty
* @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 upload(path: String, data: ByteArray, upsert: Boolean = false): FileUploadResponse {
suspend fun upload(path: String, data: ByteArray, options: FileOptionBuilder.() -> Unit = {}): FileUploadResponse {
require(data.isNotEmpty()) { "The data to upload should not be empty" }
return upload(path, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
return upload(path, UploadData(ByteReadChannel(data), data.size.toLong()), options)
}

/**
* Uploads a file in [bucketId] under [path]
* @param path The path to upload the file to
* @param data The data to upload
* @param upsert Whether to overwrite an existing file
* @return the key to the uploaded file
* @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 upload(path: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
suspend fun upload(path: String, data: UploadData, options: FileOptionBuilder.() -> Unit = {}): FileUploadResponse

/**
* Uploads a file in [bucketId] under [path] using a presigned url
* @param path The path to upload the file to
* @param token The presigned url token
* @param token The pre-signed url token
* @param data The data to upload
* @param upsert Whether to overwrite an existing file
* @return the key of the uploaded file
* @throws IllegalArgumentException if data to upload is empty
*/
suspend fun uploadToSignedUrl(path: String, token: String, data: ByteArray, upsert: Boolean = false
suspend fun uploadToSignedUrl(
path: String,
token: String,
data: ByteArray,
options: FileOptionBuilder.() -> Unit = {}
): FileUploadResponse {
require(data.isNotEmpty()) { "The data to upload should not be empty" }
return uploadToSignedUrl(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
return uploadToSignedUrl(path, token, UploadData(ByteReadChannel(data), data.size.toLong()), options)
}

/**
* Uploads a file in [bucketId] under [path] using a presigned url
* @param path The path to upload the file to
* @param token The presigned url token
* @param data The data to upload
* @param upsert Whether to overwrite an existing file
* @return the key of the uploaded file
* @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
* @throws HttpRequestException on network related issues
*/
suspend fun uploadToSignedUrl(path: String, token: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
suspend fun uploadToSignedUrl(path: String, token: String, data: UploadData, options: FileOptionBuilder.() -> Unit = {}): FileUploadResponse

/**
* Updates a file in [bucketId] under [path]
* @param path The path to update the file to
* @param data The new data
* @param upsert Whether to overwrite an existing file
* @return the key to the updated file
* @throws IllegalArgumentException if data to upload is empty
* @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 update(path: String, data: ByteArray, upsert: Boolean = false): FileUploadResponse {
suspend fun update(path: String, data: ByteArray, options: FileOptionBuilder.() -> Unit = {}): FileUploadResponse {
require(data.isNotEmpty()) { "The data to upload should not be empty" }
return update(path, UploadData(ByteReadChannel(data), data.size.toLong()), upsert)
return update(path, UploadData(ByteReadChannel(data), data.size.toLong()), options)
}

/**
* Updates a file in [bucketId] under [path]
* @param path The path to update the file to
* @param data The new data
* @param upsert Whether to overwrite an existing file
* @return the key to the updated file
* @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 update(path: String, data: UploadData, upsert: Boolean = false): FileUploadResponse
suspend fun update(path: String, data: UploadData, options: FileOptionBuilder.() -> Unit = {}): FileUploadResponse

/**
* Deletes all files in [bucketId] with in [paths]
Expand Down Expand Up @@ -242,16 +240,34 @@ sealed interface BucketApi {


/**
* Searches for buckets with the given [prefix] and [filter]
* @return The filtered buckets
* Searches for files with the given [prefix] and [filter]
* @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 list(
prefix: String = "",
filter: BucketListFilter.() -> Unit = {}
): List<BucketItem>
): 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]
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 @@ -15,6 +16,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 @@ -29,6 +31,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 All @@ -37,18 +41,22 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage

override val resumable = ResumableClientImpl(this, resumableCache)

override suspend fun update(path: String, data: UploadData, upsert: Boolean): FileUploadResponse =
override suspend fun update(
path: String,
data: UploadData,
options: FileOptionBuilder.() -> Unit
): FileUploadResponse =
uploadOrUpdate(
HttpMethod.Put, bucketId, path, data, upsert
HttpMethod.Put, bucketId, path, data, options
)

override suspend fun uploadToSignedUrl(
path: String,
token: String,
data: UploadData,
upsert: Boolean
options: FileOptionBuilder.() -> Unit
): FileUploadResponse {
return uploadToSignedUrl(path, token, data, upsert) {}
return uploadToSignedUrl(path, token, data, options) {}
}

override suspend fun createSignedUploadUrl(path: String): UploadSignedUrl {
Expand All @@ -64,9 +72,13 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
)
}

override suspend fun upload(path: String, data: UploadData, upsert: Boolean): FileUploadResponse =
override suspend fun upload(
path: String,
data: UploadData,
options: FileOptionBuilder.() -> Unit
): FileUploadResponse =
uploadOrUpdate(
HttpMethod.Post, bucketId, path, data, upsert
HttpMethod.Post, bucketId, path, data, options
)

override suspend fun delete(paths: Collection<String>) {
Expand Down Expand Up @@ -203,32 +215,44 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
override suspend fun list(
prefix: String,
filter: BucketListFilter.() -> Unit
): List<BucketItem> {
): List<FileObject> {
return storage.api.postJson("object/list/$bucketId", buildJsonObject {
put("prefix", prefix)
putJsonObject(BucketListFilter().apply(filter).build())
}).safeBody()
}

override suspend fun info(path: String): FileObjectV2 {
val response = storage.api.get("object/info/$bucketId/$path")
jan-tennert marked this conversation as resolved.
Show resolved Hide resolved
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
}
}

@OptIn(ExperimentalEncodingApi::class)
@Suppress("LongParameterList") //TODO: maybe refactor
internal suspend fun uploadOrUpdate(
method: HttpMethod,
bucket: String,
path: String,
data: UploadData,
upsert: Boolean,
options: FileOptionBuilder.() -> Unit,
extra: HttpRequestBuilder.() -> Unit = {}
): FileUploadResponse {
val optionBuilder = FileOptionBuilder(storage.serializer).apply(options)
val response = storage.api.request("object/$bucket/$path") {
this.method = method
setBody(object : OutgoingContent.ReadChannelContent() {
override val contentType: ContentType = ContentType.defaultForFilePath(path)
override val contentLength: Long = data.size
override fun readFrom(): ByteReadChannel = data.stream
})
header(HttpHeaders.ContentType, ContentType.defaultForFilePath(path))
header(UPSERT_HEADER, upsert.toString())
extra()
defaultUploadRequest(path, data, optionBuilder, extra)
}.body<JsonObject>()
val key = response["Key"]?.jsonPrimitive?.content
?: error("Expected a key in a upload response")
Expand All @@ -237,30 +261,47 @@ internal class BucketApiImpl(override val bucketId: String, val storage: Storage
return FileUploadResponse(id, path, key)
}

@OptIn(ExperimentalEncodingApi::class)
@Suppress("LongParameterList") //TODO: maybe refactor
internal suspend fun uploadToSignedUrl(
path: String,
token: String,
data: UploadData,
upsert: Boolean,
options: FileOptionBuilder.() -> Unit,
extra: HttpRequestBuilder.() -> Unit = {}
): FileUploadResponse {
val optionBuilder = FileOptionBuilder(storage.serializer).apply(options)
val response = storage.api.put("object/upload/sign/$bucketId/$path") {
parameter("token", token)
setBody(object : OutgoingContent.ReadChannelContent() {
override val contentType: ContentType = ContentType.defaultForFilePath(path)
override val contentLength: Long = data.size
override fun readFrom(): ByteReadChannel = data.stream
})
header(HttpHeaders.ContentType, ContentType.defaultForFilePath(path))
header("x-upsert", upsert.toString())
extra()
defaultUploadRequest(path, data, optionBuilder, extra)
}.body<JsonObject>()
val key = response["Key"]?.jsonPrimitive?.content
?: error("Expected a key in a upload response")
val id = response["Id"]?.jsonPrimitive?.content ?: error("Expected an id in a upload response")
return FileUploadResponse(id, path, key)
}

@Suppress("LongParameterList") //TODO: maybe refactor
@OptIn(ExperimentalEncodingApi::class)
private fun HttpRequestBuilder.defaultUploadRequest(
path: String,
data: UploadData,
optionBuilder: FileOptionBuilder,
extra: HttpRequestBuilder.() -> Unit
) {
setBody(object : OutgoingContent.ReadChannelContent() {
override val contentType: ContentType = optionBuilder.contentType ?: ContentType.defaultForFilePath(path)
override val contentLength: Long = data.size
override fun readFrom(): ByteReadChannel = data.stream
})
header(HttpHeaders.ContentType, optionBuilder.contentType ?: ContentType.defaultForFilePath(path))
header(UPSERT_HEADER, optionBuilder.upsert.toString())
optionBuilder.userMetadata?.let {
header("x-metadata", Base64.Default.encode(it.toString().encodeToByteArray()))
}
extra()
}

override suspend fun changePublicStatusTo(public: Boolean) = storage.updateBucket(bucketId) {
this@updateBucket.public = public
}
Expand Down

This file was deleted.

Loading
Loading