暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

被迫成为奋斗b 2021-08-09
1337

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

设计思路

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<DownloadFileParam>

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<DownloadFileParam>与随机生成的根目录合并成一颗资料树,通过递归的方式创建文件夹和下载文件到服务器

//伪代码实现
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

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


文章转载自被迫成为奋斗b,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论