@@ -4,29 +4,31 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil ;
import cn.hutool.http.HttpUtil ;
import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient ;
import com .amazonaws.HttpMethod ;
import com .amazonaws.auth.AWSStatic CredentialsProvider;
import com .amazonaws.auth.BasicAWS Credentials;
import com .amazonaws.client.builder.AwsClientBuilder ;
import com .amazonaws.services.s3.AmazonS3Client ;
import com .amazonaws.services.s3.Amazon S3ClientBuilder ;
import com .amazonaws.services.s3.model.ObjectMetadata ;
import com .amazonaws.services.s3.model.S3Objec t;
import software .amazon. awssdk.auth.credentials.AwsBasicCredentials ;
import software .amazon. awssdk .auth.credentials.Aws CredentialsProvider;
import software .amazon. awssdk .auth.credentials.Static CredentialsProvider ;
import software .amazon. awssdk.core.sync.RequestBody ;
import software .amazon. awssdk.regions.Region ;
import software .amazon. awssdk .services.s3.S3Client;
import software .amazon. awssdk .services.s3.S3Configuration ;
import software .amazon. awssdk .services.s3.model.DeleteObjectReques t;
import software.amazon.awssdk.services.s3.model.GetObjectRequest ;
import software.amazon.awssdk.services.s3.model.PutObjectRequest ;
import software.amazon.awssdk.services.s3.presigner.S3Presigner ;
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest ;
import java.io.ByteArrayInputStream ;
import java.u til.Date ;
import java.util.concurrent.TimeUnit ;
import java.net.URI ;
import java.time.Duration ;
/**
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
* <p>
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
*
* @author 芋道源码
*/
public class S3FileClient extends AbstractFileClient < S3FileClientConfig > {
private Amazon S3Client client ;
private S3Client client ;
private S3Presigner presigner ;
public S3FileClient ( Long id , S3FileClientConfig config ) {
super ( id , config ) ;
@@ -38,31 +40,78 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
if ( StrUtil . isEmpty ( config . getDomain ( ) ) ) {
config . setDomain ( buildDomain ( ) ) ;
}
// 初始化客户端
client = ( AmazonS3Client ) AmazonS3ClientBuilder . standard ( )
. withCredentials ( buildCredentials ( ) )
. withEndpointConfiguration ( buildEndpointConfiguration ( ) )
// 初始化 S3 客户端
Region region = Region . of ( " us-east-1 " ) ; // 必须填,但填什么都行,常见的值有 "us-east-1",不填会报错
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider . create (
AwsBasicCredentials . create ( config . getAccessKey ( ) , config . getAccessSecret ( ) ) ) ;
URI endpoint = URI . create ( buildEndpoint ( ) ) ;
S3Configuration serviceConfiguration = S3Configuration . builder ( ) // Path-style 访问
. pathStyleAccessEnabled ( Boolean . TRUE . equals ( config . getEnablePathStyleAccess ( ) ) ) . build ( ) ;
client = S3Client . builder ( )
. credentialsProvider ( credentialsProvider )
. region ( region )
. endpointOverride ( endpoint )
. serviceConfiguration ( serviceConfiguration )
. build ( ) ;
presigner = S3Presigner . builder ( )
. credentialsProvider ( credentialsProvider )
. region ( region )
. endpointOverride ( endpoint )
. serviceConfiguration ( serviceConfiguration )
. build ( ) ;
}
/**
* 基于 config 秘钥,构建 S3 客户端的认证信息
*
* @return S3 客户端的认证信息
*/
private AWSStaticCredentialsProvider buildCredentials ( ) {
return new AWSStaticCredentialsProvider (
new BasicAWSCredentials ( config . getAccessKey ( ) , config . getAccessSecret ( ) ) ) ;
@Override
public String upload ( byte [ ] content , String path , String type ) {
// 构造 PutObjectRequest
PutObjectRequest putRequest = PutObjectRequest . builder ( )
. bucket ( config . getBucket ( ) )
. key ( path )
. contentType ( type )
. contentLength ( ( long ) content . length )
. build ( ) ;
// 上传文件
client . putObject ( putRequest , RequestBody . fromBytes ( content ) ) ;
// 拼接返回路径
return config . getDomain ( ) + " / " + path ;
}
@Override
public void delete ( String path ) {
DeleteObjectRequest deleteRequest = DeleteObjectRequest . builder ( )
. bucket ( config . getBucket ( ) )
. key ( path )
. build ( ) ;
client . deleteObject ( deleteRequest ) ;
}
@Override
public byte [ ] getContent ( String path ) {
GetObjectRequest getRequest = GetObjectRequest . builder ( )
. bucket ( config . getBucket ( ) )
. key ( path )
. build ( ) ;
return IoUtil . readBytes ( client . getObject ( getRequest ) ) ;
}
@Override
public FilePresignedUrlRespDTO getPresignedObjectUrl ( String path ) {
Duration expiration = Duration . ofHours ( 24 ) ;
return new FilePresignedUrlRespDTO ( getPresignedUrl ( path , expiration ) , config . getDomain ( ) + " / " + path ) ;
}
/**
* 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint
* 生成动态的预签名上传 URL
*
* @return S3 客户端的 EndpointConfiguration 配置
* @param path 相对路径
* @param expiration 过期时间
* @return 生成的上传 URL
*/
private AwsClientBuilder . EndpointConfiguration buildEndpointConfigu ration( ) {
return new AwsClientBuilder . EndpointConfiguration ( config . getEndpoint ( ) ,
null ) ; // 无需设置 region
private String getPresignedUrl ( String path , Duration expi ration) {
return presigner . presignPutObject ( PutObjectPresignRequest . builder ( )
. signatureDuration ( expiration )
. putObjectRequest ( b - > b . bucket ( config . getBucket ( ) ) . key ( path ) )
. build ( ) ) . url ( ) . toString ( ) ;
}
/**
@@ -79,40 +128,17 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
return StrUtil . format ( " https://{}.{} " , config . getBucket ( ) , config . getEndpoint ( ) ) ;
}
@Override
public String upload ( byte [ ] content , String path , String type ) throws Exception {
// 元数据,主要用于设置文件类型
ObjectMetadata objectMetadata = new ObjectMetadata ( ) ;
objectMetadata . setContentType ( type ) ;
objectMetadata . setContentLength ( content . length ) ; // 如果不设置,会有 “ No content length specified for stream data” 警告日志
// 执行上传
client . putObject ( config . getBucke t ( ) ,
path , // 相对路径
new ByteArrayInputStream ( content ) , // 文件内容
objectMetadata ) ;
// 拼接返回路径
return config . getDomain ( ) + " / " + path ;
/**
* 节点地址补全协议头
*
* @return 节点地址
*/
private String buildEndpoint ( ) {
// 如果已经是 http 或者 https, 则不进行拼接
if ( HttpUtil . isHttp ( config . getEndpoint ( ) ) | | HttpUtil . isHttps ( config . getEndpoin t ( ) ) ) {
return config . getEndpoint ( ) ;
}
@Override
public void delete ( String path ) throws Exception {
client . deleteObject ( config . getBucket ( ) , path ) ;
}
@Override
public byte [ ] getContent ( String path ) throws Exception {
S3Object tempS3Object = client . getObject ( config . getBucket ( ) , path ) ;
return IoUtil . readBytes ( tempS3Object . getObjectContent ( ) ) ;
}
@Override
public FilePresignedUrlRespDTO getPresignedObjectUrl ( String path ) throws Exception {
// 设定过期时间为 10 分钟。取值范围: 1 秒 ~ 7 天
Date expiration = new Date ( System . currentTimeMillis ( ) + TimeUnit . MINUTES . toMillis ( 10 ) ) ;
// 生成上传 URL
String uploadUrl = String . valueOf ( client . generatePresignedUrl ( config . getBucket ( ) , path , expiration , HttpMethod . PUT ) ) ;
return new FilePresignedUrlRespDTO ( uploadUrl , config . getDomain ( ) + " / " + path ) ;
return StrUtil . format ( " https://{} " , config . getEndpoint ( ) ) ;
}
}