xiangpei
2025-04-18 23da057f35cea1ee061adc23ccb9b7511635133b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
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绑定一个域名。通过域名的形式访问即可,详情见<a>https://help.aliyun.com/document_detail/31836.html</a>
        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上的文件名
     *                <url>https://help.aliyun.com/document_detail/39607.htm?spm=a2c4g.11186623.0.0.16817f7aYiSCPO#concept-39607-zh</url>
     */
    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();
    }
 
}