• 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

48.75
/oss-spring-boot3-starter/src/main/java/com/wiblog/oss/service/QueryOperations.java
1
package com.wiblog.oss.service;
2

3
import com.wiblog.oss.bean.LazyDataList;
4
import com.wiblog.oss.bean.ObjectInfo;
5
import com.wiblog.oss.bean.ObjectTreeNode;
6
import com.wiblog.oss.bean.OssProperties;
7
import com.wiblog.oss.util.Util;
8
import jakarta.servlet.http.HttpServletRequest;
9
import jakarta.servlet.http.HttpServletResponse;
10
import lombok.extern.slf4j.Slf4j;
11
import org.springframework.util.StringUtils;
12
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
13
import software.amazon.awssdk.services.s3.S3AsyncClient;
14
import software.amazon.awssdk.services.s3.model.*;
15
import software.amazon.awssdk.services.s3.paginators.ListObjectsV2Publisher;
16
import software.amazon.awssdk.transfer.s3.S3TransferManager;
17

18
import java.io.*;
19
import java.net.URLDecoder;
20
import java.net.URLEncoder;
21
import java.nio.ByteBuffer;
22
import java.nio.charset.StandardCharsets;
23
import java.util.*;
24
import java.util.stream.Collectors;
25

26
/**
27
 * 查询操作类
28
 *
29
 * @author panwm
30
 */
