• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

weimin96 / oss-spring-boot-starter / #5

06 Apr 2026 02:14PM UTC coverage: 42.506% (-21.1%) from 63.625%
#5

push

github

web-flow
Merge pull request #9 from weimin96/spring3

Spring3

52 of 509 new or added lines in 8 files covered. (10.22%)

536 of 1261 relevant lines covered (42.51%)

0.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

19.63
/oss-spring-boot3-starter/src/main/java/com/wiblog/oss/controller/OssController.java
1
package com.wiblog.oss.controller;
2

3
import com.wiblog.oss.bean.ObjectInfo;
4
import com.wiblog.oss.bean.ObjectTreeNode;
5
import com.wiblog.oss.bean.UnzipResult;
6
import com.wiblog.oss.bean.chunk.Chunk;
7
import com.wiblog.oss.bean.chunk.ChunkMerge;
8
import com.wiblog.oss.bean.chunk.ChunkTarget;
9
import com.wiblog.oss.bean.chunk.ChunkTask;
10
import com.wiblog.oss.resp.R;
11
import com.wiblog.oss.service.OssTemplate;
12
import com.wiblog.oss.util.Util;
13
import io.swagger.v3.oas.annotations.Operation;
14
import io.swagger.v3.oas.annotations.Parameter;
15
import io.swagger.v3.oas.annotations.tags.Tag;
16
import jakarta.servlet.http.HttpServletRequest;
17
import jakarta.servlet.http.HttpServletResponse;
18
import jakarta.validation.constraints.Min;
19
import jakarta.validation.constraints.NotBlank;
20
import jakarta.validation.constraints.NotNull;
21
import lombok.RequiredArgsConstructor;
22
import org.springframework.util.AntPathMatcher;
23
import org.springframework.validation.annotation.Validated;
24
import org.springframework.web.bind.annotation.*;
25
import org.springframework.web.multipart.MultipartFile;
26
import org.springframework.web.servlet.HandlerMapping;
27

28
import java.io.IOException;
29
import java.io.InputStream;
30
import java.time.Duration;
31
import java.util.List;
32
import java.util.Map;
33

34
/**
35
 * OSS HTTP 端点。
36
 *
37
 * <p>提供完整的对象存储 REST 接口,包括:</p>
38
 * <ul>
39
 *   <li>文件上传(普通上传 / 分片上传)</li>
40
 *   <li>文件查询(列表、详情、树形结构、懒加载)</li>
41
 *   <li>文件预览与下载(支持 Range 分段)</li>
42
 *   <li>文件删除(单个 / 批量 / 文件夹)</li>
43
 *   <li>文件复制与移动</li>
44
 *   <li>流式解压(ZIP 直接解压到 OSS)</li>
45
 *   <li>预签名 URL(GET / PUT)</li>
46
 *   <li>对象标签管理</li>
47
 *   <li>Bucket 管理(版本控制、生命周期、CORS、加密)</li>
48
 * </ul>
49
 *
50
 * @author panwm
51
 */
