Java实现批量下载多文件(夹)压缩包(zip)

4,962 阅读3分钟

设计思路

1.文件(夹)层级结构传递参数设计

public class DownloadFileParam {
    /**
    *文件(夹)名称
    **/
    private String fileName;
    /**
    *文件id,文件夹不传
    **/
    private String fileId;
    /**
    *是否为文件夹,0.否,1.是
    **/
    private Integer isFolder;
    /**
    * 子文件(夹)信息
    **/
    private List<DownloadFileParam> childs;
    /**
    * 文件(夹)在服务器上的绝对路径,前端无需传递
    * 在后台递归下载文件到服务器或者在服务器上创建
    * 文件夹时赋值。
    **/
    private String file;
}

想象一下,每个DownloadFileParam代表一颗资料目录树,如果是下载的多个文件夹,前端应传递List。

2.不同用户同名文件夹文件下载

存在一种场景A、B用户同时下载同名文件夹(或者同一文件夹下的不通资料)

​ A

/     ~~~~~\     ~~~~ \

A1     ~~~~ A2     ~~~~ A3

             ~~~~~~~~~~~~~\     ~~~~ \

         ~~~~~~~~~A2-1.log     ~~~~ A3-1.txt

A用户下载A-A1、A-A2-A2-1.txt;B用户下载A-A1、A-A2-A3-1.txt,怎么保证下载zip包的正确性?

我们可以对于每一次下载请求,生成随机的uuid作为根文件夹目录,再在根据文件夹下,创建用户A、B的文件夹和临时文件。

//伪代码实现
//创建虚拟文件夹
String mockFileName = IdGenerator.newShortId();
String tmpDir = System.getProperty("user.dir") + "/downloadfile/" + mockFileName;
FileUtil.mkdir(tmpDir);
try {
    //xxx
} finally {
    FileUtil.del(tmpDir);
}

3.在服务器创建文件夹和临时文件

结合2.中提到的临时文件夹,我们不难将前端传递的List与随机生成的根目录合并成一颗资料树,通过递归的方式创建文件夹和下载文件到服务器

//伪代码实现
private void downloadFileToServer(String tmpDir, DownloadFileParam downloadFileParam) throws Exception {
    List<DownloadFileParam> childs = downloadFileParam.getChilds();
    if (EmptyUtils.isNotEmpty(childs)) {
        for (int i = 0; i < childs.size(); i++) {
            DownloadFileParam param = childs.get(i);
            if (param.getIsFolder() == 1) {
                //如果是文件夹则创建文件夹
                
            } else {
                //否则下载文件到tmpDir
                
            }
            //递归下载文件到服务器
            downloadFileToServer(tmpDir, param);
        }
    }
}

4.压缩服务器文件(夹)并写入到输出流

Java提供了ZipOutputStream输出流结合hutool包下的ZipUtils方法,很容易的能够把压缩包流返回给前端下载

/**
 * 对文件或文件目录进行压缩
 *
 * @param zipOutputStream    生成的Zip到的目标流,不关闭此流
 * @param withSrcDir 是否包含被打包目录,只针对压缩目录有效。若为false,则只压缩目录下的文件或目录,为true则将本目录也压缩
 * @param filter     文件过滤器,通过实现此接口,自定义要过滤的文件(过滤掉哪些文件或文件夹不加入压缩)
 * @param srcFiles   要压缩的源文件或目录。如果压缩一个文件,则为该文件的全路径;如果压缩一个目录,则为该目录的顶层目录路径
 * @throws IORuntimeException IO异常
 * @since 5.1.1
 */
public static void zip(ZipOutputStream zipOutputStream, boolean withSrcDir, FileFilter filter, File... srcFiles)

完整代码:

DownloadFileParam.java

public class DownloadFileParam {
    /**
    *文件(夹)名称
    **/
    private String fileName;
    /**
    *文件id,文件夹不传
    **/
    private String fileId;
    /**
    *是否为文件夹,0.否,1.是
    **/
    private Integer isFolder;
    /**
    * 子文件(夹)信息
    **/
    private List<DownloadFileParam> childs;
    /**
    * 文件(夹)在服务器上的绝对路径,前端无需传递
    * 在后台递归下载文件到服务器或者在服务器上创建
    * 文件夹时赋值。
    **/
    private String file;
}

接口定义

