package com.monkeylessey.file.config;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.auth.sts.AssumeRoleRequest;
import com.aliyuncs.auth.sts.AssumeRoleResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.monkeylessey.constant.RedisKeyPrefixConstants;
import com.monkeylessey.enums.ConstantEnum;
import com.monkeylessey.exception.UploadException;
import com.monkeylessey.file.domain.entity.FileInfo;
import com.monkeylessey.file.properties.OssProperties;
import com.monkeylessey.response.Result;
import com.monkeylessey.framework.utils.RedisUtil;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author 29443
* @date 2022/4/23
*/
@Component
@RequiredArgsConstructor
public class OssTemplate {
private final OssProperties ossProperties;
private final RedisUtil redisUtil;
private final OSS ossClient;
private final static Logger log = LoggerFactory.getLogger(OssTemplate.class);
private final static String TMP_FILE_PREFIX = "/usr/local/tmpfile/";
/**
* STS获取令牌、AccessId、AccessKeySecret
*
* @return
*/
public void getSTSToken() {
String securityToken = "";
// regionId表示RAM的地域ID。以华东1(杭州)地域为例,regionID填写为cn-hangzhou。也可以保留默认值,默认值为空字符串("")。
String regionId = "cn-chengdu";
// 添加endpoint。适用于Java SDK 3.12.0及以上版本。
DefaultProfile.addEndpoint(regionId, "Sts", ossProperties.getStsEndpoint());
// 添加endpoint。适用于Java SDK 3.12.0以下版本。
// DefaultProfile.addEndpoint("",regionId, "Sts", endpoint);
// 构造default profile。
IClientProfile profile = DefaultProfile.getProfile(regionId,
ossProperties.getAccessKeyId(),
ossProperties.getAccessKeySecret());
// 构造client。
DefaultAcsClient client = new DefaultAcsClient(profile);
final AssumeRoleRequest request = new AssumeRoleRequest();
// 适用于Java SDK 3.12.0及以上版本。
request.setSysMethod(MethodType.POST);
// 适用于Java SDK 3.12.0以下版本。
//request.setMethod(MethodType.POST);
request.setRoleArn(ossProperties.getRoleArn());
request.setRoleSessionName(ossProperties.getRoleSessionName());
String policy = "{\n" +
" \"Version\": \"1\",\n" +
" \"Statement\": [\n" +
" {\n" +
" \"Effect\": \"Allow\",\n" +
" \"Action\": \"oss:PutObject\",\n" +
" \"Resource\": [\n" +
" \"acs:oss:*:*:xpstart-test/*\",\n" +
" \"acs:oss:*:*:xpstart-test/exampledir/*\"\n" +
" ]\n" +
" }\n" +
" ]\n" +
" }";
request.setPolicy(policy); // 如果policy为空,则用户将获得该角色下所有权限。
request.setDurationSeconds(ossProperties.getStsExpireTime()); // 设置临时访问凭证的有效时间为3600秒。
try {
final AssumeRoleResponse response = client.getAcsResponse(request);
System.out.println("Expiration: " + response.getCredentials().getExpiration());
System.out.println("Access Key Id: " + response.getCredentials().getAccessKeyId());
System.out.println("Access Key Secret: " + response.getCredentials().getAccessKeySecret());
System.out.println("Security Token: " + response.getCredentials().getSecurityToken());
System.out.println("RequestId: " + response.getRequestId());
// 获得的临时访问令牌
securityToken = response.getCredentials().getSecurityToken();
// RAM角色临时的AccessKeyId
String accessKeyId = response.getCredentials().getAccessKeyId();
// RAM角色临时的AccessKeySecret
String accessKeySecret = response.getCredentials().getAccessKeySecret();
redisUtil.saveForValueWithExpire(RedisKeyPrefixConstants.OSSTOKEN,
securityToken,
ossProperties.getStsExpireTime() - 100,
TimeUnit.SECONDS);
redisUtil.saveForValueWithExpire(RedisKeyPrefixConstants.OSSACCESSKEYID,
accessKeyId,
ossProperties.getStsExpireTime() - 100,
TimeUnit.SECONDS);
redisUtil.saveForValueWithExpire(RedisKeyPrefixConstants.OSSACCESSKEYSECRET,
accessKeySecret,
ossProperties.getStsExpireTime() - 100,
TimeUnit.SECONDS);
} catch (ClientException e) {
// todo 抛出获取sts临时访问令牌的异常
System.out.println("Failed:");
System.out.println("Error code: " + e.getErrCode());
System.out.println("Error message: " + e.getErrMsg());
System.out.println("RequestId: " + e.getRequestId());
}
}
/**
* 追加上传,第一次上传:metadata 可选
* 后续追加
* @see nextAppend()
* @param content
* @param fileInfo
* @param metadata
* @return
*/
public AppendObjectRequest firstAppend(InputStream content, FileInfo fileInfo, @Nullable ObjectMetadata metadata) {
if (Objects.isNull(content)) {
throw new UploadException("请传入要追加的内容");
}
if (Objects.isNull(metadata)) {
metadata = getDefaultMetadata(fileInfo);
}
// 构造追加上传请求
AppendObjectRequest request = getAppendObjectRequest(content, fileInfo, metadata);
// 设置文件的追加位置。
request.setPosition(0L);
AppendObjectResult appendObjectResult = ossClient.appendObject(request);
request.setPosition(appendObjectResult.getNextPosition());
log.info("下一次追加位置:【{}】", appendObjectResult.getNextPosition());
return request;
}
/**
* 后续追加上传方法
* @param request
* @return 返回的AppendObjectRequest对象已经设置好了:下一次追加位置
*/
public AppendObjectRequest nextAppend(AppendObjectRequest request) {
if (Objects.isNull(request)) {
throw new UploadException("请传入追加上传的请求对象");
}
if (Objects.isNull(request.getInputStream())) {
throw new UploadException("请在上传请求中设置追加内容");
}
AppendObjectResult appendObjectResult = ossClient.appendObject(request);
request.setPosition(appendObjectResult.getNextPosition());
log.info("下一次追加位置:【{}】", appendObjectResult.getNextPosition());
return request;
}
/**
* 构造默认的ObjectMetadata
* @param fileInfo
* @return ObjectMetadata,包含下载文件名、文件长度
*/
public ObjectMetadata getDefaultMetadata(FileInfo fileInfo) {
ObjectMetadata meta = new ObjectMetadata();
// 指定该Object被下载时的名称。
meta.setContentDisposition("attachment;filename=" + fileInfo.getOriginalFilename());
return meta;
// 设置文件内容长度
// meta.setContentLength(fileInfo.getSize());
// 指定上传的内容类型。
//meta.setContentType("text/plain");
// 指定该Object的网页缓存行为。
//meta.setCacheControl("no-cache");
// 指定该Object的内容编码格式。
//meta.setContentEncoding(OSSConstants.DEFAULT_CHARSET_NAME);
// 该请求头用于检查消息内容是否与发送时一致。
//meta.setContentMD5("ohhnqLBJFiKkPSBO1eNaUA==");
// 指定过期时间。
//try {
// meta.setExpirationTime(DateUtil.parseRfc822Date("Wed, 08 Jul 2022 16:57:01 GMT"));
//} catch (ParseException e) {
// e.printStackTrace();
//}
// 指定服务器端加密方式。此处指定为OSS完全托管密钥进行加密(SSE-OSS)。
//meta.setServerSideEncryption(ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION);
// 指定Object的访问权限。此处指定为私有访问权限。
//meta.setObjectAcl(CannedAccessControlList.Private);
// 指定Object的存储类型。
//meta.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard);
// 创建AppendObject时可以添加x-oss-meta-*,继续追加时不可以携带此参数。如果配置以x-oss-meta-*为前缀的参数,则该参数视为元数据。
//meta.setHeader("x-oss-meta-author", "Alice");
}
/**
* 构造追加上传请求
* @param content
* @param fileInfo
* @param metadata
* @return
*/
public AppendObjectRequest getAppendObjectRequest(InputStream content, FileInfo fileInfo, ObjectMetadata metadata) {
// 通过构造函数设置多个参数。
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(ossProperties.getBucketName(), fileInfo.getFileKey(), content, metadata);
// 通过AppendObjectRequest设置单个参数。
// 设置Bucket名称。
//appendObjectRequest.setBucketName(bucketName);
// 设置Object名称。
//appendObjectRequest.setKey(objectName);
// 设置待追加的内容。可选类型包括InputStream类型和File类型。此处为InputStream类型。
//appendObjectRequest.setInputStream(new ByteArrayInputStream(content1.getBytes()));
// 设置待追加的内容。可选类型包括InputStream类型和File类型。此处为File类型。
//appendObjectRequest.setFile(new File("D:\\localpath\\examplefile.txt"));
// 指定文件的元信息,第一次追加时有效。
//appendObjectRequest.setMetadata(meta);
return appendObjectRequest;
}
/**
* 上传文件
*
* @param stream 文件输入流
* @param info 文件信息
*/
public void putObject(InputStream stream, FileInfo info) throws IOException {
// 设置对象元信息
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(info.getSize());
objectMetadata.setContentType(info.getContentType());
// 阿里云的文件访问链接默认是浏览器直接下载,加上这个指定下载的文件名
// 如果想让链接是预览而不是下载,那么需要将目标bucket绑定一个域名。通过域名的形式访问即可,详情见https://help.aliyun.com/document_detail/31836.html
objectMetadata.setContentDisposition("attachment;filename=" + URLEncoder.encode(info.getOriginalFilename(), "UTF-8"));
ossClient.putObject(ossProperties.getBucketName(), info.getFileKey(), stream, objectMetadata);
}
/**
* 获取有时效的访问链接, 业务代码需要调用该方法刷新访问链接
*
* @param fileKey oss上的文件名
*/
public String getHasExpireUrl(String fileKey) {
// 公司阿里云oss都设置为私有,所以每次需要重新获取文件链接
// 使用redis做缓存,上传文件后缓存url。
String fileUrl = redisUtil.getValue(ConstantEnum.FILE_URL_REDIS_PREFIX.getValue() + fileKey, String.class);
if (StringUtils.hasText(fileUrl)) {
return fileUrl;
}
Date expiration = new Date(System.currentTimeMillis() + ossProperties.getUrlExpireTime() * 1000);
URL url = ossClient.generatePresignedUrl(ossProperties.getBucketName(), fileKey, expiration);
fileUrl = url.toString();
redisUtil.saveForValueWithExpire(ConstantEnum.FILE_URL_REDIS_PREFIX.getValue() + fileKey, fileUrl, ossProperties.getUrlExpireTime() - 100, TimeUnit.SECONDS);
return fileUrl;
}
/**
* 获取没有时效的访问链接
*
* @param fileKey oss上的文件名
* https://help.aliyun.com/document_detail/39607.htm?spm=a2c4g.11186623.0.0.16817f7aYiSCPO#concept-39607-zh
*/
public String getNoExpireUrl(String fileKey) {
// 无过期时间的访问url, 按照阿里云的规则拼接即可
// 需要将bucket设置为公共读才行
String url = new StringBuilder().append("https://")
.append(ossProperties.getBucketName())
.append(".")
.append(ossProperties.getEndpoint())
.append("/")
.append(fileKey)
.toString();
return url;
}
/**
* 删除某个文件
*
* @param fileKey oss上的文件名
*/
public void removeObject(String fileKey) {
ossClient.deleteObject(ossProperties.getBucketName(), fileKey);
}
/**
* 获取某个文件
*
* @param fileKey oss上的文件名
* @return
*/
public InputStream getObject(String fileKey) {
OSSObject object = ossClient.getObject(ossProperties.getBucketName(), fileKey);
return object.getObjectContent();
}
/**
* 断点续传
* @param file
* @return
*/
public Result checkPointUpload(InputStream file, FileInfo fileInfo) throws Throwable {
String tmpFile = TMP_FILE_PREFIX + fileInfo.getFileKey();
// 将用户上传的文件缓存下来
this.saveTmpFile(file, tmpFile);
// 构造上传请求
UploadFileRequest uploadFileRequest = new UploadFileRequest(ossProperties.getBucketName(), fileInfo.getFileKey());
// 设置断点续传
uploadFileRequest.setEnableCheckpoint(Boolean.TRUE);
// 指定上传并发线程数,默认为一
uploadFileRequest.setTaskNum(5);
// 设置每个分片的大小:单位为字节,取值范围为100 KB~5 GB。默认值为100 KB。
uploadFileRequest.setPartSize(1 * 1024 * 1024);
// 记录本地分片上传结果的文件。上传过程中的进度信息会保存在该文件中,如果某一分片上传失败,再次上传时会根据文件中记录的点继续上传。上传完成后,该文件会被删除。
// 如果未设置该值,默认与待上传的本地文件同路径,名称为${uploadFile}.ucp。
uploadFileRequest.setCheckpointFile(fileInfo.getFileKey() + ".ucp");
// 设置要上传的文件
uploadFileRequest.setUploadFile(tmpFile);
// 设置回调
// uploadFileRequest.setCallback(new Callback());
// 上传
ossClient.uploadFile(uploadFileRequest);
return Result.ok();
}
/**
* 将用户上传的文件缓存下来
* @param in
* @param tmpFilePath 临时文件路径
* @throws IOException
*/
private void saveTmpFile(InputStream in, String tmpFilePath) throws IOException {
File tmp = new File(tmpFilePath);
if (! tmp.exists()) {
tmp.createNewFile();
}
FileOutputStream fileOutputStream = new FileOutputStream(tmp);
int len = -1;
byte[] b=new byte[1024];
while ((len = in.read(b)) != -1) {
fileOutputStream.write(b, 0, len);
}
in.close();
fileOutputStream.close();
}
}