52
@Validated
53
@RestController
54
@RequiredArgsConstructor
55
@RequestMapping("${oss.http.prefix:}/oss")
56
@Tag(name = "OSS 对象存储接口")
57
public class OssController {
58

59
    private final OssTemplate ossTemplate;
60
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
61

62
    // ================================================================
63
    // 分片上传
64
    // ================================================================
65

66
    @PostMapping("/multipart/init")
67
    @Operation(summary = "初始化分片上传任务", description = "返回 uploadId,后续分片上传和合并都需要此 ID")
68
    public R<String> initTask(@Validated ChunkTask chunkTask) {
69
        String uploadId = ossTemplate.put().initTask(chunkTask);
1✔
70
        return R.data(uploadId);
1✔
71
    }
72

73
    @PostMapping("/multipart/chunk")
74
    @Operation(summary = "上传文件分片", description = "按分片号上传,允许并发,顺序无关")
75
    public R<ChunkTarget> chunk(@Validated Chunk chunk) {
76
        ChunkTarget target = ossTemplate.put().chunk(chunk);
1✔
77
        return R.data(target);
1✔
78
    }
79

80
    @PostMapping("/multipart/merge")
81
    @Operation(summary = "合并分片", description = "所有分片上传完毕后调用,完成对象创建")
82
    public R<ObjectInfo> merge(@Validated ChunkMerge chunkMerge) {
NEW
83
        ObjectInfo info = ossTemplate.put().merge(chunkMerge);
×
NEW
84
        return R.data(info);
×
85
    }
86

87
    @GetMapping("/multipart/parts")
88
    @Operation(summary = "查询已上传的分片列表", description = "可用于断点续传场景,判断哪些分片已上传")
89
    public R<List<?>> listParts(
90
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName,
91
            @Parameter(description = "uploadId", required = true) @NotBlank @RequestParam String uploadId) {
NEW
92
        List<?> parts = ossTemplate.put().listParts(
×
NEW
93
                ossTemplate.query().getOssProperties().getBucketName(), objectName, uploadId);
×
NEW
94
        return R.data(parts);
×
95
    }
96

97
    // ================================================================
98
    // 文件上传
99
    // ================================================================
100

101
    @PostMapping("/object")
102
    @Operation(summary = "上传文件", description = "单文件上传,支持自定义存储路径和文件名")
103
    public R<ObjectInfo> uploadObject(
104
            @Parameter(description = "上传文件", required = true)
105
            @NotNull @RequestParam("file") MultipartFile file,
106
            @Parameter(description = "存放路径", required = true)
107
            @NotBlank @RequestParam String path,
108
            @Parameter(description = "文件名,为空时使用原始文件名")
109
            @RequestParam(required = false) String filename) throws IOException {
110
        InputStream in = file.getInputStream();
1✔
111
        String name = Util.isBlank(filename) ? file.getOriginalFilename() : filename;
1✔
112
        ObjectInfo info = ossTemplate.put().putObject(path, name, in);
1✔
113
        return R.data(info);
1✔
114
    }
115

116
    @PostMapping("/folder")
117
    @Operation(summary = "创建文件夹(目录占位符)")
118
    public R<ObjectInfo> createFolder(
119
            @Parameter(description = "文件夹路径", required = true) @NotBlank @RequestParam String path) {
NEW
120
        ObjectInfo info = ossTemplate.put().mkdirs(path);
×
NEW
121
        return R.data(info);
×
122
    }
123

124
    // ================================================================
125
    // 文件删除
126
    // ================================================================
127

128
    @DeleteMapping("/object")
129
    @Operation(summary = "删除单个文件")
130
    public R<Void> deleteObject(
131
            @Parameter(description = "文件全路径(key)", required = true)
132
            @NotBlank @RequestParam String objectName) {
133
        ossTemplate.delete().removeObject(objectName);
1✔
134
        return R.success("删除成功");
1✔
135
    }
136

137
    @DeleteMapping("/objects")
138
    @Operation(summary = "批量删除文件", description = "一次最多 1000 个,超出请分批调用")
139
    public R<Void> deleteObjects(
140
            @Parameter(description = "文件 key 列表", required = true)
141
            @RequestBody @NotNull List<String> objectNames) {
NEW
142
        objectNames.forEach(key -> ossTemplate.delete().removeObject(key));
×
NEW
143
        return R.success("批量删除成功");
×
144
    }
145

146
    @DeleteMapping("/folder")
147
    @Operation(summary = "删除文件夹(递归删除文件夹下所有对象)")
148
    public R<Void> deleteFolder(
149
            @Parameter(description = "文件夹路径", required = true)
150
            @NotBlank @RequestParam String path) {
151
        ossTemplate.delete().removeFolder(path);
1✔
152
        return R.success("删除成功");
1✔
153
    }
154

155
    // ================================================================
156
    // 文件查询
157
    // ================================================================
158

159
    @GetMapping("/object")
160
    @Operation(summary = "获取文件详情(元数据)")
161
    public R<ObjectInfo> getObject(
162
            @Parameter(description = "文件全路径", required = true)
163
            @NotBlank @RequestParam String objectName) {
164
        ObjectInfo info = ossTemplate.query().getObjectInfo(objectName);
1✔
165
        return R.data(info);
1✔
166
    }
167

168
    @GetMapping("/object/exists")
169
    @Operation(summary = "检查文件是否存在")
170
    public R<Boolean> objectExists(
171
            @Parameter(description = "文件全路径", required = true)
172
            @NotBlank @RequestParam String objectName) {
NEW
173
        boolean exists = ossTemplate.query().checkExist(objectName);
×
NEW
174
        return R.data(exists);
×
175
    }
176

177
    @GetMapping("/object/list")
178
    @Operation(summary = "列举指定路径下的所有对象(含子路径)")
179
    public R<List<ObjectInfo>> listObjects(
180
            @Parameter(description = "目录路径", required = true)
181
            @NotBlank @RequestParam String path) {
182
        List<ObjectInfo> list = ossTemplate.query().listObjects(path);
1✔
183
        return R.data(list);
1✔
184
    }
185

186
    @GetMapping("/object/list/next-level")
187
    @Operation(summary = "列举指定路径下一层级的文件和文件夹")
188
    public R<List<ObjectTreeNode>> listNextLevel(
189
            @Parameter(description = "目录路径", required = true)
190
            @NotBlank @RequestParam String path) {
NEW
191
        List<ObjectTreeNode> list = ossTemplate.query().listNextLevel(path);
×
NEW
192
        return R.data(list);
×
193
    }
194

195
    @GetMapping("/object/list/lazy")
196
    @Operation(summary = "懒加载文件列表(分页)",
197
            description = "首次调用不传 continuationToken,后续将上次返回的 token 传入实现翻页")
198
    public R<?> lazyList(
199
            @Parameter(description = "目录路径", required = true) @NotBlank @RequestParam String path,
200
            @Parameter(description = "每页数量,默认 100") @RequestParam(defaultValue = "100") @Min(1) int maxKeys,
201
            @Parameter(description = "分页 token,首次无需传入") @RequestParam(required = false) String continuationToken) {
NEW
202
        return R.data(ossTemplate.query().lazyList(path, maxKeys, continuationToken));
×
203
    }
204

205
    @GetMapping("/object/tree")
206
    @Operation(summary = "获取完整目录树")
207
    public R<ObjectTreeNode> getObjectTree(
208
            @Parameter(description = "根路径", required = true)
209
            @NotBlank @RequestParam String path) {
210
        ObjectTreeNode tree = ossTemplate.query().getTreeList(path);
1✔
211
        return R.data(tree);
1✔
212
    }
213

214
    @GetMapping("/object/tree/search")
215
    @Operation(summary = "按关键字搜索并返回目录树")
216
    public R<ObjectTreeNode> searchObjectTree(
217
            @Parameter(description = "根路径", required = true) @NotBlank @RequestParam String path,
218
            @Parameter(description = "搜索关键字", required = true) @NotBlank @RequestParam String keyword) {
NEW
219
        ObjectTreeNode tree = ossTemplate.query().getTreeListByName(path, keyword);
×
NEW
220
        return R.data(tree);
×
221
    }
222

223
    @GetMapping("/object/tree/folder")
224
    @Operation(summary = "获取文件夹树(仅包含文件夹节点,不含文件)")
225
    public R<List<ObjectTreeNode>> getFolderTree(
226
            @Parameter(description = "根路径", required = true)
227
            @NotBlank @RequestParam String path) {
NEW
228
        List<ObjectTreeNode> tree = ossTemplate.query().getFolderTreeList(path);
×
NEW
229
        return R.data(tree);
×
230
    }
231

232
    @GetMapping("/buckets")
233
    @Operation(summary = "列举所有 Bucket")
234
    public R<List<?>> listBuckets() {
NEW
235
        return R.data(ossTemplate.query().getAllBuckets());
×
236
    }
237

238
    @GetMapping("/connect")
239
    @Operation(summary = "测试 OSS 连接与 Bucket 可访问性")
240
    public R<Boolean> testConnect() {
NEW
241
        boolean ok = ossTemplate.query().testConnect();
×
NEW
242
        return R.data(ok);
×
243
    }
244

245
    // ================================================================
246
    // 文件预览 / 下载
247
    // ================================================================
248

249
    @GetMapping("/object/preview/**")
250
    @Operation(summary = "预览文件", description = "以 inline 方式呈现,支持 Range 分段请求,适用于视频/音频流")
251
    public void previewObject(HttpServletResponse response, HttpServletRequest request) throws IOException {
NEW
252
        ossTemplate.query().previewObject(request, response, extractObjectName(request), false);
×
NEW
253
    }
×
254

255
    @GetMapping("/object/download/**")
256
    @Operation(summary = "下载文件", description = "以 attachment 方式触发浏览器下载")
257
    public void downloadObject(HttpServletResponse response, HttpServletRequest request) throws IOException {
NEW
258
        ossTemplate.query().previewObject(request, response, extractObjectName(request), true);
×
NEW
259
    }
×
260

261
    // ================================================================
262
    // 文件复制 / 移动
263
    // ================================================================
264

265
    @PostMapping("/object/copy")
266
    @Operation(summary = "复制文件(同 Bucket 内)")
267
    public R<Void> copyObject(
268
            @Parameter(description = "源文件 key", required = true) @NotBlank @RequestParam String sourceKey,
269
            @Parameter(description = "目标文件 key", required = true) @NotBlank @RequestParam String destKey) {
NEW
270
        ossTemplate.put().copyFile(sourceKey, destKey);
×
NEW
271
        return R.success("复制成功");
×
272
    }
273

274
    @PostMapping("/object/move")
275
    @Operation(summary = "移动文件(复制后删除源文件)")
276
    public R<Void> moveObject(
277
            @Parameter(description = "源文件 key", required = true) @NotBlank @RequestParam String sourceKey,
278
            @Parameter(description = "目标目录路径", required = true) @NotBlank @RequestParam String destPath) {
NEW
279
        ossTemplate.put().move(sourceKey, destPath);
×
NEW
280
        return R.success("移动成功");
×
281
    }
282

283
    // ================================================================
284
    // 流式解压
285
    // ================================================================
286

287
    @PostMapping("/unzip")
288
    @Operation(summary = "流式解压 ZIP 文件",
289
            description = "将 OSS 中的 ZIP 对象边下载边解压,解压后的文件写入目标路径,无需落盘")
290
    public R<UnzipResult> unzip(
291
            @Parameter(description = "ZIP 文件的 key", required = true)
292
            @NotBlank @RequestParam String zipObjectKey,
293
            @Parameter(description = "解压后文件存放的目标路径前缀", required = true)
294
            @NotBlank @RequestParam String targetPath) {
NEW
295
        UnzipResult result = ossTemplate.unzip().unzip(zipObjectKey, targetPath);
×
NEW
296
        return R.data(result);
×
297
    }
298

299
    @PostMapping("/unzip/cross-bucket")
300
    @Operation(summary = "跨 Bucket 流式解压 ZIP 文件")
301
    public R<UnzipResult> unzipCrossBucket(
302
            @Parameter(description = "源 Bucket", required = true) @NotBlank @RequestParam String sourceBucket,
303
            @Parameter(description = "ZIP 文件 key", required = true) @NotBlank @RequestParam String zipObjectKey,
304
            @Parameter(description = "目标 Bucket", required = true) @NotBlank @RequestParam String targetBucket,
305
            @Parameter(description = "目标路径前缀", required = true) @NotBlank @RequestParam String targetPath) {
NEW
306
        UnzipResult result = ossTemplate.unzip().unzip(sourceBucket, zipObjectKey, targetBucket, targetPath);
×
NEW
307
        return R.data(result);
×
308
    }
309

310
    @PostMapping("/unzip/filter")
311
    @Operation(summary = "流式解压 ZIP 文件(按路径前缀过滤)",
312
            description = "只解压 ZIP 包内以 entryPrefix 开头的条目,实现「只解压部分文件」的需求")
313
    public R<UnzipResult> unzipWithFilter(
314
            @Parameter(description = "ZIP 文件 key", required = true) @NotBlank @RequestParam String zipObjectKey,
315
            @Parameter(description = "ZIP 内条目路径前缀,为空则解压全部") @RequestParam(required = false) String entryPrefix,
316
            @Parameter(description = "目标路径前缀", required = true) @NotBlank @RequestParam String targetPath) {
NEW
317
        UnzipResult result = ossTemplate.unzip().unzipWithFilter(zipObjectKey, entryPrefix, targetPath);
×
NEW
318
        return R.data(result);
×
319
    }
320

321
    // ================================================================
322
    // 预签名 URL
323
    // ================================================================
324

325
    @GetMapping("/presign/get")
326
    @Operation(summary = "生成文件下载预签名 URL",
327
            description = "生成带临时鉴权的下载链接,默认有效期 1 小时,最长 7 天")
328
    public R<String> getPresignedUrl(
329
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName,
330
            @Parameter(description = "有效时长(秒),默认 3600") @RequestParam(defaultValue = "3600") @Min(1) long expirationSeconds) {
NEW
331
        String url = ossTemplate.presign().generateGetPresignedUrl(objectName, Duration.ofSeconds(expirationSeconds));
×
NEW
332
        return R.data(url);
×
333
    }
334

335
    @GetMapping("/presign/put")
336
    @Operation(summary = "生成文件上传预签名 URL",
337
            description = "前端可直接用此 URL 通过 HTTP PUT 上传文件,无需后端转发,减少带宽占用")
338
    public R<String> putPresignedUrl(
339
            @Parameter(description = "目标对象 key", required = true) @NotBlank @RequestParam String objectName,
340
            @Parameter(description = "MIME 类型,如 image/jpeg") @RequestParam(defaultValue = "application/octet-stream") String contentType,
341
            @Parameter(description = "有效时长(秒),默认 3600") @RequestParam(defaultValue = "3600") @Min(1) long expirationSeconds) {
NEW
342
        String url = ossTemplate.presign().generatePutPresignedUrl(
×
NEW
343
                objectName, contentType, Duration.ofSeconds(expirationSeconds), null);
×
NEW
344
        return R.data(url);
×
345
    }
346

347
    // ================================================================
348
    // 对象标签
349
    // ================================================================
350

351
    @GetMapping("/object/tags")
352
    @Operation(summary = "获取对象标签")
353
    public R<Map<String, String>> getObjectTags(
354
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName) {
NEW
355
        Map<String, String> tags = ossTemplate.tagging().getObjectTags(objectName);
×
NEW
356
        return R.data(tags);
×
357
    }
358

359
    @PutMapping("/object/tags")
360
    @Operation(summary = "设置对象标签(覆盖)", description = "会替换对象上已有的所有标签,每个对象最多 10 个标签")
361
    public R<Void> setObjectTags(
362
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName,
363
            @RequestBody @NotNull Map<String, String> tags) {
NEW
364
        ossTemplate.tagging().setObjectTags(objectName, tags);
×
NEW
365
        return R.success("标签设置成功");
×
366
    }
367

368
    @PatchMapping("/object/tags")
369
    @Operation(summary = "追加/更新对象标签(合并)", description = "保留已有标签,仅更新/新增指定键")
370
    public R<Void> mergeObjectTags(
371
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName,
372
            @RequestBody @NotNull Map<String, String> tags) {
NEW
373
        ossTemplate.tagging().mergeObjectTags(objectName, tags);
×
NEW
374
        return R.success("标签更新成功");
×
375
    }
376

377
    @DeleteMapping("/object/tags")
378
    @Operation(summary = "删除对象的所有标签")
379
    public R<Void> deleteObjectTags(
380
            @Parameter(description = "对象 key", required = true) @NotBlank @RequestParam String objectName) {
NEW
381
        ossTemplate.tagging().deleteObjectTags(objectName);
×
NEW
382
        return R.success("标签删除成功");
×
383
    }
384

385
    // ================================================================
386
    // Bucket 管理
387
    // ================================================================
388

389
    @PostMapping("/bucket")
390
    @Operation(summary = "创建 Bucket")
391
    public R<Void> createBucket(
392
            @Parameter(description = "Bucket 名称", required = true) @NotBlank @RequestParam String bucketName) {
NEW
393
        ossTemplate.put().createBucket(bucketName);
×
NEW
394
        return R.success("Bucket 创建成功");
×
395
    }
396

397
    @GetMapping("/bucket/versioning")
398
    @Operation(summary = "获取 Bucket 版本控制状态")
399
    public R<String> getVersioningStatus() {
NEW
400
        String status = ossTemplate.bucket().getVersioningStatus();
×
NEW
401
        return R.data(status);
×
402
    }
403

404
    @PutMapping("/bucket/versioning/enable")
405
    @Operation(summary = "启用 Bucket 版本控制")
406
    public R<Void> enableVersioning() {
NEW
407
        ossTemplate.bucket().enableVersioning();
×
NEW
408
        return R.success("版本控制已启用");
×
409
    }
410

411
    @PutMapping("/bucket/versioning/suspend")
412
    @Operation(summary = "挂起 Bucket 版本控制")
413
    public R<Void> suspendVersioning() {
NEW
414
        ossTemplate.bucket().suspendVersioning();
×
NEW
415
        return R.success("版本控制已挂起");
×
416
    }
417

418
    @GetMapping("/bucket/lifecycle")
419
    @Operation(summary = "获取 Bucket 生命周期规则")
420
    public R<List<?>> getLifecycleRules() {
NEW
421
        return R.data(ossTemplate.bucket().getLifecycleRules());
×
422
    }
423

424
    @DeleteMapping("/bucket/lifecycle")
425
    @Operation(summary = "删除 Bucket 所有生命周期规则")
426
    public R<Void> deleteLifecycleRules() {
NEW
427
        ossTemplate.bucket().deleteLifecycleRules();
×
NEW
428
        return R.success("生命周期规则已删除");
×
429
    }
430

431
    @PostMapping("/bucket/lifecycle/expiration")
432
    @Operation(summary = "添加文件过期删除规则",
433
            description = "指定路径前缀下的文件超过 expirationDays 天后自动删除")
434
    public R<Void> addExpirationRule(
435
            @Parameter(description = "规则 ID", required = true) @NotBlank @RequestParam String ruleId,
436
            @Parameter(description = "作用路径前缀,为空表示全 Bucket") @RequestParam(defaultValue = "") String prefix,
437
            @Parameter(description = "过期天数", required = true) @RequestParam @Min(1) int expirationDays) {
NEW
438
        ossTemplate.bucket().addExpirationRule(ruleId, prefix, expirationDays);
×
NEW
439
        return R.success("过期规则添加成功");
×
440
    }
441

442
    @GetMapping("/bucket/cors")
443
    @Operation(summary = "获取 Bucket CORS 配置")
444
    public R<List<?>> getCorsRules() {
NEW
445
        return R.data(ossTemplate.bucket().getCorsRules());
×
446
    }
447

448
    @PutMapping("/bucket/cors/allow-all")
449
    @Operation(summary = "设置 Bucket 允许所有来源的 CORS(前端直传场景)")
450
    public R<Void> allowAllOriginsCors() {
NEW
451
        ossTemplate.bucket().allowAllOriginsCors();
×
NEW
452
        return R.success("CORS 配置成功");
×
453
    }
454

455
    @DeleteMapping("/bucket/cors")
456
    @Operation(summary = "删除 Bucket CORS 配置")
457
    public R<Void> deleteCorsRules() {
NEW
458
        ossTemplate.bucket().deleteCorsRules();
×
NEW
459
        return R.success("CORS 配置已删除");
×
460
    }
461

462
    @GetMapping("/bucket/policy")
463
    @Operation(summary = "获取 Bucket 访问策略")
464
    public R<String> getBucketPolicy() {
NEW
465
        String policy = ossTemplate.bucket().getBucketPolicy();
×
NEW
466
        return R.data(policy);
×
467
    }
468

469
    @PutMapping("/bucket/policy")
470
    @Operation(summary = "设置 Bucket 访问策略", description = "传入 JSON 格式的 IAM 策略文档")
471
    public R<Void> putBucketPolicy(
472
            @RequestBody @NotBlank String policyJson) {
NEW
473
        ossTemplate.bucket().putBucketPolicy(policyJson);
×
NEW
474
        return R.success("策略设置成功");
×
475
    }
476

477
    @DeleteMapping("/bucket/policy")
478
    @Operation(summary = "删除 Bucket 访问策略")
479
    public R<Void> deleteBucketPolicy() {
NEW
480
        ossTemplate.bucket().deleteBucketPolicy();
×
NEW
481
        return R.success("策略已删除");
×
482
    }
483

484
    @PutMapping("/bucket/encryption/enable")
485
    @Operation(summary = "启用 Bucket 服务端加密(SSE-S3 / AES-256)")
486
    public R<Void> enableEncryption() {
NEW
487
        ossTemplate.bucket().enableServerSideEncryption();
×
NEW
488
        return R.success("加密已启用");
×
489
    }
490

491
    @PutMapping("/bucket/public-access/block")
492
    @Operation(summary = "开启 Bucket 公共访问屏蔽(最高安全级别)")
493
    public R<Void> blockAllPublicAccess() {
NEW
494
        ossTemplate.bucket().blockAllPublicAccess();
×
NEW
495
        return R.success("公共访问已屏蔽");
×
496
    }
497

498
    // ================================================================
499
    // Bucket 标签
500
    // ================================================================
501

502
    @GetMapping("/bucket/tags")
503
    @Operation(summary = "获取 Bucket 标签")
504
    public R<Map<String, String>> getBucketTags() {
NEW
505
        Map<String, String> tags = ossTemplate.tagging().getBucketTags();
×
NEW
506
        return R.data(tags);
×
507
    }
508

509
    @PutMapping("/bucket/tags")
510
    @Operation(summary = "设置 Bucket 标签(覆盖)")
511
    public R<Void> setBucketTags(@RequestBody @NotNull Map<String, String> tags) {
NEW
512
        ossTemplate.tagging().setBucketTags(tags);
×
NEW
513
        return R.success("Bucket 标签设置成功");
×
514
    }
515

516
    @DeleteMapping("/bucket/tags")
517
    @Operation(summary = "删除 Bucket 所有标签")
518
    public R<Void> deleteBucketTags() {
NEW
519
        ossTemplate.tagging().deleteBucketTags();
×
NEW
520
        return R.success("Bucket 标签已删除");
×
521
    }
522

523
    // ================================================================
524
    // 兼容旧版端点(保留,避免升级时接口断裂)
525
    // ================================================================
526

527
    /**
528
     * @deprecated 请使用 POST /multipart/init
529
     */
530
    @Deprecated
531
    @PostMapping("/initTask")
532
    @Operation(summary = "[已废弃] 初始化分片上传,请改用 /multipart/init")
533
    public R<String> initTaskLegacy(@Validated ChunkTask chunkTask) {
534
        return initTask(chunkTask);
1✔
535
    }
536

537
    /**
538
     * @deprecated 请使用 POST /multipart/chunk
539
     */
540
    @Deprecated
541
    @PostMapping("/chunk")
542
    @Operation(summary = "[已废弃] 上传分片,请改用 /multipart/chunk")
543
    public R<ChunkTarget> chunkLegacy(@Validated Chunk chunk) {
544
        return chunk(chunk);
1✔
545
    }
546

547
    /**
548
     * @deprecated 请使用 POST /multipart/merge
549
     */
550
    @Deprecated
551
    @PostMapping("/merge")
552
    @Operation(summary = "[已废弃] 合并分片,请改用 /multipart/merge")
553
    public R<ObjectInfo> mergeLegacy(@Validated ChunkMerge chunkMerge) {
NEW
554
        return merge(chunkMerge);
×
555
    }
556

557
    /**
558
     * @deprecated 请使用 GET /object
559
     */
560
    @Deprecated
561
    @GetMapping("/object/getObject")
562
    @Operation(summary = "[已废弃] 获取文件信息,请改用 GET /object")
563
    public R<ObjectInfo> getObjectLegacy(@NotBlank @RequestParam String objectName) {
564
        return getObject(objectName);
1✔
565
    }
566

567
    // ================================================================
568
    // 私有工具
569
    // ================================================================
570

571
    /**
572
     * 从通配符映射路径中提取对象 key。
573
     * 例:请求路径 /oss/object/preview/images/foo.jpg → 返回 images/foo.jpg
574
     */
575
    private String extractObjectName(HttpServletRequest request) {
NEW
576
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
×
NEW
577
        String pattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
×
NEW
578
        return antPathMatcher.extractPathWithinPattern(pattern, path);
×
579
    }
580
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc