package tech.aiflowy.common.filestorage;
|
|
|
import cn.hutool.core.date.DateField;
|
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.io.FileTypeUtil;
|
import cn.hutool.core.io.IoUtil;
|
import cn.hutool.core.io.file.FileNameUtil;
|
import cn.hutool.core.util.IdUtil;
|
import cn.hutool.core.util.ObjectUtil;
|
import cn.hutool.core.util.StrUtil;
|
import com.amazonaws.ClientConfiguration;
|
import com.amazonaws.HttpMethod;
|
import com.amazonaws.Protocol;
|
import com.amazonaws.SdkClientException;
|
import com.amazonaws.auth.AWSCredentials;
|
import com.amazonaws.auth.AWSCredentialsProvider;
|
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
import com.amazonaws.auth.BasicAWSCredentials;
|
import com.amazonaws.client.builder.AwsClientBuilder;
|
import com.amazonaws.services.s3.AmazonS3;
|
import com.amazonaws.services.s3.AmazonS3Client;
|
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
|
import com.amazonaws.services.s3.model.*;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
import org.springframework.web.multipart.MultipartFile;
|
|
import java.io.*;
|
import java.net.URL;
|
import java.security.MessageDigest;
|
import java.util.Date;
|
|
/**
|
* S3 存储协议 所有兼容S3协议的云厂商均支持 阿里云 腾讯云 七牛云 minio
|
*
|
*
|
*/
|
public class OssClient {
|
private static final Logger log = LoggerFactory.getLogger(OssClient.class);
|
/**
|
* https 状态
|
*/
|
private final String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu", "obs"};
|
private static final int IS_HTTPS = 1;
|
private final StorageConfig properties;
|
|
private final AmazonS3 client;
|
|
public OssClient(StorageConfig ossProperties) {
|
this.properties = ossProperties;
|
try {
|
AwsClientBuilder.EndpointConfiguration endpointConfig =
|
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
|
|
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
|
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
|
ClientConfiguration clientConfig = new ClientConfiguration();
|
if (IS_HTTPS == properties.getIsHttps()) {
|
clientConfig.setProtocol(Protocol.HTTPS);
|
} else {
|
clientConfig.setProtocol(Protocol.HTTP);
|
}
|
AmazonS3ClientBuilder build = AmazonS3Client.builder().withEndpointConfiguration(endpointConfig)
|
.withClientConfiguration(clientConfig).withCredentials(credentialsProvider).disableChunkedEncoding();
|
if (isPathStyleAccessRequired(properties.getEndpoint())) {
|
build.enablePathStyleAccess();
|
}
|
|
this.client = build.build();
|
|
createBucket();
|
} catch (Exception e) {
|
throw new RuntimeException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
|
}
|
}
|
|
private boolean isPathStyleAccessRequired(String endpoint) {
|
return !StrUtil.containsAny(endpoint,
|
CLOUD_SERVICE);
|
}
|
public void createBucket() {
|
try {
|
String bucketName = properties.getBucketName();
|
if (client.doesBucketExistV2(bucketName)) {
|
return;
|
}
|
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
|
AccessPolicyType accessPolicy = getAccessPolicy();
|
createBucketRequest.setCannedAcl(accessPolicy.getAcl());
|
client.createBucket(createBucketRequest);
|
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
|
} catch (Exception e) {
|
throw new RuntimeException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
|
}
|
}
|
|
public void upload(byte[] data, String objectPath, String contentType) {
|
upload(new ByteArrayInputStream(data), objectPath, contentType);
|
}
|
public void upload(InputStream inputStream, String objectPath, String contentType) {
|
if (!(inputStream instanceof ByteArrayInputStream)) {
|
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
|
}
|
try {
|
ObjectMetadata metadata = new ObjectMetadata();
|
metadata.setContentType(contentType);
|
metadata.setContentLength(inputStream.available());
|
PutObjectRequest putObjectRequest =
|
new PutObjectRequest(properties.getBucketName(), objectPath, inputStream, metadata);
|
// 设置上传对象的 Acl 为公共读
|
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
|
client.putObject(putObjectRequest);
|
} catch (Exception e) {
|
throw new RuntimeException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
}
|
}
|
|
public String upload(MultipartFile file) throws Exception {
|
byte[] content = file.getBytes();
|
String name = file.getOriginalFilename();
|
String path = generatePath(content, name);
|
upload(content, path, file.getContentType());
|
return path;
|
}
|
|
public static String generatePath(byte[] content, String originalName) throws Exception {
|
// 计算文件内容的 SHA256 哈希值
|
String sha256Hex = sha256Hex(content);
|
|
// 情况一:如果存在原始文件名,优先使用其后缀
|
if (StrUtil.isNotBlank(originalName)) {
|
// 提取文件后缀
|
String extName = FileNameUtil.extName(originalName);
|
// 如果后缀存在,返回 "哈希值.后缀",否则返回 "哈希值"
|
return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName;
|
}
|
|
// 情况二:如果原始文件名为空,基于文件内容推断文件类型
|
return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content));
|
}
|
|
public static String sha256Hex(byte[] input) throws Exception {
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
byte[] hash = digest.digest(input);
|
|
StringBuilder hexString = new StringBuilder();
|
for (byte b : hash) {
|
String hex = Integer.toHexString(0xff & b);
|
if (hex.length() == 1) hexString.append('0');
|
hexString.append(hex);
|
}
|
return hexString.toString(); // 返回字符串类型的哈希值
|
}
|
/**
|
* 根据OSS对象存储路径, 从OSS存储删除文件
|
*
|
* @param objectPath
|
* 文件访问路径
|
*/
|
public void delete(String objectPath) {
|
try {
|
client.deleteObject(properties.getBucketName(), objectPath);
|
} catch (Exception e) {
|
throw new RuntimeException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
|
}
|
}
|
|
|
/**
|
* 根据OSS对象存储路径, 获取文件元数据
|
*
|
* @param objectPath
|
* 文件访问路径(对应OSS存储数据的key)
|
*/
|
public ObjectMetadata getObjectMetadata(String objectPath) {
|
return client.getObjectMetadata(properties.getBucketName(), objectPath);
|
}
|
|
/**
|
* 根据OSS对象存储路径, 获取文件内容
|
*
|
* @param objectPath
|
* 文件访问路径
|
* @return 文件内容
|
*/
|
public InputStream getObjectContent(String objectPath) {
|
return getObjectContent(objectPath, null, null);
|
}
|
|
/**
|
* 根据OSS对象存储路径, 获取文件内容(指定文件字节范围)
|
*
|
* @param objectPath
|
* 文件访问路径
|
* @param start
|
* 起始字节
|
* @param end
|
* 结束字节
|
* @return 文件内容(指定文件字节范围)
|
*/
|
public InputStream getObjectContent(String objectPath, Long start, Long end) {
|
GetObjectRequest request = new GetObjectRequest(properties.getBucketName(), objectPath);
|
if (start != null) {
|
if (end != null) {
|
request.setRange(start, end);
|
} else {
|
request.setRange(start);
|
}
|
}
|
S3Object object = client.getObject(request);
|
return object.getObjectContent();
|
}
|
|
private String getSchema() {
|
String schema = (IS_HTTPS == properties.getIsHttps() ? "https://" : "http://");
|
return schema;
|
}
|
|
/**
|
* 获取OSS基础路径
|
*
|
* @return 基础路径
|
*/
|
public String getBasePath() {
|
String domain = properties.getDomain();
|
String endpoint = properties.getEndpoint();
|
String schema = this.getSchema();
|
// 云服务厂商object url格式: <schema>://<bucket>.<endpoint>/<objectPath>
|
if (StrUtil.isNotBlank(domain)) {
|
return schema + domain;
|
}
|
return schema + properties.getBucketName() + "." + endpoint;
|
}
|
|
/**
|
* 根据访问url, 获取OSS对象存储路径
|
*
|
* @return OSS对象存储路径
|
*/
|
public String getObjectPath(String url) {
|
if (StrUtil.isBlank(url)) {
|
return url;
|
}
|
return url.replace(getBasePath() + "/", "");
|
}
|
|
/**
|
* 根据OSS对象存储路径, 获取访问url
|
*
|
* @param objectPath
|
* 文件访问路径
|
* @return 访问url
|
*/
|
public String getUrl(String objectPath) {
|
return getBasePath() + "/" + objectPath;
|
}
|
|
public String getUrl() {
|
String domain = properties.getDomain();
|
String endpoint = properties.getEndpoint();
|
String header = getSchema();
|
// 云服务商直接返回
|
if (StrUtil.isNotBlank(domain)) {
|
return header + domain;
|
}
|
return header + properties.getBucketName() + "." + endpoint;
|
}
|
|
/**
|
* 根据前缀和后缀, 生成OSS对象存储路径
|
*
|
* @param prefix
|
* 前缀
|
* @param suffix
|
* 后缀
|
* @return OSS对象存储路径
|
*/
|
public String getObjectPath(String prefix, String suffix) {
|
// 生成uuid
|
String uuid = IdUtil.fastSimpleUUID();
|
// 文件路径
|
String path = DateUtil.format(new Date(), "yyyy/MM/dd") + "/" + uuid;
|
if (StrUtil.isNotBlank(prefix)) {
|
path = prefix + "/" + path;
|
}
|
return path + suffix;
|
}
|
|
/**
|
* 获取私有URL链接
|
*
|
* @param objectKey
|
* 对象KEY
|
* @param second
|
* 授权时间
|
*/
|
public String getPrivateUrl(String objectKey, Integer second) {
|
GeneratePresignedUrlRequest generatePresignedUrlRequest =
|
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey).withMethod(HttpMethod.GET)
|
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
|
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
|
return url.toString();
|
}
|
|
/**
|
* 检查配置是否相同
|
*/
|
public boolean checkPropertiesSame(StorageConfig properties) {
|
return this.properties.equals(properties);
|
}
|
|
/**
|
* 获取当前桶权限类型
|
*
|
* @return 当前桶权限类型code
|
*/
|
public AccessPolicyType getAccessPolicy() {
|
return AccessPolicyType.getByType(properties.getAccessPolicy());
|
}
|
|
private static String getPolicy(String bucketName, PolicyType policyType) {
|
StringBuilder builder = new StringBuilder();
|
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
|
if (policyType == PolicyType.WRITE) {
|
builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n");
|
} else if (policyType == PolicyType.READ_WRITE) {
|
builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n");
|
} else {
|
builder.append("\"s3:GetBucketLocation\"\n");
|
}
|
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
builder.append(bucketName);
|
builder.append("\"\n},\n");
|
if (policyType == PolicyType.READ) {
|
builder.append(
|
"{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
builder.append(bucketName);
|
builder.append("\"\n},\n");
|
}
|
builder.append("{\n\"Action\": ");
|
switch (policyType) {
|
case WRITE:
|
builder.append(
|
"[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
|
break;
|
case READ_WRITE:
|
builder.append(
|
"[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n");
|
break;
|
default:
|
builder.append("\"s3:GetObject\",\n");
|
break;
|
}
|
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
|
builder.append(bucketName);
|
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
|
return builder.toString();
|
}
|
|
|
/**
|
* @param path
|
* 相对路径
|
* @return 文件内容
|
* @throws Exception
|
* @description 获取文件内容
|
*/
|
public InputStream getContent(String path) throws Exception {
|
return getObjectContent(path);
|
}
|
|
|
public String getPreSignedAccessUrl(String path) {
|
try {
|
return getPreSignedAccessUrl(path, DateField.MINUTE, 30);
|
} catch (Exception e) {
|
log.error("获取文件访问地址异常,path={}", path, e);
|
throw new RuntimeException(String.format("获取文件访问地址异常,path=[%s]:[%s]", path, e.getMessage()));
|
}
|
}
|
|
/**
|
* 获取私有URL链接
|
*
|
* @param objectKey
|
* 对象KEY
|
*/
|
public String getPreSignedAccessUrl(String objectKey, DateField dateField, Integer time) {
|
GeneratePresignedUrlRequest generatePresignedUrlRequest =
|
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey).withMethod(HttpMethod.GET)
|
.withExpiration(DateUtil.offset(DateUtil.date(), dateField, time));
|
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
|
return url.toString();
|
}
|
|
}
|