基于腾讯云COS 对大文件断点续传的实现

最近在做文件存储服务,使用腾讯云对象存储(COS),对于大文件的分块上传及断点续传问题,腾讯云API文档写的很粗糙,腾讯云推荐使用高级API 实现大文件上传,并且高级API上有断点续传API,经测试后根本不好用,断点续传API 必须和上传API 一起使用(实际谁会在上传的时候暂停一会,然后继续上传的),对于这个问题去问腾讯工程师那边回复也很模糊,一开始推荐我使用高级API,之后又告诉我高级API实现不了,再然后又让我使用高级API,直接把我搞晕了。

最后还是使用分块API 实现的,具体实现如下:

大文件上传

/**
     * 初始化分块上传
     *
     * @param bucketName 桶名
     * @param key        key
     * @return
     */
    private String initiateMultipartUpload(String bucketName, String key, String storageClass) {

        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
        // 设置存储类型:标准存储(Standard), 低频存储存储(Standard_IA),归档存储(ARCHIVE)。默认是标准(Standard)
        if (StringUtils.isBlank(storageClass)) {
            request.setStorageClass(StorageClass.Standard);
        } else {
            if (StorageClass.valueOf(storageClass) == StorageClass.Standard) {
                request.setStorageClass(StorageClass.Standard);
            } else {
                request.setStorageClass(StorageClass.Standard_IA);
            }
        }
        String uploadId = null;
        try {
            InitiateMultipartUploadResult initResult = cosClient.initiateMultipartUpload(request);
            // 获取uploadid
            uploadId = initResult.getUploadId();
        } catch (CosServiceException e) {
            logger.error("分块上传失败:", e);
            throw new CdpBaseBusinessException(CdpFileServerError.FILE_UPLOAD_FAILED);
        }

        return uploadId;
    }

 

