Skip to content

Commit

Permalink
Simplified implementation of "requireBucketExists"
Browse files Browse the repository at this point in the history
This implementation depends on multiple (reasonable) assumptions and insights gained from the source code of MinIO:
- "maxKeys" is implemented as page size: The iterator returned by "MinioClient.listObjects" automatically loads another batch of 0 <= count <= maxKeys results if the number of calls to the iterator exceeds the number of previously loaded results. The extension function "MinioClient.listObjectsLimit" limits the number of objects returned in total.
- The iterator returned by "MinioClient.listObjects" will only return up to one error result. If there is an error result, it will always be returned as the next and only remaining result.
- AWS as well as other S3 providers return the results of "listObjects" in a sorted fashion. This implementation breaks if a provider doesn't abide to this contract.
  • Loading branch information
JaniruTEC committed Nov 20, 2023
1 parent 379843d commit 1baf1a7
Showing 1 changed file with 16 additions and 26 deletions.
42 changes: 16 additions & 26 deletions data/src/main/java/org/cryptomator/data/cloud/s3/S3Impl.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.cryptomator.data.cloud.s3

import io.minio.Result as ListObjectsResult
import android.content.Context
import org.cryptomator.data.cloud.s3.S3CloudApiExceptions.isAccessProblem
import org.cryptomator.data.util.CopyStream
Expand Down Expand Up @@ -33,6 +32,7 @@ import io.minio.MinioClient
import io.minio.PutObjectArgs
import io.minio.RemoveObjectArgs
import io.minio.RemoveObjectsArgs
import io.minio.Result
import io.minio.StatObjectArgs
import io.minio.errors.ErrorResponseException
import io.minio.messages.DeleteObject
Expand Down Expand Up @@ -346,33 +346,23 @@ internal class S3Impl(private val cloud: S3Cloud, private val client: MinioClien
@Throws(NoSuchBucketException::class, BackendException::class)
private fun requireBucketExists() {
try {
//Throw appropriate exception implicitly through "handleApiError"
val results: List<Result<Item?>> = ListObjectsArgs.builder() //
val returned: Sequence<Result<Item?>?> = ListObjectsArgs.builder() //
.bucket(cloud.s3Bucket()) //
.recursive(true) //
.maxKeys(1) //
.recursive(true) // //TODO
.maxKeys(1) // Batch size
.build() //
.let { client.listObjects(it) } //
.map { result -> result.runCatching(ListObjectsResult<Item?>::get) } //

if(results.isEmpty()) {
return
}

if (results.any { it.isSuccess }) {
results.asSequence().filter { it.isFailure }.forEach { Timber.d(it.exceptionOrNull(), "Non-critical exception while checking for bucket %s", cloud.s3Bucket()) }
return
}

val exceptions = results.map { it.exceptionOrNull()!! }
if(exceptions.any { it is ErrorResponseException && it.errorResponse().code() == S3CloudApiErrorCodes.NO_SUCH_BUCKET.value }) {
results.asSequence().filter { it.isFailure }.forEach { Timber.d(it.exceptionOrNull(), "Non-critical exception while checking for bucket %s", cloud.s3Bucket()) }
throw NoSuchBucketException(cloud.s3Bucket())
}
//No success and no NoSuchBucketException
val toThrow = exceptions.first()
results.asSequence().drop(1).filter { it.isFailure }.forEach { Timber.d(it.exceptionOrNull(), "Exception while checking for bucket %s", cloud.s3Bucket()) }
throw toThrow
.let { client.listObjectsLimit(it, 1) }
//returned
// |-- <Empty> No elements in bucket
// |-- Result<Err> Any error
// |-- Result<Item>
// |-- "name/.bzEmpty" Web interface folder
// |-- "name/" 0 Byte folder
// |-- "name" 0 or X Byte file
//Note: This implementation depends on listObjects/listObjectsLimit returning elements in a sensible order

val result: Result<Item?> = returned.firstOrNull() ?: return //No item, but above all no exception, ergo: Bucket exists
result.get() //Throw appropriate exception (if any) implicitly through "handleApiError"
} catch (e: ErrorResponseException) {
throw handleApiError(e, cloud.s3Bucket())
}
Expand Down

0 comments on commit 1baf1a7

Please sign in to comment.