文件分片下载

1、将文件进行分片,每片10M,以临时文件的方式保存,全部下载完毕之后合并再删除临时文件

2、用多线程下载

3、支持断点续传

4、文件名扩展,如第一次下载test.txt,下一次再下载这个文件,保存的文件名为test(1).txt

5、分片下载完毕之后,先对分片文件进行排序再合并,以免合并写入的时候顺序错误导致文件错误

6、合并之后再对比md5经行校验

7、传参只需要url,文件名,以及此文件的md5

 

package cn.ctyuncdn.service;
 
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
 
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
 
@Slf4j
public class MultipartDownload {
 
    /**
     * 文件分片大小(10M)
     */
    private static final int BLOCK_SIZE = 10 * 1024 * 1024;
 
    /**
     * 临时文件后缀,需要根据实际情况修改
     */
    private static final String TEMP_FILE_SUFFIX = StrPool.DOT + "tmp";
 
    /**
     * 文件名称
     */
    private static String FILE_NAME;
 
    /**
     * 下载总目录路径
     */
    private static String DOWNLOAD_PATH;
 
    /**
     * 查询进度的时间间隔,单位为毫秒
     */
    private static final int PROGRESS_UPDATE_INTERVAL = 3000;
 
    /**
     * 线程数
     */
    private static final int THREAD_NUM = 10;
 