@PostMapping(value = "/batchDownloadFile", produces = "application/octet-stream;charset=UTF-8")
public void batchDownloadFile(@RequestBody List<DownloadFileParam> params) throws Exception {
    try {
        fileService.batchDownloadFile(params, getRequest(), getResponse());
    } catch (Exception e) {
        logger.error("downloadFileBy error params={}", params, e);
        throw e;
    }
}

fileService.batchDownloadFile

@Override
public void batchDownloadFile(List<DownloadFileParam> params, HttpServletRequest request, HttpServletResponse response) throws Exception {
    //创建虚拟文件夹
    String mockFileName = IdGenerator.newShortId();
    String tmpDir = System.getProperty("user.dir") + "/downloadfile/" + mockFileName;
    FileUtil.mkdir(tmpDir);
    try {
        //设置响应
        response.reset();
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(DateFormatUtil.formatDate(DateFormatUtil.yyyyMMdd, new Date()) + ".zip", "UTF-8"));
        //参数组装
        ZipOutputStream zos = new ZipOutputStream(response.getOutputStream());
        DownloadFileParam downloadFileParam = new DownloadFileParam();
        downloadFileParam.setFileName(mockFileName);
        downloadFileParam.setIsFolder(1);
        downloadFileParam.setChilds(params);
        //在服务器上生成指定文件
        downloadFileToServer(tmpDir, downloadFileParam);
        //压缩并写到输出流
        ZipUtil.zip(zos, false, pathname -> true, new File(tmpDir));
    } finally {
        FileUtil.del(tmpDir);
    }
}

downloadFileToServer

/**
 * 递归创建文件夹或者下载文件到服务器
 *
 * @param tmpDir
 * @param downloadFileParam
 * @throws Exception
 * @author ZhouNing
 * @date 2021/8/6 10:57
 **/
private void downloadFileToServer(String tmpDir, DownloadFileParam downloadFileParam) throws Exception {
    List<DownloadFileParam> childs = downloadFileParam.getChilds();
    if (EmptyUtils.isNotEmpty(childs)) {
        final String finalPath = tmpDir;
        //设置文件或者文件夹的绝对路径层级
        childs.stream().forEach(dwp -> dwp.setFile(finalPath + File.separator + dwp.getFileName()));
        for (int i = 0; i < childs.size(); i++) {
            DownloadFileParam param = childs.get(i);
            if (param.getIsFolder() == 1) {
                //如果是文件夹则创建文件夹
                tmpDir = tmpDir + File.separator + param.getFileName();
                FileUtil.mkdir(param.getFile());
            } else {
                //否则下载文件到tmpDir
                File tmpFile = new File(param.getFile());
                // 创建基于文件的输出流
                FileOutputStream fos = new FileOutputStream(tmpFile);
                //从mongodb或者下载文件到本地
                FileInfo fileInfo = fileInfoDao.findById(param.getFileId()).orElseThrow(() -> new Exception("文件不存在"));
                List<GridFsResource> gridFSFileList = fileChunkDao.findAll(fileInfo.getFileMd5());
                if (gridFSFileList != null && gridFSFileList.size() > 0) {
                    try {
                        for (GridFsResource gridFSFile : gridFSFileList) {
                            InputStream inputStream = gridFSFile.getInputStream();
                            try {
                                int len;
                                byte[] bytes = new byte[1024];
                                while ((len = inputStream.read(bytes)) != -1) {
                                    fos.write(bytes, 0, len);
                                }
                            } finally {
                                IoUtil.close(inputStream);
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        IoUtil.close(fos);
                    }
                }
            }
            //递归下载文件到服务器
            downloadFileToServer(tmpDir, param);
        }
    }
}

实现思路总结

文件(夹)的压缩下载,程序实现的复杂度和下载参数设计有很大关系。笔者曾经遇到过类似如下方式传递文件(夹)参数

/1111&/新建文件夹A(1)&/新建文件夹A/OA请假.docx$f43ea9e25a504e899404d9b026ea2cc9&/新建文件夹A/新建文件夹B/api-ms-win-core-console-l1-1-0.dll$ebd2b5ca5f0d447aa3c02caae16dff67&/新建文件夹(1)/1.py$3a51e7f6c30d4af89fc190cedd5711b3

这种实现也未尝不可,真正写起来代码可读性、可维护性堪忧。