31
@Slf4j
1✔
32
public class QueryOperations extends Operations {
33

34
    /**
35
     * 预览/下载时的 IO 缓冲区大小 4KB
36
     */
37
    private static final int BUFFER_SIZE = 4 * 1024;
38

39
    /**
40
     * 列举对象时每页最大数量(可通过调整适配不同场景)
41
     */
42
    private static final int LIST_MAX_KEYS = 1000;
43

44
    public QueryOperations(OssProperties ossProperties, S3AsyncClient client, S3TransferManager transferManager) {
45
        super(ossProperties, client, transferManager);
1✔
46
    }
1✔
47

48
    /**
49
     * 暴露 OssProperties 供 Controller 等上层组件访问 bucketName 等配置。
50
     */
51
    public OssProperties getOssProperties() {
NEW
52
        return ossProperties;
×
53
    }
54

55
    // ----------------------------------------------------------------
56
    // 连接 / Bucket 检测
57
    // ----------------------------------------------------------------
58

59
    public boolean testConnect() {
60
        return testConnectForBucket();
1✔
61
    }
62

63
    public boolean testConnectForBucket(String bucketName) {
64
        try {
65
            client.headBucket(HeadBucketRequest.builder().bucket(bucketName).build()).join();
1✔
66
            return true;
1✔
67
        } catch (Exception e) {
×
68
            return false;
×
69
        }
70
    }
71

72
    public boolean testConnectForBucket() {
73
        return testConnectForBucket(ossProperties.getBucketName());
1✔
74
    }
75

76
    public List<Bucket> getAllBuckets() {
77
        return client.listBuckets().join().buckets();
×
78
    }
79

80
    // ----------------------------------------------------------------
81
    // 对象列表查询
82
    // ----------------------------------------------------------------
83

84
    public List<ObjectInfo> listObjects(String path) {
85
        return listObjects(ossProperties.getBucketName(), path);
1✔
86
    }
87

88
    public List<ObjectInfo> listObjects(String bucketName, String path) {
89
        return listObject(bucketName, path, null).stream()
1✔
90
                .map(e -> ObjectInfo.builder()
1✔
91
                        .uri(e.key())
1✔
92
                        .url(getDomain() + e.key())
1✔
93
                        .name(Util.getFilename(e.key()))
1✔
94
                        .uploadTime(Date.from(e.lastModified()))
1✔
95
                        .build())
1✔
96
                .collect(Collectors.toList());
1✔
97
    }
98

99
    public List<S3Object> listObject(String path) {
100
        return listObject(ossProperties.getBucketName(), path, null);
×
101
    }
102

103
    public List<S3Object> listObject(String bucketName, String path) {
104
        return listObject(bucketName, path, null);
1✔
105
    }
106

107
    public List<S3Object> listObject(String bucketName, String path, String keyword) {
108
        List<S3Object> list = new ArrayList<>();
1✔
109
        String prefix = Util.formatPath(path);
1✔
110

111
        ListObjectsV2Request request = ListObjectsV2Request.builder()
1✔
112
                .bucket(bucketName)
1✔
113
                .maxKeys(LIST_MAX_KEYS)
1✔
114
                .prefix(prefix)
1✔
115
                .build();
1✔
116

117
        ListObjectsV2Publisher publisher = client.listObjectsV2Paginator(request);
1✔
118
        publisher.subscribe(response -> {
1✔
119
            if (Util.isBlank(keyword)) {
1✔
120
                list.addAll(response.contents());
1✔
121
            } else {
122
                response.contents().stream()
×
123
                        .filter(e -> e.key().contains(keyword))
×
124
                        .forEach(list::add);
×
125
            }
126
        }).join();
1✔
127
        return list;
1✔
128
    }
129

130
    // ----------------------------------------------------------------
131
    // 懒加载列表
132
    // ----------------------------------------------------------------
133

134
    public LazyDataList<ObjectInfo> lazyList(String path, int maxKeys, String continuationToken) {
135
        return lazyList(ossProperties.getBucketName(), path, maxKeys, continuationToken);
1✔
136
    }
137

138
    public LazyDataList<ObjectInfo> lazyList(String bucketName, String path, int maxKeys, String continuationToken) {
139
        if (maxKeys <= 0) {
1✔
140
            maxKeys = 1000;
×
141
        }
142
        LazyDataList<ObjectInfo> resultList = new LazyDataList<>();
1✔
143

144
        ListObjectsV2Request.Builder builder = ListObjectsV2Request.builder()
1✔
145
                .bucket(bucketName)
1✔
146
                .prefix(Util.formatPath(path))
1✔
147
                .maxKeys(maxKeys)
1✔
148
                .delimiter("/");
1✔
149

150
        if (StringUtils.hasText(continuationToken)) {
1✔
151
            builder.continuationToken(continuationToken);
1✔
152
        } else {
153
            // 首次加载:先拿当前层所有文件夹
154
            resultList.addAll(listNextLevelFolder(bucketName, path));
1✔
155
        }
156

157
        ListObjectsV2Response response = client.listObjectsV2(builder.build()).join();
1✔
158
        response.contents().stream()
1✔
159
                .filter(e -> e.size() > 0)
1✔
160
                .map(e -> buildObjectInfo(e.key(), Date.from(e.lastModified()), e.size()))
1✔
161
                .forEach(resultList::add);
1✔
162

163
        resultList.setMaxKeys(maxKeys);
1✔
164
        resultList.setContinuationToken(response.nextContinuationToken());
1✔
165
        return resultList;
1✔
166
    }
167

168
    // ----------------------------------------------------------------
169
    // 树形结构查询
170
    // ----------------------------------------------------------------
171

172
    public List<ObjectTreeNode> listNextLevel(String path) {
173
        return listNextLevel(ossProperties.getBucketName(), path);
1✔
174
    }
175

176
    public List<ObjectTreeNode> listNextLevel(String bucketName, String path) {
177
        List<ObjectTreeNode> resultList = new ArrayList<>();
1✔
178
        String prefix = Util.formatPath(path);
1✔
179

180
        ListObjectsV2Request request = ListObjectsV2Request.builder()
1✔
181
                .bucket(bucketName).prefix(prefix)
1✔
182
                .maxKeys(LIST_MAX_KEYS).delimiter("/").build();
1✔
183

184
        Set<String> seen = new HashSet<>(64);
1✔
185
        client.listObjectsV2Paginator(request).subscribe(response -> {
1✔
186
            response.contents().stream()
1✔
187
                    .filter(e -> e.size() > 0)
1✔
188
                    .map(this::buildTreeNode)
1✔
189
                    .forEach(resultList::add);
1✔
190

191
            response.commonPrefixes().stream()
1✔
192
                    .map(CommonPrefix::prefix)
1✔
193
                    .filter(seen::add)       // distinct + track in one step
1✔
194
                    .map(this::buildTreeNode)
1✔
195
                    .forEach(resultList::add);
1✔
196
        }).join();
1✔
197
        return resultList;
1✔
198
    }
199

200
    public List<ObjectTreeNode> getFolderTreeList(String path) {
201
        return getFolderTreeList(ossProperties.getBucketName(), path);
×
202
    }
203

204
    public List<ObjectTreeNode> getFolderTreeList(String bucketName, String path) {
205
        String prefix = Util.formatPath(path);
×
206
        List<S3Object> list = new ArrayList<>();
×
207

208
        ListObjectsV2Request request = ListObjectsV2Request.builder()
×
209
                .bucket(bucketName).maxKeys(LIST_MAX_KEYS).prefix(prefix).build();
×
210

211
        client.listObjectsV2Paginator(request)
×
212
                .subscribe(r -> list.addAll(r.contents())).join();
×
213

214
        return buildFolderTree(list, prefix).getChildren();
×
215
    }
216

217
    public List<ObjectInfo> listNextLevelFolder(String path) {
218
        return listNextLevelFolder(ossProperties.getBucketName(), path);
×
219
    }
220

221
    public List<ObjectInfo> listNextLevelFolder(String bucketName, String path) {
222
        List<ObjectInfo> resultList = new ArrayList<>();
1✔
223
        String prefix = Util.formatPath(path);
1✔
224

225
        ListObjectsV2Request request = ListObjectsV2Request.builder()
1✔
226
                .bucket(bucketName).prefix(prefix).delimiter("/").build();
1✔
227

228
        Set<String> seen = new HashSet<>(64);
1✔
229
        client.listObjectsV2Paginator(request).subscribe(response ->
1✔
230
                response.commonPrefixes().stream()
1✔
231
                        .map(CommonPrefix::prefix)
1✔
232
                        .filter(seen::add)
1✔
233
                        .map(this::buildTreeNode)   // buildTreeNode(String) → folder
1✔
234
                        .map(node -> ObjectInfo.builder()
1✔
235
                                .uri(node.getUri()).url(node.getUrl())
1✔
236
                                .name(node.getName()).build())
1✔
237
                        .forEach(resultList::add)
1✔
238
        ).join();
1✔
239
        return resultList;
1✔
240
    }
241

242
    // ----------------------------------------------------------------
243
    // 文件存在性 & 元数据
244
    // ----------------------------------------------------------------
245

246
    public boolean checkExist(String objectName) {
247
        return checkExist(ossProperties.getBucketName(), objectName);
1✔
248
    }
249

250
    public boolean checkExist(String bucketName, String objectName) {
251
        try {
252
            client.headObject(HeadObjectRequest.builder().bucket(bucketName).key(objectName).build()).join();
1✔
253
            return true;
1✔
254
        } catch (Exception e) {
1✔
255
            return false;
1✔
256
        }
257
    }
258

259
    public ObjectInfo getObjectInfo(String objectName) {
260
        return getObjectInfo(ossProperties.getBucketName(), objectName);
1✔
261
    }
262

263
    public ObjectInfo getObjectInfo(String bucketName, String objectName) {
264
        HeadObjectRequest req = HeadObjectRequest.builder().bucket(bucketName).key(objectName).build();
1✔
265
        HeadObjectResponse response = handleRequest(() -> client.headObject(req));
1✔
266
        return buildObjectInfo(objectName, response);
1✔
267
    }
268

269
    // ----------------------------------------------------------------
270
    // 内容读取
271
    // ----------------------------------------------------------------
272

273
    public String getContent(String objectName) {
274
        return getContent(ossProperties.getBucketName(), objectName);
1✔
275
    }
276

277
    public String getContent(String bucketName, String objectName) {
278
        try {
279
            return client.getObject(buildGetRequest(bucketName, objectName),
1✔
280
                            AsyncResponseTransformer.toBytes())
1✔
281
                    .thenApply(rb -> StandardCharsets.UTF_8.decode(rb.asByteBuffer()).toString())
1✔
282
                    .join();
1✔
283
        } catch (NoSuchKeyException e) {
×
284
            log.error("File not found: [{}]", objectName);
×
285
            return null;
×
286
        }
287
    }
288

289
    public InputStream getInputStream(String objectName) {
290
        return getInputStream(ossProperties.getBucketName(), objectName);
×
291
    }
292

293
    public InputStream getInputStream(String bucketName, String objectName) {
294
        return handleRequest(() ->
×
295
                client.getObject(buildGetRequest(bucketName, objectName), AsyncResponseTransformer.toBytes())
×
296
                        .thenApply(rb -> toInputStream(rb.asByteBuffer())));
×
297
    }
298

299
    public InputStream getInputStream(String bucketName, String objectName, String range) {
300
        GetObjectRequest req = GetObjectRequest.builder()
×
301
                .bucket(bucketName).key(Util.formatPath(objectName)).range(range).build();
×
302
        return handleRequest(() ->
×
303
                client.getObject(req, AsyncResponseTransformer.toBytes())
×
304
                        .thenApply(rb -> toInputStream(rb.asByteBuffer())));
×
305
    }
306

307
    // ----------------------------------------------------------------
308
    // 文件下载
309
    // ----------------------------------------------------------------
310

311
    public File getFile(String objectName, String localFilePath) {
312
        return getFile(ossProperties.getBucketName(), objectName, localFilePath);
×
313
    }
314

315
    public File getFile(String bucketName, String objectName, String localFilePath) {
316
        File outputFile = new File(localFilePath);
×
317
        outputFile.getParentFile().mkdirs();
×
318

319
        if (!Util.checkIsFile(localFilePath)) {
×
320
            outputFile.mkdirs();
×
321
            outputFile = new File(Util.formatPath(localFilePath) + Util.getFilename(objectName));
×
322
        }
323
        File finalFile = outputFile;
×
324
        handleRequest(() -> client.getObject(buildGetRequest(bucketName, objectName),
×
325
                AsyncResponseTransformer.toFile(finalFile)));
×
326
        return outputFile;
×
327
    }
328

329
    public void getFolder(String objectName, String localFilePath) {
330
        getFolder(ossProperties.getBucketName(), objectName, localFilePath);
×
331
    }
×
332

333
    /**
334
     * 下载文件夹。
335
     * 修复:原代码使用 File.pathSeparator(值为";")而非 File.separator("/"或"\"),
336
     * 导致本地路径拼接错误。
337
     */
338
    public void getFolder(String bucketName, String objectName, String localFilePath) {
339
        List<S3Object> objects = listObject(bucketName, objectName, null);
×
340
        // 修复:使用 File.separator 而非 File.pathSeparator(原代码 Bug)
341
        if (!localFilePath.endsWith(File.separator)) {
×
342
            localFilePath += File.separator;
×
343
        }
344
        for (S3Object s3Object : objects) {
×
345
            // 将 S3 key 中的路径转为本地路径分隔符
346
            String relativePath = s3Object.key()
×
347
                    .replace(objectName + "/", "")
×
348
                    .replace("/", File.separator);
×
349
            getFile(bucketName, s3Object.key(), localFilePath + relativePath);
×
350
        }
×
351
    }
×
352

353
    // ----------------------------------------------------------------
354
    // 预览 / 下载(HTTP 响应)
355
    // ----------------------------------------------------------------
356

357
    public void previewObject(HttpServletRequest request, HttpServletResponse response,
358
                              String objectName) throws IOException {
359
        previewObject(request, response, objectName, false);
×
360
    }
×
361

362
    /**
363
     * 预览或下载文件,支持 Range 分段请求。
364
     * <p>
365
     * 改进:
366
     * - 编码统一使用 StandardCharsets,去掉过时字符串形式。
367
     * - 404 处理提取为 writeNotFound(),消除重复代码。
368
     * - Range 解析提取为 parseRange(),主流程更清晰。
369
     * - 变量命名修正(原 "String Disposition" 首字母大写违反规范)。
370
     */
371
    public void previewObject(HttpServletRequest request, HttpServletResponse response,
372
                              String objectName, boolean isDownload) throws IOException {
373
        if (Util.isBlank(objectName)) {
×
374
            return;
×
375
        }
376

377
        if (objectName.contains("%")) {
×
378
            objectName = URLDecoder.decode(objectName, StandardCharsets.UTF_8);
×
379
        }
380

381
        try {
382
            String fileName = Util.getFilename(objectName);
×
383
            // 改进:使用 StandardCharsets 重载(原代码使用已废弃的字符串形式)
384
            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
×
385
                    .replace("+", "%20");
×
386

387
            ObjectInfo objectInfo = getObjectInfo(objectName);
×
388
            if (objectInfo == null) {
×
389
                writeNotFound(response);
×
390
                return;
×
391
            }
392

393
            long fileSize = objectInfo.getSize();
×
394
            response.setContentType(Util.getContentType(objectName));
×
395
            // 修复命名:原代码 "String Disposition" 首字母大写
396
            String disposition = isDownload ? "attachment" : "inline";
×
397
            response.setHeader("Content-Disposition",
×
398
                    disposition + "; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
399
            response.setHeader("Accept-Ranges", "bytes");
×
400

401
            if ("HEAD".equals(request.getMethod())) {
×
402
                response.setContentLengthLong(fileSize);
×
403
                return;
×
404
            }
405

406
            String rangeHeader = request.getHeader("Range");
×
407
            if (rangeHeader == null) {
×
408
                serveFullContent(response, objectName, fileSize);
×
409
            } else {
410
                serveRangeContent(response, objectName, fileSize, rangeHeader);
×
411
            }
412

413
        } catch (NoSuchKeyException e) {
×
414
            writeNotFound(response);
×
415
        } catch (IOException e) {
×
416
            if (!"Broken pipe".equals(e.getMessage())) {
×
417
                throw e;
×
418
            }
419
            // Broken pipe:客户端主动断开,忽略
420
        }
×
421
    }
×
422

423
    // ----------------------------------------------------------------
424
    // 树形结构构建(私有)
425
    // ----------------------------------------------------------------
426

427
    public ObjectTreeNode getTreeList(String path) {
428
        return getTreeList(ossProperties.getBucketName(), path);
1✔
429
    }
430

431
    public ObjectTreeNode getTreeList(String bucketName, String path) {
432
        return buildTree(listObject(bucketName, path), path);
1✔
433
    }
434

435
    public ObjectTreeNode getTreeListByName(String path, String keyword) {
436
        return getTreeListByName(ossProperties.getBucketName(), path, keyword);
×
437
    }
438

439
    public ObjectTreeNode getTreeListByName(String bucketName, String path, String keyword) {
440
        return buildTree(listObject(bucketName, path, keyword), path);
×
441
    }
442

443
    private ObjectTreeNode buildTree(List<S3Object> objects, String objectName) {
444
        String rootName = extractRootName(objectName);
1✔
445
        ObjectTreeNode root = new ObjectTreeNode(rootName, objectName,
1✔
446
                getDomain() + objectName, null, "folder", 0, null);
1✔
447
        for (S3Object obj : objects) {
1✔
448
            String remaining = obj.key().startsWith(objectName + "/")
1✔
449
                    ? obj.key().substring(objectName.length() + 1)
1✔
450
                    : obj.key();
1✔
451
            addNode(root, remaining, obj);
1✔
452
        }
1✔
453
        return root;
1✔
454
    }
455

456
    private ObjectTreeNode buildFolderTree(List<S3Object> objects, String objectName) {
457
        String rootName = extractRootName(objectName);
×
458
        ObjectTreeNode root = new ObjectTreeNode(rootName, objectName,
×
459
                getDomain() + objectName, null, "folder", 0, null);
×
460
        for (S3Object obj : objects) {
×
461
            String remaining = obj.key().startsWith(objectName + "/")
×
462
                    ? obj.key().substring(objectName.length() + 1)
×
463
                    : obj.key();
×
464
            addFolderNode(root, remaining);
×
465
        }
×
466
        return root;
×
467
    }
468

469
    /**
470
     * 改进:提取重复的 rootName 计算逻辑
471
     */
472
    private static String extractRootName(String objectName) {
473
        if (Util.isBlank(objectName)) {
1✔
474
            return "";
×
475
        }
476
        int i = objectName.lastIndexOf('/');
1✔
477
        return i > 0 ? objectName.substring(i + 1) : objectName;
1✔
478
    }
479

480
    private void addNode(ObjectTreeNode parent, String remaining, S3Object object) {
481
        if (Util.isBlank(remaining)) {
1✔
482
            return;
×
483
        }
484
        int slashIdx = remaining.indexOf('/');
1✔
485
        if (slashIdx == -1) {
1✔
486
            // 文件节点
487
            parent.addChild(new ObjectTreeNode(remaining, object.key(),
1✔
488
                    getDomain() + object.key(), Date.from(object.lastModified()),
1✔
489
                    "file", object.size(), Util.getExtension(object.key())));
1✔
490
        } else {
491
            // 文件夹节点:找或建
492
            String folderName = remaining.substring(0, slashIdx);
1✔
493
            String newRemaining = remaining.substring(slashIdx + 1);
1✔
494
            ObjectTreeNode folder = findOrCreateFolder(parent, folderName);
1✔
495
            addNode(folder, newRemaining, object);
1✔
496
        }
497
    }
1✔
498

499
    private void addFolderNode(ObjectTreeNode parent, String remaining) {
500
        int slashIdx = remaining.indexOf('/');
×
501
        if (slashIdx == -1) {
×
502
            return; // 文件,跳过
×
503
        }
504
        String folderName = remaining.substring(0, slashIdx);
×
505
        String newRemaining = remaining.substring(slashIdx + 1);
×
506
        ObjectTreeNode folder = findOrCreateFolder(parent, folderName);
×
507
        addFolderNode(folder, newRemaining);
×
508
    }
×
509

510
    /**
511
     * 查找已存在的子文件夹节点;不存在则创建并挂载。
512
     * 改进:将 findFolderNode + 创建 + addChild 三步合并为一个方法,消除重复。
513
     */
514
    private ObjectTreeNode findOrCreateFolder(ObjectTreeNode parent, String folderName) {
515
        if (parent.getChildren() != null) {
1✔
516
            for (ObjectTreeNode child : parent.getChildren()) {
1✔
517
                if ("folder".equals(child.getType()) && folderName.equals(child.getName())) {
1✔
518
                    return child;
×
519
                }
520
            }
1✔
521
        }
522
        String uri = Util.isBlank(parent.getUri())
1✔
523
                ? folderName
×
524
                : parent.getUri() + "/" + folderName;
1✔
525
        ObjectTreeNode folder = new ObjectTreeNode(folderName, uri,
1✔
526
                getDomain() + uri, null, "folder", 0, null);
1✔
527
        parent.addChild(folder);
1✔
528
        return folder;
1✔
529
    }
530

531
    // ----------------------------------------------------------------
532
    // 私有工具方法
533
    // ----------------------------------------------------------------
534

535
    private GetObjectRequest buildGetRequest(String bucketName, String objectName) {
536
        return GetObjectRequest.builder()
1✔
537
                .bucket(bucketName).key(Util.formatPath(objectName)).build();
1✔
538
    }
539

540
    private static InputStream toInputStream(ByteBuffer buffer) {
541
        byte[] bytes = new byte[buffer.remaining()];
×
542
        buffer.get(bytes);
×
543
        return new ByteArrayInputStream(bytes);
×
544
    }
545

546
    /**
547
     * 向响应写 404 页面。原代码中此块出现两次,提取为方法消除重复。
548
     */
549
    private static void writeNotFound(HttpServletResponse response) throws IOException {
550
        response.setStatus(HttpServletResponse.SC_NOT_FOUND);
×
551
        response.setHeader("content-type", "text/html;charset=utf-8");
×
552
        response.getWriter().println(
×
553
                "<html><head><title>404 Not Found</title></head>" +
554
                        "<body><h1>404 Not Found</h1></body></html>");
555
    }
×
556

557
    /**
558
     * 全量内容输出
559
     */
560
    private void serveFullContent(HttpServletResponse response,
561
                                  String objectName, long fileSize) throws IOException {
562
        response.setContentLengthLong(fileSize);
×
563
        try (InputStream in = getInputStream(objectName);
×
564
             OutputStream out = response.getOutputStream()) {
×
565
            pipe(in, out, fileSize);
×
566
        }
567
    }
×
568

569
    /**
570
     * Range 分段内容输出。
571
     * 改进:Range 解析提取为 parseRange(),主流程只关注传输逻辑。
572
     */
573
    private void serveRangeContent(HttpServletResponse response, String objectName,
574
                                   long fileSize, String rangeHeader) throws IOException {
575
        long[] range = parseRange(rangeHeader, fileSize);
×
576
        long start = range[0], end = range[1];
×
577
        long contentLength = end - start + 1;
×
578

579
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
×
580
        response.setHeader("Content-Length", String.valueOf(contentLength));
×
581
        response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
×
582

583
        try (InputStream in = getInputStream(ossProperties.getBucketName(), objectName, rangeHeader);
×
584
             OutputStream out = response.getOutputStream()) {
×
585
            pipe(in, out, contentLength);
×
586
        }
587
    }
×
588

589
    /**
590
     * 解析 Range 请求头,返回 [start, end]。
591
     * 改进:从 previewObject() 主流程中提取,提高可读性和可测试性。
592
     */
593
    private static long[] parseRange(String rangeHeader, long fileSize) {
594
        // rangeHeader 格式: "bytes=0-1023"
595
        String rangeValue = rangeHeader.split("=")[1];
×
596
        String[] parts = rangeValue.split("-");
×
597
        long start = Long.parseLong(parts[0]);
×
598
        long end = parts.length > 1 && !parts[1].isEmpty()
×
599
                ? Long.parseLong(parts[1])
×
600
                : fileSize - 1;
×
601
        return new long[]{start, end};
×
602
    }
603

604
    /**
605
     * 流拷贝,最多写 maxBytes 字节
606
     */
607
    private static void pipe(InputStream in, OutputStream out, long maxBytes) throws IOException {
608
        byte[] buffer = new byte[BUFFER_SIZE];
×
609
        long remaining = maxBytes;
×
610
        int read;
611
        while (remaining > 0 && (read = in.read(buffer, 0, (int) Math.min(buffer.length, remaining))) != -1) {
×
612
            out.write(buffer, 0, read);
×
613
            remaining -= read;
×
614
        }
615
    }
×
616
}
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