    /**
     * 分块下载文件
     *
     * @param url      url
     * @param filename 文件名称
     * @param md5      md5
     * @throws Exception 异常
     */
    public static void download(String url, String filename, String md5) throws Exception {
 
        // 文件名赋值
        FILE_NAME = filename;
        // 获取后缀名
        // String suffix = filename.substring(filename.lastIndexOf(StrPool.DOT) + 1);
        // 获取文件名(不包含后缀)
        String name = filename.substring(0, filename.lastIndexOf(StrPool.DOT));
 
        // 获取连接 得到完整文件的长度
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        long fileSize = connection.getContentLengthLong();
 
        // windows 跟 linux 层级分隔符
        String separator = File.separator;
        // 当前所在目录
        String currentPath = System.getProperty("user.dir");
        // 设置文件下载所在目录 files/xxx
        String filesDirectoryPath = currentPath + separator + "files" + separator;
 
        // 临时目录名 文件名+md5 如:temp/xxx_xxx
        String tempDirectoryName = name + StrPool.UNDERLINE + md5;
 
        // 临时目录路径 temp/文件名+md5/临时文件名 如:temp/0260e4ce2175f7632f543af764e7a3d8/xxx-temp.txt
        String tempDirectoryPath = filesDirectoryPath + "temp" + separator + tempDirectoryName + separator;
 
        // 完整文件保存目录
        DOWNLOAD_PATH = filesDirectoryPath + "download" + separator;
 
        // 将下载文件的大小和分片数量计算出来
        int splitNum = (int) Math.ceil((double) fileSize / BLOCK_SIZE);
        // log.info(">>>总分片数 :_{}", splitNum);
 
        // 先判断目录是否存在 文件保存目录跟临时文件保存目录
        ArrayList<String> directoryPaths = new ArrayList<>();
        directoryPaths.add(tempDirectoryPath);
        directoryPaths.add(filesDirectoryPath);
        directoryPaths.add(DOWNLOAD_PATH);
        for (String directoryPath : directoryPaths) {
            Path path = Paths.get(directoryPath);
            if (!Files.isDirectory(path)) {
                try {
                    // Path absolutePath = path.toAbsolutePath();
                    // // log.info(">>>需要创建文件夹的绝对路径:{}", absolutePath);
                    Files.createDirectories(path);
                    // log.info(">>>文件夹创建成功 ...");
                } catch (IOException e) {
                    e.printStackTrace();
                    return;
                }
            }
        }
 
        // 获取配置文件中的线程配置 启动线程下载每个分片
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
        for (int i = 0; i < splitNum; i++) {
            int splitIndex = i;
            String tempFileName = tempDirectoryPath + i + StrPool.UNDERLINE + name + TEMP_FILE_SUFFIX;
            // Path absolutePath = Paths.get(tempFileName).toAbsolutePath();
            // log.info(">>>临时文件的所在位置 :_{} ", absolutePath);
            File outFile = new File(tempFileName);
            executor.execute(() -> {
                try {
                    downloadSplit(url, splitIndex, outFile);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
 
        // 等待所有分片下载完毕
        executor.shutdown();
 
        // 下载总进度条
 
        while (!executor.isTerminated()) {
            try {
                Thread.sleep(PROGRESS_UPDATE_INTERVAL);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
            // 统计下载进度
            long totalDownloaded = 0;
            for (int i = 0; i < splitNum; i++) {
                File tempFile = new File(tempDirectoryPath + i + StrPool.UNDERLINE + name + TEMP_FILE_SUFFIX);
                totalDownloaded += tempFile.exists() ? tempFile.length() : 0;
            }
 
            // 已经下载的文件大小
            long downloaded = totalDownloaded;
            double progress = (double) downloaded / fileSize * 100;
            log.info("Downloaded: {}%", String.format("%.2f", progress));
        }
 
        /*用于设定超时时间及单位。
        当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。
        一般情况下会和shutdown方法组合使用。*/
        executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
 
 
        // 判断分片文件是否全部下载完成
        if (!isAllChunksDownloaded(tempDirectoryPath, splitNum)) {
            log.error("Not all chunks are downloaded yet!");
            return;
        }
 
        // 合并文件
        mergeChunks(tempDirectoryPath, md5);
 
        // 删除临时文件目录以及临时文件
        deleteTempDirectory(tempDirectoryPath);
 
 
        log.info("Download task completed ");
    }
 
    /**
     * 删除临时目录
     *
     * @param tempDirectoryPath 临时目录路径
     * @throws IOException ioexception
     */
    private static void deleteTempDirectory(String tempDirectoryPath) throws IOException {
 
        /*删除目录
        使用Java的文件IO API来遍历目标目录中的每个文件,
        其中使用了Files.walk(directory)方法来遍历目录下的所有文件,
        并且使用了File::delete方法来逐个删除目录下的文件。
        最终,通过sorted()方法来保证我们能够在删除文件之前先删除包含更多文件的目录。*/
 
        Path directory = Paths.get(tempDirectoryPath);
        // 检查目录是否存在
        if (!Files.isDirectory(directory)) {
            // log.info(">>>目录不存在 ...");
            return;
        }
        Files.walk(directory)
                .sorted(Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(File::delete);
    }
 
 
    /**
     * 分片下载
     *
     * @param url            url
     * @param splitIndex     第几分片
     * @param temporaryFiles 临时文件
     * @throws IOException ioexception
     */
    private static void downloadSplit(String url, int splitIndex, File temporaryFiles) throws IOException {
 
        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
        connection.setRequestMethod("GET");
        long startByte = (long) splitIndex * BLOCK_SIZE;
        long endByte = (long) (splitIndex + 1) * BLOCK_SIZE - 1;
 
        // 这里判断进行断点续传
        if (temporaryFiles.exists()) {
            // 获取此临时文件还缺少的的部分
            long downloadedBytes = temporaryFiles.length();
            startByte = startByte + downloadedBytes;
            connection.setRequestProperty("Range", "bytes=" + startByte + StrPool.DASHED + endByte);
        } else {
            // 文件不存在说明是第一次下载,不用续传
            connection.setRequestProperty("Range", "bytes=" + startByte + StrPool.DASHED + endByte);
        }
 
 
        /*log.info(">>>此临时文件的起始位置 :_{}", startByte);
        log.info(">>>此临时文件的结束位置 :_{}", endByte);*/
 
        InputStream in = connection.getInputStream();
        RandomAccessFile out = new RandomAccessFile(temporaryFiles, "rw");
        byte[] buffer = new byte[1024];
        int len;
 
        if (temporaryFiles.exists()) {
            // 从尾部继续写入
            out.seek(out.length());
        }
        // 开始写入
        // log.info(">>>开始写入到此临时文件 :_{}", temporaryFiles);
        while ((len = in.read(buffer)) != -1) {
            out.write(buffer, 0, len);
        }
        // 关闭流
        out.close();
        in.close();
 
        // 关闭此连接
        connection.disconnect();
    }
 
 
    /**
     * 将文件分片合并成一个完整的文件
     *
     * @param tempDirectoryPath 分片文件所在的目录
     * @param md5               md5
     * @throws IOException ioexception
     */
    public static void mergeChunks(String tempDirectoryPath, String md5) throws IOException {
        File chunksDir = new File(tempDirectoryPath);
        // 获取分片文件列表
        List<File> chunkFiles = Arrays.stream(Objects.requireNonNull(chunksDir.listFiles((dir, name) -> name.endsWith(".tmp"))))
                .collect(Collectors.toList());
        // 按文件名升序排序
        chunkFiles = chunkFiles.stream().sorted(Comparator.comparingInt(file -> Integer.parseInt(StrUtil.subBefore(file.getName(), StrPool.UNDERLINE, false))))
                .collect(Collectors.toList());
 
        // 文件输出路径
        Path filePath = Paths.get(DOWNLOAD_PATH, FILE_NAME);
        // 判断文件是否存在,如果存在就加数字编号
        int index = 0;
        while (Files.exists(filePath)) {
            index++;
            String newName = addNumberSuffix(FILE_NAME, index);
            filePath = Paths.get(DOWNLOAD_PATH, newName);
        }
 
        File mergedFile= filePath.toFile();
 
        FileOutputStream os = new FileOutputStream(mergedFile, true);
        byte[] buffer = new byte[1024];
        int len;
 
        for (File file : chunkFiles) {
            InputStream is = Files.newInputStream(file.toPath());
            /*log.info(">>>>开始合并文件 ...");
            log.info(">>>>start_merging_files ...");*/
            while ((len = is.read(buffer)) > 0) {
                os.write(buffer, 0, len);
            }
 
            // 输入流关闭
            is.close();
        }
        // 流关闭
        os.close();
        // 合并完成
        String fileMd5 = DigestUtil.md5Hex(mergedFile);
        log.info("Verifying file integrity ");
        if (Objects.equals(fileMd5, md5)) {
            // log.error(">>>> md5值匹配 文件完整");
            log.error("File validation succeeded ");
            Path absolutePath = filePath.toAbsolutePath();
            // log.info(">>>>完整文件保存路径 :_{}", absolutePath);
            log.info("The location where the file is saved : {}", absolutePath);
        } else {
            // log.error(">>>> md5值不匹配 文件损坏");
            log.error("File validation failed ......");
            // 提示出错重新下载 删除掉临时目录跟合并完成的文件
            deleteTempDirectory(tempDirectoryPath);
            FileUtil.del(filePath);
            // 提示。。。
        }
    }
 
    /**
     * 判断分片文件是否全部下载完成
     *
     * @param tempDirectoryPath 临时目录路径
     * @param totalChunks       总分片数
     * @return boolean
     */
    public static boolean isAllChunksDownloaded(String tempDirectoryPath, int totalChunks) {
        File chunksDir = new File(tempDirectoryPath);
        File[] files = chunksDir.listFiles(file -> file.getName().endsWith(".tmp"));
        return files != null && files.length == totalChunks;
    }
 
 
    /**
     * 添加后缀数量 
     * 如第一次下载了 test.txt,下一次再下载此文件,文件保存时文件名为test(1).txt,以此类推
     *
     * @param fileName 文件名称
     * @param index    指数
     * @return {@link String}
     */
    private static String addNumberSuffix(String fileName, int index) {
        // 如果文件名没有后缀,则在文件名后面添加编号
        // 如果有后缀,则在后缀前面添加编号
        int dotIndex = fileName.lastIndexOf(StrPool.DOT);
        StringBuilder sb = new StringBuilder(fileName.length() + 2);
        sb.append(dotIndex >= 0 ? fileName.substring(0, dotIndex) : fileName);
        sb.append("(").append(index).append(")");
        if (dotIndex >= 0) {
            sb.append(fileName.substring(dotIndex));
        }
        return sb.toString();
    }
}

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
文件分片下载是一种常见的下载方式,可以通过将大文件分成多个小文件下载,从而提高下载速度和稳定性。在 Vue 和 SpringBoot 中,可以通过以下步骤实现大文件分片下载: 1. 将大文件分成多个小文件,每个小文件大小为固定的值,比如1MB或2MB。 2. 在前端 Vue 中,使用 axios 发送请求,并设置请求头 Range,表示请求文件的某个片段。 3. 在后端 SpringBoot 中,接收前端请求,并根据请求头 Range,返回对应的文件片段。 4. 前端 Vue 接收到多个文件片段后,将它们合并成一个完整的文件。 5. 下载完成后,将多个小文件删除,以释放存储空间。 具体实现细节可以参考以下代码: 前端 Vue: ``` downloadFile() { const url = '/api/download'; const file_name = 'large_file.mp4'; const chunk_size = 2 * 1024 * 1024; // 2MB per chunk const total_size = 1024 * 1024 * 1024; // 1GB const total_chunks = Math.ceil(total_size / chunk_size); const headers = { 'Content-Type': 'application/json' }; axios.post(url, { file_name, chunk_size, total_size }, { headers }) .then(res => { const { data } = res; const blobs = data.map((chunk, index) => { return new Blob([chunk], { type: 'application/octet-stream' }); }); const blob = new Blob(blobs, { type: 'application/octet-stream' }); const object_url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = object_url; link.download = file_name; link.click(); window.URL.revokeObjectURL(object_url); }); } ``` 后端 SpringBoot: ``` @PostMapping("/download") public List<byte[]> downloadFile(@RequestBody Map<String, Object> params, HttpServletRequest request, HttpServletResponse response) throws IOException { String file_name = (String) params.get("file_name"); int chunk_size = (int) params.get("chunk_size"); int total_size = (int) params.get("total_size"); int total_chunks = (int) Math.ceil((double) total_size / chunk_size); String range_header = request.getHeader("Range"); if (range_header == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); return null; } String[] range = range_header.split("=")[1].split("-"); int start = Integer.parseInt(range[0]); int end = range.length > 1 ? Integer.parseInt(range[1]) : start + chunk_size - 1; if (end >= total_size) { end = total_size - 1; } response.setHeader("Content-Type", "application/octet-stream"); response.setHeader("Content-Length", String.valueOf(end - start + 1)); response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + total_size); response.setHeader("Accept-Ranges", "bytes"); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); RandomAccessFile file = new RandomAccessFile(file_name, "r"); file.seek(start); byte[] bytes = new byte[chunk_size]; List<byte[]> chunks = new ArrayList<>(); int read = 0; while (read < chunk_size && start + read <= end) { int n = file.read(bytes, read, chunk_size - read); if (n <= 0) { break; } read += n; } chunks.add(Arrays.copyOf(bytes, read)); file.close(); return chunks; } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值