public static String uploadPart(String path, File file) {

        String key = path + "/" + file.getName();

        // 初始化分块上传的请求,调用COS InitiateMultipartUploadRequest 方法
        String uploadId = initiateMultipartUpload(key);
        Long batch = null;
        try {
            // 计算文件总大小
            long totalSize = file.length();

            // 设置分块大小:1M
            byte data[] = new byte[1024 * 1024];
            int batchSize = data.length;

            // 计算分块数
            batch = totalSize / batchSize + (totalSize % batchSize > 0 ? 1 : 0);

            // 文件分块
            List<String> strings = new FileUtil().splitBySize(file.getPath(), batchSize);
            Thread.sleep(1000);

            for (int i = 0; i < batch; i++) {
                System.out.println("共" + batch + "块,正在进行第" + (i + 1) + "块");
                // 如果是最后一个分块,需要重新计算分块大小
                long partSize = batchSize;
                if (i == batch - 1) {
                    partSize = totalSize - i * batchSize;
                }
                // 分块上传
                batchUpload(uploadId, strings.get(i), partSize, i + 1, key, false);
            }

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        cosclient.shutdown();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("uploadId", uploadId);
        jsonObject.put("key", key);
        jsonObject.put("pieceSum", batch);
        return jsonObject.toString();
    }

 

/**
     * 拆分文件
     *
     * @param fileName 待拆分的完整文件名
     * @param byteSize 按多少字节大小拆分
     * @return 拆分后的文件名列表
     * @throws IOException
     */
    public List<String> splitBySize(String fileName, int byteSize)
            throws IOException {
        List<String> parts = new ArrayList<String>();
        File file = new File(fileName);
        int count = (int) Math.ceil(file.length() / (double) byteSize);
        int countLen = (count + "").length();
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(count,
                count * 3, 1, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(count * 2));

        for (int i = 0; i < count; i++) {
            String partFileName = file.getName() + "."
                    + leftPad((i + 1) + "", countLen, '0') + ".part";
            threadPool.execute(new SplitRunnable(byteSize, i * byteSize,
                    partFileName, file));
            parts.add(partFileName);
        }
        return parts;
    }



/**
     * 分割处理Runnable
     *
     * @author yjmyzz@126.com
     */
    private class SplitRunnable implements Runnable {
        int byteSize;
        String partFileName;
        File originFile;
        int startPos;

        public SplitRunnable(int byteSize, int startPos, String partFileName,
                             File originFile) {
            this.startPos = startPos;
            this.byteSize = byteSize;
            this.partFileName = partFileName;
            this.originFile = originFile;
        }

        public void run() {
            RandomAccessFile rFile;
            OutputStream os;
            try {
                rFile = new RandomAccessFile(originFile, "r");
                byte[] b = new byte[byteSize];
                rFile.seek(startPos);// 移动指针到每“段”开头
                int s = rFile.read(b);
                os = new FileOutputStream(partFileName);
                os.write(b, 0, s);
                os.flush();
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
/**
     * 分块上传
     */
    private static void batchUpload(String uploadId, String path, Long partSize,
                                    Integer partNumber, String key, Boolean isLastPart) {
        try {
            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setKey(key);
            uploadPartRequest.setUploadId(uploadId);

            // 设置分块的数据来源输入流
            File file = new File(path);
            uploadPartRequest.setInputStream(new FileInputStream(file));
            // 设置分块的长度
            uploadPartRequest.setPartSize(file.length()); // 设置数据长度
            uploadPartRequest.setPartNumber(partNumber);     // 假设要上传的part编号是10
            uploadPartRequest.setLastPart(isLastPart);

            UploadPartResult uploadPartResult = cosclient.uploadPart(uploadPartRequest);
            PartETag partETag = uploadPartResult.getPartETag();
            file.delete();
            System.out.println(partETag.getPartNumber());
            System.out.println(partETag.getETag());
        } catch (CosServiceException e) {
            e.printStackTrace();
        } catch (CosClientException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

重点就是文件拆分,一开始使用分块上传API时,往UploadPartRequest  中放的是整个文件的输入流,这会导致断点续传后的文件打不开,后询问腾讯云工程师后得知此处要放文件分块的输入流,所以就去找了个对文件拆分的工具类。

之后的断点续传就简单了,同样的对文件进行拆分然后上传就可以了

/**
     * 断点续传
     */
    public static String continueUpload(File file, String uploadId, String key) {
        initCOSClient();
        Long batch = null;
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            // 计算文件总大小
            long totalSize = file.length();

            // 设置每个分块的大小:1M
            byte data[] = new byte[1024 * 1024];
            int batchSize = data.length;

            // 计算分块数
            batch = totalSize / batchSize + (totalSize % batchSize > 0 ? 1 : 0);

            List<String> part = new FileUtil().splitBySize(file.getPath(), batchSize);
            Thread.sleep(1000);

            // 查询已上传的分块
            ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
            PartListing partListing = cosclient.listParts(listPartsRequest);

            // 将已上传的分块编号放到list 中
            List<Integer> completePiece = new ArrayList<>();
            for (PartSummary partSummary : partListing.getParts()) {
                completePiece.add(partSummary.getPartNumber());
            }
            // 遍历所有分块
            for (int i = 1; i <= batch; i++) {
                // 判断当前分块是否已经上传
                if (!completePiece.contains(i)) {
                    System.out.println("共" + batch + "块,正在进行第" + i + "块");

                    // 如果是最后一个分块,则重新计算分块大小
                    long partSize = batchSize;
                    if (i == batch) {
                        partSize = totalSize - (i - 1) * batchSize;
                        batchUpload(uploadId, part.get(i - 1), partSize, i, key, true);
                    } else {
                        batchUpload(uploadId, part.get(i - 1), partSize, i, key, false);
                    }
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        cosclient.shutdown();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("uploadId", uploadId);
        jsonObject.put("key", key);
        jsonObject.put("pieceSum", batch);
        return jsonObject.toString();
    }

总结:腾讯云的API 文档写的是真的不好,相对比阿里云的OOS 文档就清晰很多

分块上传困扰了我三天,最后终于是解决了,第一次写文章,写的不好请见谅;(上面代码为我测试时使用的代码,一些地方写的不规范,使用时请根据代码进行修改)

  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
现在大部分的网站使用的是标准HTML的上传方式来上传文件。一般情况下标准HTML方式在网页中只能上传4MB左右的文件,如果访问的用户比较多的时侯这种方式容易上传失败。虽然标准HTML上传方式开发起来比较简单,但是这种方式用户体验比较差,上传的文件大小受到限制,所以如果我们需要上传上百或者更大的上G的文件时,HTML标准上传方式是无法满足我们的需求的。 而另一方面,随着互联网行业的发展用户产生的新的需求也越来越多,同时对用户体验也提出了更高的要求,传统的HTML方式也越来越难已满足新的用户需求。现在大部分的用户有文件批量上传的需求,希望只通过点击一次鼠标就能够批量的上传多张图片,而不是一张张的选择文件上传,这样操作即浪费时间又非常烦琐。 近年来,由于数码和影视行业的迅猛发展刺激了用户对大文件的上传需求,现在越来越多的用户希望能够通过WEB的方式上传更大的文件,比如电影和图片。这些类型的文件通常都非常大,一般都在500MB以上,高清的影视文件至少在1G以上。这样的大文件是根本无法通过标准HTML方式来上传的。 不仅如此,由于国内网络环境比较特殊,有许多地区的网络不够稳定,在上传文件的过程中可能会发生断网的情况。如果用户正在上传一个1000MB的文件,已经上传了500MB,这时网络出现问题上传中止了。那么下一次用户需要要重新上传前面的500MB,而不是从500MB开始上传,这将浪费用户的许多时间。 新颖网络HTTP文件断点续传控件是专门用于解决HTTP大文件上传的需求而开发的产品。通过我们的HttpPartition模块用户能够非常方便的一次性选择超过200个的文件。而且我们升级了用户体验,用户现在不仅能够通过点击按钮来选择多个文件,还可以通过HttpDroper来拖拽文件甚至是文件夹。 现在我们能够轻松支持2G左右的大文件上传。为了减轻服务器的压力在HttpUploader模块中我们并不是一次上传2G的数据,而是将2G化分为小的数据块,每次向服务器上传约128KB左右的数据。同时在每次上传的数据中附带了文件大小,起始位置,文件MD5等信息。对于开发人员来说,有了这些信息,断点续传功能将会变的和普通的文件上传功能一样简单。 相信新颖网络HTTP断点续传控件能够帮助您赢利市场。 产品介绍:[url=http://www.cnblogs.com/xproer/archive/2012/02/17/2355440.html][/url]
可以使用腾讯云官方提供的Java SDK,具体步骤如下: 1. 在pom.xml文件中引入腾讯云cos-java-sdk-v5依赖: ```xml <dependency> <groupId>com.qcloud</groupId> <artifactId>cos_api</artifactId> <version>5.6.19</version> </dependency> ``` 2. 创建腾讯云cos的配置类: ```java @Configuration public class TencentCosConfig { @Value("${tencent.cos.secretId}") private String secretId; @Value("${tencent.cos.secretKey}") private String secretKey; @Value("${tencent.cos.region}") private String region; @Value("${tencent.cos.bucketName}") private String bucketName; @Bean public COSCredentials cosCredentials() { return new BasicCOSCredentials(secretId, secretKey); } @Bean public ClientConfig clientConfig() { ClientConfig clientConfig = new ClientConfig(); clientConfig.setRegion(new Region(region)); return clientConfig; } @Bean public COSClient cosClient() { return new COSClient(cosCredentials(), clientConfig()); } @Bean public String bucketName() { return bucketName; } } ``` 其中,secretId和secretKey是腾讯云提供的访问密钥,region是存储桶所在的地域,bucketName是存储桶的名称。可以在配置文件中配置这些变量,这里用@Value注解获取。 3. 在上传文件的Controller中注入cosClient和bucketName,实现文件上传方法: ```java @RestController public class FileController { @Autowired private COSClient cosClient; @Autowired private String bucketName; @PostMapping("/uploadFile") public String uploadFile(@RequestParam("file") MultipartFile file) throws Exception { ObjectMetadata objectMetadata = new ObjectMetadata(); objectMetadata.setContentLength(file.getSize()); objectMetadata.setContentType(file.getContentType()); String fileName = UUID.randomUUID().toString() + "." + FilenameUtils.getExtension(file.getOriginalFilename()); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, file.getInputStream(), objectMetadata); cosClient.putObject(putObjectRequest); return "https://" + bucketName + ".cos." + "region" + ".myqcloud.com/" + fileName; } } ``` 这里上传文件的方式为MultipartFile类型,使用Apache Commons IO工具类获取文件后缀名,并用UUID生成随机文件名。然后创建PutObjectRequest对象,调用cosClient的putObject方法上传文件,最后将文件URL返回给前端。 希望以上信息能对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值