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

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

07 Apr 2026 07:52AM UTC coverage: 44.066% (+1.6%) from 42.506%
#6

push

github

panweimin
feat:demo功能拆分

557 of 1264 relevant lines covered (44.07%)

0.44 hits per line

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

55.67
/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() {
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()
1✔
123
                        .filter(e -> e.key().contains(keyword))
1✔
124
                        .forEach(list::add);
1✔
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);
1✔
202
    }
203

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

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

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

214
        ObjectTreeNode root = buildFolderTree(list, prefix);
1✔
215
        return root.getChildren() == null ? Collections.emptyList() : root.getChildren();
1✔
216
    }
217

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

436
    public ObjectTreeNode getTreeListByName(String path, String keyword) {
437
        return getTreeListByName(ossProperties.getBucketName(), path, keyword);
1✔
438
    }
439

440
    public ObjectTreeNode getTreeListByName(String bucketName, String path, String keyword) {
441
        return buildTree(listObject(bucketName, path, keyword), path);
1✔
442
    }
443

444
    private ObjectTreeNode buildTree(List<S3Object> objects, String objectName) {
445
        if (objects == null || objects.isEmpty()) {
1✔
446
            // 无命中时返回空,避免把查询路径误表达成真实存在的目录节点。
447
            return null;
1✔
448
        }
449
        String rootName = extractRootName(objectName);
1✔
450
        ObjectTreeNode root = new ObjectTreeNode(rootName, objectName,
1✔
451
                getDomain() + objectName, null, "folder", 0, null);
1✔
452
        for (S3Object obj : objects) {
1✔
453
            String remaining = obj.key().startsWith(objectName + "/")
1✔
454
                    ? obj.key().substring(objectName.length() + 1)
1✔
455
                    : obj.key();
1✔
456
            addNode(root, remaining, obj);
1✔
457
        }
1✔
458
        return root;
1✔
459
    }
460

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

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

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

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

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

536
    // ----------------------------------------------------------------
537
    // 私有工具方法
538
    // ----------------------------------------------------------------
539

540
    private GetObjectRequest buildGetRequest(String bucketName, String objectName) {
541
        return GetObjectRequest.builder()
1✔
542
                .bucket(bucketName).key(Util.formatPath(objectName)).build();
1✔
543
    }
544

545
    private static InputStream toInputStream(ByteBuffer buffer) {
546
        byte[] bytes = new byte[buffer.remaining()];
×
547
        buffer.get(bytes);
×
548
        return new ByteArrayInputStream(bytes);
×
549
    }
550

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

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

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

584
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
×
585
        response.setHeader("Content-Length", String.valueOf(contentLength));
×
586
        response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
×
587

588
        try (InputStream in = getInputStream(ossProperties.getBucketName(), objectName, rangeHeader);
×
589
             OutputStream out = response.getOutputStream()) {
×
590
            pipe(in, out, contentLength);
×
591
        }
592
    }
×
593

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

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