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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, lets wait a bit for adding the info method as there is a bug in the JS lib, this should be object/info/public, but there is also a object/info/authenticated.

We're figuring out internally on how we're naming methods.

So lets just wait a bit before merging this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I commented on the JS PR regarding this. I couldn't make this work on the hosted Supabase instance, but the self-hosted Docker one works.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grdsdev Any news regarding this?

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) {
[email protected] = public
}
Expand Down

This file was deleted.

Loading
Loading