minIO是什么
一个开源的OSS存储方案....就这样。
Amazon S3是什么
Amazon S3(Simple Storage Service)是亚马逊云服务(AWS, Amazon Web Services)提供的一个对象存储服务,提供高可用性、可扩展性和安全的数据存储基础设施。minIO并且兼容 Amazon S3的API。
前景:
公司需要将阿里云存储缓存minIO存储,minIO有java的SDK,但是没有提供安卓的SDK。由于minIO兼容Amazon S3的API,所以我们客户端用S3的API 来上传文件。
依赖集成
下载Android S3的 SDK 地址,github.com/aws-amplify… 这里面有很多库,只copy我们需要的,
在S3 modules中的gradle 要添加上面的core 和kms依赖
api project(':libs:amazon-s3:aws-android-sdk-core')
api project(':libs:amazon-s3:aws-android-sdk-kms')
然后在项目合适的地方添加S3的依赖就可以了
implementation(project(":libs:amazon-s3:aws-android-sdk-s3"))
基本实现
在实现之前,我们一般需要确定几个东西:
- endPoint:服务器地址,后台开发者返回给你。
- 凭证数据:
token
,SecretKey
,AccessKey
,用于服务器的鉴权,后台开发者返回给你。 - 签名方式:
QueryStringSignerType
,AWS4SignerType
,NoOpSignerType
,AWSS3V4SignerType
,跟服务器保持一致, - bucketName:桶的位置,也就是服务器地址某个文件夹的路径。后台开发者返回给你。
确定了以上的东西,我们就可以初始化client了。
OSSUpDownloader 类
import android.util.Log
import com.amazonaws.AmazonClientException
import com.amazonaws.AmazonServiceException
import com.amazonaws.ClientConfiguration
import com.amazonaws.SDKGlobalConfiguration
import com.amazonaws.services.s3.AmazonS3
import com.amazonaws.services.s3.AmazonS3Client
import com.amazonaws.services.s3.S3ClientOptions
import com.amazonaws.services.s3.model.GetObjectRequest
import com.amazonaws.services.s3.model.ObjectMetadata
import com.amazonaws.services.s3.model.PutObjectRequest
import com.boyasec.ime.data.cross.file.StoreType
import com.boyasec.ime.data.oss.remote.OSSRemoteDataSource
import com.boyasec.ime.persistent.map.OssConfigPref
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.InputStream
private const val TAG = "ctyun_OSSUpDownloader"
interface OSSUpDownloader {
suspend fun preload()
/**
* 简单同步上传文件
*
* @param objectName 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
* @param uploadData 二进制byte[]数组
*/
suspend fun uploadBytes(
objectName: String,
uploadData: ByteArray,
teamId: Long,
@StoreType storeType: Int
)
/**
* 简单同步上传文件
*
* @param objectName 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
* @param uploadFilePath 本地文件地址
*/
suspend fun uploadFile(
objectName: String,
uploadFilePath: String,
teamId: Long,
@StoreType storeType: Int
)
suspend fun uploadFileAsync(
objectName: String,
uploadFilePath: String,
isPrivateBucket: Boolean =true,
progressCallback: (currentSize: Long, totalSize: Long, result: Boolean?) -> Unit,
)
/**
* 同步下载到本地文件
*
* @param objectKey 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
*/
suspend fun downloadFile(
objectKey: String,
teamId: Long,
@StoreType storeType: Int
): InputStream
}
internal class OSSUpDownloaderImpl(
private val ossRemoteDataSource: OSSRemoteDataSource,
private val ossConfigPref: OssConfigPref,
private val defaultDispatcher: CoroutineDispatcher,
) : OSSUpDownloader {
//鉴权相关
private val refreshCredential = RefreshCredential(ossRemoteDataSource)
private val ossClient: AmazonS3 by lazy {
Log.i(TAG, "ossClient初始化: ")
Log.i(TAG, "ossClient初始化: endpoint = " +ossConfigPref.ossEndPoint)
// 创建client 配置对象
val clientConfig = ClientConfiguration()
clientConfig.connectionTimeout = 120 * 1000 // 设置连接的超时时间,单位毫秒
clientConfig.socketTimeout = 120 * 1000 // 设置socket 超时时间,单位毫秒
clientConfig.signerOverride="AWSS3V4SignerType"
// clientConfig.signerOverride="NoOpSignerType"
System.setProperty(SDKGlobalConfiguration.DISABLE_CERT_CHECKING_SYSTEM_PROPERTY,"true")
// // 可以使用V4 签名,也可以使用V2 签名,false 为V2 签名,使用系统属性来设置
// System.setProperty(
// SDKGlobalConfiguration.ENABLE_S3_SIGV4_SYSTEM_PROPERTY,
// "false"
// )
// V4 签名可以用负载参与签名、本例设置负载不参与签名
val options = S3ClientOptions.builder().setPayloadSigningEnabled(false)
.build()
//创建client
AmazonS3Client(
refreshCredential,
clientConfig
).apply {
// 设置endpoint 这个地址是跟服务端协商的.
endpoint = ossConfigPref.ossEndPoint
// 设置选项
setS3ClientOptions(options)
}
}
//获取凭证
override suspend fun preload() {
refreshCredential.refreshCredentials()
}
/**
* 简单同步上传文件
*
* @param objectName 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
* @param uploadData 二进制byte[]数组
*/
override suspend fun uploadBytes(
objectName: String,
uploadData: ByteArray,
teamId: Long,
storeType: Int,
) {
when (storeType) {
StoreType.PUBLIC -> {
//bucketName 也是服务器告知的
val bucketName = ossConfigPref.privateOssBucket
val metadata = ObjectMetadata()
// val dataLocation = CtyunBucketDataLocation()
// metadata.dataLocation = dataLocation
val request = PutObjectRequest(bucketName, objectName, uploadData.inputStream(), metadata)
try {
ossClient.putObject(request)
} catch (e: Exception) {
Timber.e(e)
when (e) {
is AmazonServiceException -> throw ServiceException(null)
is AmazonClientException -> throw ClientException(null)
else -> throw e
}
}
}
StoreType.SELF_HOSTED -> ossRemoteDataSource.uploadBytes(teamId, objectName, uploadData)
}
}
/**
* 简单同步上传文件
*
* @param objectName 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
* @param uploadFilePath 本地文件地址
*/
override suspend fun uploadFile(
objectName: String,
uploadFilePath: String,
teamId: Long,
storeType: Int,
) {
when (storeType) {
StoreType.PUBLIC -> {
val bucketName = ossConfigPref.privateOssBucket
val metadata = ObjectMetadata()
// val dataLocation = CtyunBucketDataLocation()
// metadata.dataLocation = dataLocation
val request = PutObjectRequest(bucketName, objectName, File(uploadFilePath))
request.metadata = metadata
try {
ossClient.putObject(request)
} catch (e: Exception) {
Timber.e(e)
when (e) {
is AmazonServiceException -> throw ServiceException(null)
is AmazonClientException -> throw ClientException(null)
else -> throw e
}
}
}
StoreType.SELF_HOSTED -> ossRemoteDataSource.uploadFile(
teamId,
objectName,
uploadFilePath
)
}
}
override suspend fun uploadFileAsync(
objectName: String,
uploadFilePath: String,
isPrivateBucket: Boolean ,
progressCallback: (currentSize: Long, totalSize: Long, result: Boolean?) -> Unit,
) {
val bucketName =
if (isPrivateBucket) ossConfigPref.privateOssBucket else ossConfigPref.ossBucket
val metadata = ObjectMetadata()
// val dataLocation = CtyunBucketDataLocation()
// metadata.dataLocation = dataLocation
val file = File(uploadFilePath)
val totalSize = file.length()
val request = PutObjectRequest(bucketName, objectName, file)
request.metadata = metadata
request.withGeneralProgressListener {
progressCallback(it.bytesTransferred, totalSize, null)
}
withContext(defaultDispatcher) {
try {
ossClient.putObject(request)
progressCallback(totalSize, totalSize, true)
} catch (_: Exception) {
progressCallback(0, totalSize, false)
}
}
}
/**
* 同步下载到本地文件
*
* @param objectKey 包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
*/
override suspend fun downloadFile(
objectKey: String,
teamId: Long,
storeType: Int,
): InputStream {
return when (storeType) {
StoreType.PUBLIC -> {
val bucketName = ossConfigPref.privateOssBucket
val request = GetObjectRequest(bucketName, objectKey)
try {
ossClient.getObject(request).objectContent
} catch (e: Exception) {
Timber.e(e)
when (e) {
is AmazonServiceException -> throw ServiceException(e.message)
is AmazonClientException -> throw ClientException(e.message)
else -> throw e
}
}
}
StoreType.SELF_HOSTED -> ossRemoteDataSource.downloadFile(teamId, objectKey)
else -> error("Illegal store type = $storeType")
}
}
}
RefreshCredential 类
主要用于获取凭证数据:token
,AccessKey
,SecretKey
,用来表明客户端身份的。
import com.amazonaws.auth.AWSRefreshableSessionCredentials
import com.amazonaws.services.securitytoken.model.Credentials
import com.boyasec.ime.data.oss.remote.OSSRemoteDataSource
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class RefreshCredential(
private val ossRemoteDataSource: OSSRemoteDataSource,
) : AWSRefreshableSessionCredentials {
private val mutex = Mutex()
private var sessionCredentials: Credentials? = null
override fun getAWSAccessKeyId(): String {
return getSessionCredentials().accessKeyId
}
override fun getAWSSecretKey(): String {
return getSessionCredentials().secretAccessKey
}
override fun getSessionToken(): String {
return getSessionCredentials().sessionToken
}
/**
* 从后台服务器获取 需要的key 以及token
*/
override fun refreshCredentials() {
runBlocking {
mutex.withLock {
if (needsNewSession()) {
sessionCredentials = ossRemoteDataSource.getOssToken()
}
}
}
}
private fun getSessionCredentials(): Credentials {
if (needsNewSession()) {
refreshCredentials()
}
return sessionCredentials!!
}
private fun needsNewSession(): Boolean {
return if (sessionCredentials == null) {
true
} else {
val expiration = sessionCredentials!!.expiration
val timeRemaining = expiration.time - System.currentTimeMillis()
timeRemaining < 60000L
}
}
}
到这基本就可以实现了基本的上传下载功能了,代码中的ossConfigPref
这个只是我项目中用来全局保存一些值的工具类,你们可以用自己的实现。
忽略HTTPS校验
如果上面的实现在验证的时候提示了Turst相关的错误,那么可能是服务器使用的http,我们需要忽略到HTTPS的校验,
查看源码得知 最终上传执行是在 UrlHttpClient 这个类,该类有个配置connection 的函数。里面有一段被注释的代码
// // disable cert check
// /*
// * Commented as per https://support.google.com/faqs/answer/6346016. Uncomment for testing.
//
// */
if (System.getProperty(DISABLE_CERT_CHECKING_SYSTEM_PROPERTY) != null) {
disableCertificateValidation(https);
}
把其中的if判断直接去掉就可以了。 当然,也可以尝试设置 DISABLE_CERT_CHECKING_SYSTEM_PROPERTY
这个系统属性的值,我当时好像设置了没效果。