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(); } }