java自定义zip压缩,支持排除指定文件

项目中需要用到zip压缩,但是zip4j不满足需求,所以写了一个自定义的zip压缩类。支持功能如下:

  • 指定一次性传入多个路径进行压缩,使用绝对路径
  • 指定压缩文件存储路径
  • 支持自定义压缩文件内部目录,例:压缩文件路径为 /opt/user/depdence/java/doc,zip 包的内部路径为 /depdence/java/doc,多 /depdence/java 两个层级,若不指定则默认压缩父目录。这是为了保留目录结构方便解压后直接覆盖。
  • 支持指定 excludes 排除指定目录/文件,支持 *? 通配符

引入包
commons-io: 作通配符的匹配
commons-lang3: 字符串和列表为空的判断,可不用

实现代码如下

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * zip 文件压缩
 * @author Chenfei
 * @date 2021/12/1
 * @see [相关类/方法](可选)
 * @since [产品/模块版本] (可选)
 */
public class ZipUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(ZipUtil.class);

    private static final int BUFFER_SIZE = 2 * 1024;

    /**
     * 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储
     */
    private final static ThreadLocal<String[]> EXCLUDES_LOCAL = ThreadLocal.withInitial(() -> new String[0]);

    public static final String FILE_SEPARATOR = ";";

    /**
     * 将指定文件或文件夹压缩成ZIP
     * @param sourceFile 压缩文件夹
     * @param parentSub  父级目录截断,即压缩文件保留指定路径之外的目录结构
     *                   例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
     *                   则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
     * @param out        压缩文件输出流
     * @param excludes   排除文件/文件夹
     * @throws RuntimeException 压缩失败抛出异常
     */
    public static void compress(File sourceFile, String parentSub, OutputStream out,
            String[] excludes) throws IOException {

        long start = System.currentTimeMillis();
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(out);
            // 处理截断父目录
            String parentDir = parentDir(sourceFile, parentSub);
            // 预处理排除路径
            excludePrepare(excludes);
            // 压缩文件
            compressFile(sourceFile, zos, parentDir);
            long end = System.currentTimeMillis();
            LOGGER.info("压缩完成 compress end, cost: " + (end - start));
        } catch (IOException e) {
            LOGGER.error("zip compress error!", e);
            throw new IOException("zip compress error!", e);
        } finally {
            IOUtils.closeQuietly(zos);
            // ThreadLocal 使用后清除,避免内存泄露
            EXCLUDES_LOCAL.remove();
        }
    }

    /**
     * 将指定的多个文件或文件夹压缩到指定路径ZIP包内,支出排除文件和路径,
     * 压缩和排除路径支持 ; 分割多个路径
     * @param sourceDir 待压缩文件夹,支持 ; 分割多个路径
     * @param parentSub 父级目录截断,即压缩文件保留指定路径之外的目录结构
     *                  例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
     *                  则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
     * @param outPath   压缩文件指定地址
     * @param excludes  排除文件/文件夹,支持 ; 分割多个路径
     * @throws IOException 异常抛出
     */
    public static void compress(String sourceDir, String parentSub, String outPath,
            String excludes) throws IOException {
        // 输出流
        FileOutputStream outStream = outStream(outPath);

        // 处理排除路径,支持以 ; 分割匹配
        String[] excludesArray = null;
        if (StringUtils.isNotEmpty(excludes)) {
            if (excludes.indexOf(FILE_SEPARATOR) > 0) {
                excludesArray = excludes.split(FILE_SEPARATOR);
            } else {
                excludesArray = new String[]{excludes};
            }
        }

        // 处理压缩文件路径,支持以 ; 分割将多个文件压缩至一个压缩包
        if (sourceDir.indexOf(FILE_SEPARATOR) > 0) {
            String[] sources = sourceDir.split(FILE_SEPARATOR);
            for (String src : sources) {
                if (StringUtils.isNotBlank(src)) {
                    File srcFile = new File(src);
                    compress(srcFile, parentSub, outStream, excludesArray);
                }
            }
        } else {
            File sourceFile = new File(sourceDir);
            compress(sourceFile, parentSub, outStream, excludesArray);
        }
    }

    /**
     * 将指定的多个文件或文件夹压缩到指定路径ZIP包内,支出排除文件和路径
     * @param sourceList  待压缩文件夹列表
     * @param parentSub   父级目录截断,即压缩文件保留指定路径之外的目录结构
     *                    例 srcDir=D:\opt\files\search\index, parentSub=D:\opt\files\
     *                    则压缩后,zip包内目录结构则为search/index,若不指定,则为 index
     * @param outPath     压缩文件指定地址
     * @param excludeList 排除文件/文件夹
     * @throws IOException 异常抛出
     */
    public static void compress(List<String> sourceList, String parentSub, String outPath,
            List<String> excludeList) throws IOException {
        // 输出流
        FileOutputStream outStream = outStream(outPath);

        // 排除文件处理
        String[] excludesArray = null;
        if (excludeList != null) {
            excludesArray = excludeList.toArray(new String[0]);
        }

        // 压缩文件处理
        for (String src : sourceList) {
            if (StringUtils.isNotBlank(src)) {
                File srcFile = new File(src);
                compress(srcFile, parentSub, outStream, excludesArray);
            }
        }
    }

    /**
     * 压缩文件父目录创建并创建输出流
     * @param outPath 压缩路径
     * @return 输出流
     * @throws FileNotFoundException 异常
     */
    private static FileOutputStream outStream(String outPath) throws FileNotFoundException {
        // 输出路径处理
        File outFile = new File(outPath);
        if (!outFile.getParentFile().exists() && !outFile.getParentFile().mkdirs()) {
            LOGGER.error("create dir failed. path: " + outFile.getParentFile().getAbsolutePath());
            throw new FileNotFoundException("create dir failed. path: " + outFile.getParentFile().getAbsolutePath());
        }
        return new FileOutputStream(new File(outPath));
    }

    /**
     * 处理截断父目录,判断截断父目录是否存在,并规范路径格式,以“/”或“\”分隔符结尾
     * @param sourceFile 压缩文件
     * @param parentSub  需截断目录
     * @return 截断后目录结构
     */
    private static String parentDir(File sourceFile, String parentSub) {
        // 处理截断父目录,判断截断父目录是否存在,并规范路径格式,以“/”或“\”分隔符结尾
        String parentDir = "";
        if (StringUtils.isNotEmpty(parentSub)) {
            String parentPath = sourceFile.getParent() + File.separator;
            File parentSubFile = new File(parentSub);
            if (parentSubFile.exists() && parentSubFile.isDirectory()) {
                String psub = parentSubFile.getAbsolutePath() + File.separator;
                if (parentPath.startsWith(psub)) {
                    parentDir = parentPath.substring(psub.length());
                }
            }
        }

        return parentDir;
    }

    /**
     * 对排除列表做预处理,转换路径,并存入 threadLocal
     * @param excludes 排除列表
     */
    private static void excludePrepare(String[] excludes) {

        if (ArrayUtils.isEmpty(excludes) || EXCLUDES_LOCAL.get().length > 0) {
            // 排除列表一次压缩只处理一次
            return;
        }

        // 处理排除路径列表,确认路径是否存在,并格式化路径,若是目录则以“/”或“\”分隔符结尾
        List<String> excludeList = new ArrayList<>();

        for (String exclude : excludes) {
            if (StringUtils.isBlank(exclude)) {
                continue;
            }

            if (exclude.indexOf('?') != -1 || exclude.indexOf('*') != -1) {
                excludeList.add(FilenameUtils.separatorsToSystem(exclude));
            } else {

                File exFile = new File(exclude);
                if (exFile.exists()) {
                    // 若是目录则以“/”或“\”分隔符结尾
//                String exPath = exFile.getAbsolutePath() + (exFile.isFile() ? "" : File.separator);
                    String exPath = exFile.getAbsolutePath();
                    excludeList.add(exPath);
                }
            }
        }

        // 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储,少传一个参数
        if (!excludeList.isEmpty()) {
            EXCLUDES_LOCAL.set(excludeList.toArray(new String[0]));
        }
    }

    /**
     * 递归压缩方法
     * @param sourceFile 源文件
     * @param zos        zip输出流
     * @param parentDir  父级目录路径,以 / 结尾
     * @throws IOException 压缩文件流异常
     */
    private static void compressFile(File sourceFile, ZipOutputStream zos, String parentDir) throws IOException {
        // 判断当前路径是否被排除
        if (excludePath(sourceFile.getAbsolutePath())) {
            // FIXME 如果文件被排除,则创建父目录的节点,否则文件夹内如果没有其他文件,会导致所有空的父目录节点不创建
            // 必须判断当前节点是否已创建才可以,但没有这个接口,直接创建如果匹配了多个文件则会导致节点重复报错,不想捕捉异常,不做了
//            zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir)));
//            zos.closeEntry();
            return;
        }

        byte[] buf = new byte[BUFFER_SIZE];
        if (sourceFile.isFile()) {
            // 向 zip 输出流中添加一个 zip 实体,构造器中 name 为 zip 实体的文件的路径加名字
            // 此处 FilenameUtils.separatorsToUnix 不用也可以,用了是为了风格名统一
            zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir) + sourceFile.getName()));
            // copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);
            while ((len = in.read(buf)) != -1) {
                zos.write(buf, 0, len);
            }
            // 压缩文件写入完成
            zos.closeEntry();
            IOUtils.closeQuietly(in);
        } else {
            File[] listFiles = sourceFile.listFiles();
            if (ArrayUtils.isEmpty(listFiles)) {
                // 保留空文件夹,只增加路径,不写入内容
                // // 此处 FilenameUtils.separatorsToUnix 不用也可以,但最后一定要以 / 结尾才是文件夹,\ 不行
                zos.putNextEntry(new ZipEntry(FilenameUtils.separatorsToUnix(parentDir) + sourceFile.getName() + "/"));
                zos.closeEntry();

            } else {
                // 循环处理子文件和子文件夹
                assert listFiles != null;
                for (File file : listFiles) {
                    // 传入父文件夹的目录结构,并在最后加一斜杠,
                    // 否则最后压缩包中无法保留原始文件结构,所有文件都压缩到包根目录下
                    compressFile(file, zos, parentDir + sourceFile.getName() + File.separator);
                }
            }
        }
    }

    /**
     * 判断路径是否排除
     * @param path 被判断路径
     * @return 排除 true,未排除 false
     */
    private static boolean excludePath(String path) {
        // 排除文件/文件夹,文件夹以 / 结尾, 使用 ThreadLocal 存储,少传一个参数
        String[] excludes = EXCLUDES_LOCAL.get();
        if (ArrayUtils.isEmpty(excludes)) {
            return false;
        }

        for (String exPath : excludes) {
            /* // 全匹配判断
            if (exPath.endsWith(File.separator)) {
                if (path.startsWith(exPath)) {
                    return true;
                }
            } else {
                if (path.equals(exPath)) {
                    return true;
                }
            }
            */
            // 支持通配符 *? 的判断
            if (FilenameUtils.wildcardMatch(path, exPath)) {
                return true;
            }
        }


        return false;
    }

    public static void main(String[] args) throws Exception {
        /** 测试 */
        String outPath = "E:\\zProfiler2\\xunjian\\210315.zip";
        ZipUtil.compress("E:\\zProfiler\\xunjian\\210315", "E:\\zProfiler\\xunjian\\210315\\", outPath,
                "E:/zProfiler/xunjian/210315/solrhome/**/conf/*.txt");

    }
}


  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值