【异步上传文件,压缩解压文件、文件夹】

异步上传文件、压缩解压文件、文件夹

项目链接

gitee项目链接:文件操作汇总

异步上传文件,压缩解压文件、文件夹

同步上传文件,当文件过大时,返回响应时间过长,超过20000ms页面会报错超时异常,此时需要异步上传。

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.test</groupId>
    <artifactId>file-operation</artifactId>
    <version>1.0-SNAPSHOT</version>


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--excel 依赖1-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.15</version>
        </dependency>

        <!--excel 依赖2-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.15</version>
        </dependency>

        <dependency>
            <groupId>fr.opensagres.xdocreport</groupId>
            <artifactId>fr.opensagres.poi.xwpf.converter.pdf-gae</artifactId>
            <version>2.0.1</version>
        </dependency>

        <!--pdf依赖1-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.13</version>
        </dependency>
        <!--pdf依赖2-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>

        <!--xml文件依赖1-->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!--xml文件依赖2-->
        <dependency>
            <groupId>jaxen</groupId>
            <artifactId>jaxen</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!--pdf转word依赖-->
        <dependency>
            <groupId>e-iceblue</groupId>
            <artifactId>spire.pdf.free</artifactId>
            <version>5.1.0</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.3</version>
        </dependency>

    </dependencies>

    <repositories>
        <repository>
            <id>com.e-iceblue</id>
            <url>https://repo.e-iceblue.cn/repository/maven-public/</url>
        </repository>
    </repositories>


</project>

ctroller

@PostMapping("zipFile")
    @ApiOperation("压缩文件")
    public ResponseEntity zipFile(String sourceDir ,String zipFile){
        return fileService.zipFile(sourceDir,zipFile);
    }

    @PostMapping("unzipFile")
    @ApiOperation("解压文件")
    public ResponseEntity unzipFile(String zipFile ,String targetDir ){
        return fileService.unzipFile(zipFile,targetDir);
    }

    @PostMapping("zipFolder")
    @ApiOperation("压缩文件夹")
    public ResponseEntity zipFolder(String sourceDir ,String zipFile ,boolean containRoot){
        return fileService.zipFolder(sourceDir,zipFile,containRoot);
    }

    @PostMapping("unzipFolder")
    @ApiOperation("解压文件夹")
    public ResponseEntity unzipFolder(String zipFile ,String targetDir){
        return fileService.unzipFolder(zipFile,targetDir);
    }
@PostMapping("uploadByThread")
    public ResponseEntity uploadByThread(@RequestPart MultipartFile file){
        return fileService.uploadByThread(file);
    }

@PostMapping("zipFolderByThread")
    @ApiOperation("异步压缩文件夹")
    public ResponseEntity zipFolderByThread(String sourceDir ,String zipFile ,boolean containRoot){
        return fileService.zipFolderByThread(sourceDir,zipFile,containRoot);
    }

    @PostMapping("unzipFolderByThread")
    @ApiOperation("异步解压文件夹")
    public ResponseEntity unzipFolderByThread(String zipFile ,String targetDir){
        return fileService.unzipFolderByThread(zipFile,targetDir);
    }

工具类

package com.test.file.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

@Component
public class FileUtils {
    private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);

    // 允许的文件类型白名单
    private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>();

    // 文件类型与Content-Type映射
    private static final Map<String, String> EXTENSION_TO_CONTENT_TYPE = new HashMap<>();

    // 静态初始化块
    static {
        // 初始化允许的文件扩展名
        Collections.addAll(ALLOWED_EXTENSIONS,
                "jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "xls", "xlsx",
                "txt", "zip", "rar", "mp4", "avi", "mov"
        );

        // 初始化扩展名到Content-Type的映射
        EXTENSION_TO_CONTENT_TYPE.put("jpg", "image/jpeg");
        EXTENSION_TO_CONTENT_TYPE.put("jpeg", "image/jpeg");
        EXTENSION_TO_CONTENT_TYPE.put("png", "image/png");
        EXTENSION_TO_CONTENT_TYPE.put("gif", "image/gif");
        EXTENSION_TO_CONTENT_TYPE.put("pdf", "application/pdf");
        EXTENSION_TO_CONTENT_TYPE.put("doc", "application/msword");
        EXTENSION_TO_CONTENT_TYPE.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
        EXTENSION_TO_CONTENT_TYPE.put("xls", "application/vnd.ms-excel");
        EXTENSION_TO_CONTENT_TYPE.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        EXTENSION_TO_CONTENT_TYPE.put("txt", "text/plain");
        EXTENSION_TO_CONTENT_TYPE.put("zip", "application/zip");
        EXTENSION_TO_CONTENT_TYPE.put("rar", "application/x-rar-compressed");
        EXTENSION_TO_CONTENT_TYPE.put("mp4", "video/mp4");
        EXTENSION_TO_CONTENT_TYPE.put("avi", "video/x-msvideo");
        EXTENSION_TO_CONTENT_TYPE.put("mov", "video/quicktime");
    }

    /**
     * 获取文件扩展名
     * @param filename 文件名
     * @return 小写的文件扩展名(不带点)
     */
    public String getFileExtension(String filename) {
        if (filename == null || filename.trim().isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        // 处理包含路径的文件名
        String pureFilename = filename;
        int lastSeparator = Math.max(filename.lastIndexOf('/'), filename.lastIndexOf('\\'));
        if (lastSeparator != -1) {
            pureFilename = filename.substring(lastSeparator + 1);
        }

        // 获取扩展名
        int lastDotIndex = pureFilename.lastIndexOf('.');
        if (lastDotIndex == -1 || lastDotIndex == pureFilename.length() - 1) {
            return ""; // 没有扩展名或以点结尾
        }

        String extension = pureFilename.substring(lastDotIndex + 1).toLowerCase();

        // 验证扩展名安全性
        if (!isExtensionSafe(extension)) {
            logger.warn("检测到潜在的不安全文件扩展名: {}", extension);
            throw new SecurityException("不支持的文件类型: " + extension);
        }

        return extension;
    }

    /**
     * 生成安全的文件名
     * @param originalFile 原始文件
     * @return 生成的文件名
     */
    public String generateFileName(MultipartFile originalFile) {
        String originalFilename = originalFile.getOriginalFilename();
        String extension = getFileExtension(originalFilename);

        // 使用UUID作为基础,避免文件名冲突
        String baseName = UUID.randomUUID().toString().replace("-", "");

        // 如果有扩展名则添加
        if (!extension.isEmpty()) {
            return baseName + "." + extension;
        }

        // 如果没有扩展名,尝试从Content-Type推断
        String inferredExtension = inferExtensionFromContentType(originalFile.getContentType());
        if (!inferredExtension.isEmpty()) {
            return baseName + "." + inferredExtension;
        }

        return baseName;
    }

    /**
     * 生成带目录结构的文件名(避免单个目录文件过多)
     * @param originalFile 原始文件
     * @return 包含目录路径的文件名
     */
    public String generateFileNameWithPath(MultipartFile originalFile) {
        String filename = generateFileName(originalFile);

        // 使用前2个字符作为一级目录,3-4个字符作为二级目录
        // 例如: ab/cd/abcdef123456.jpg
        if (filename.length() >= 4) {
            String firstDir = filename.substring(0, 2);
            String secondDir = filename.substring(2, 4);
            return firstDir + "/" + secondDir + "/" + filename;
        }

        return filename;
    }

    /**
     * 验证文件扩展名是否安全
     * @param extension 文件扩展名
     * @return 是否安全
     */
    private boolean isExtensionSafe(String extension) {
        if (extension == null || extension.trim().isEmpty()) {
            return false;
        }

        // 检查是否在白名单中
        boolean isAllowed = ALLOWED_EXTENSIONS.contains(extension);

        // 额外的安全检查:防止路径遍历等攻击
        boolean isSafe = extension.matches("^[a-zA-Z0-9]{1,10}$");

        return isAllowed && isSafe;
    }

    /**
     * 从Content-Type推断文件扩展名
     * @param contentType Content-Type
     * @return 推断的扩展名
     */
    private String inferExtensionFromContentType(String contentType) {
        if (contentType == null) {
            return "";
        }

        // 反向查找扩展名
        for (Map.Entry<String, String> entry : EXTENSION_TO_CONTENT_TYPE.entrySet()) {
            if (entry.getValue().equalsIgnoreCase(contentType)) {
                return entry.getKey();
            }
        }

        // 使用HashMap初始化常见的Content-Type映射
        Map<String, String> commonMappings = new HashMap<>();
        commonMappings.put("image/jpeg", "jpg");
        commonMappings.put("image/png", "png");
        commonMappings.put("image/gif", "gif");
        commonMappings.put("application/pdf", "pdf");
        commonMappings.put("text/plain", "txt");
        commonMappings.put("application/msword", "doc");
        commonMappings.put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx");
        commonMappings.put("application/vnd.ms-excel", "xls");
        commonMappings.put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx");
        commonMappings.put("application/zip", "zip");
        commonMappings.put("video/mp4", "mp4");

        return commonMappings.getOrDefault(contentType.toLowerCase(), "");
    }

    /**
     * 验证文件类型(基于扩展名和Magic Number)
     * @param file 文件
     * @param expectedExtension 期望的扩展名
     * @return 是否验证通过
     */
    public boolean validateFileType(MultipartFile file, String expectedExtension) throws IOException {
        // 1. 检查扩展名
        String actualExtension = getFileExtension(file.getOriginalFilename());
        if (!actualExtension.equalsIgnoreCase(expectedExtension)) {
            return false;
        }

        // 2. 检查Magic Number(文件头)
        try (InputStream is = file.getInputStream()) {
            byte[] header = new byte[8];
            int bytesRead = is.read(header);

            if (bytesRead < 8) {
                return false; // 文件太小
            }

            return validateMagicNumber(header, expectedExtension);
        }
    }

    /**
     * 验证文件Magic Number
     */
    private boolean validateMagicNumber(byte[] header, String extension) {
        switch (extension.toLowerCase()) {
            case "jpg":
            case "jpeg":
                return header[0] == (byte) 0xFF && header[1] == (byte) 0xD8;
            case "png":
                return header[0] == (byte) 0x89 && header[1] == (byte) 0x50 &&
                        header[2] == (byte) 0x4E && header[3] == (byte) 0x47;
            case "gif":
                return new String(header, 0, 3).equals("GIF");
            case "pdf":
                return new String(header, 0, 4).equals("%PDF");
            default:
                return true; // 对于其他类型,暂时信任扩展名
        }
    }

    /**
     * 获取文件的Content-Type
     * @param filename 文件名
     * @return Content-Type
     */
    public String getContentType(String filename) {
        String extension = getFileExtension(filename);
        return EXTENSION_TO_CONTENT_TYPE.getOrDefault(extension, "application/octet-stream");
    }
}

文件夹压缩解压工具类

package com.test.file.utils;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 文件夹压缩解压工具类
 */
public class FolderZipUtility {
    // 常量定义
    private static final int BUFFER_SIZE = 8192;
    private static final long MAX_FILE_SIZE = 10 * 1024L * 1024 * 1024; // 10GB
    private static final long MAX_TOTAL_SIZE = 10L * 1024 * 1024 * 1024; // 100GB
    private static final int MAX_FILE_COUNT = 100000; // 最大文件数量

    /**
     * 私有构造函数,防止实例化
     */
    private FolderZipUtility() {
        throw new AssertionError("工具类不允许实例化");
    }

    /**
     * 压缩文件夹到ZIP文件
     *
     * @param folderPath 要压缩的文件夹路径
     * @param zipFilePath 目标ZIP文件路径
     * @param includeRootFolder 是否包含根文件夹本身
     * @throws IOException 当IO操作失败时抛出
     * @throws IllegalArgumentException 当参数无效时抛出
     */
    public static void compressFolder(String folderPath, String zipFilePath,
                                      boolean includeRootFolder) throws IOException {
        // 参数校验
        validateInputParameters(folderPath, zipFilePath);

        Path sourceFolder = Paths.get(folderPath).toAbsolutePath().normalize();
        Path targetZip = Paths.get(zipFilePath).toAbsolutePath().normalize();

        // 验证源文件夹
        validateSourceFolder(sourceFolder);

        // 创建目标目录
        createParentDirectories(targetZip);

        // 检查目标文件是否已存在
        if (Files.exists(targetZip)) {
            throw new FileAlreadyExistsException("目标ZIP文件已存在: " + zipFilePath);
        }

        long totalSize = 0;
        int fileCount = 0;

        try (ZipOutputStream zos = new ZipOutputStream(
                new BufferedOutputStream(Files.newOutputStream(targetZip)))) {

            // 设置压缩级别
            zos.setLevel(6);

            Path rootPath = includeRootFolder ? sourceFolder.getParent() : sourceFolder;

            // 收集要压缩的文件
            List<Path> filesToCompress = collectFiles(sourceFolder);
            fileCount = filesToCompress.size();

            if (fileCount > MAX_FILE_COUNT) {
                throw new IOException("文件数量超过限制: " + fileCount);
            }

            // 压缩每个文件
            for (Path file : filesToCompress) {
                totalSize += compressSingleFile(file, zos, rootPath, totalSize);
            }

        } catch (IOException e) {
            // 清理可能创建的不完整文件
            cleanupIncompleteFile(targetZip);
            throw new IOException("压缩文件夹失败: " + e.getMessage(), e);
        }

        System.out.println(String.format("压缩完成: 共压缩 %d 个文件, 总大小: %.2f MB",
                fileCount, totalSize / (1024.0 * 1024.0)));
    }

    /**
     * 解压ZIP文件到文件夹
     *
     * @param zipFilePath ZIP文件路径
     * @param targetFolderPath 目标文件夹路径
     * @throws IOException 当IO操作失败时抛出
     * @throws IllegalArgumentException 当参数无效时抛出
     */
    public static void decompressToFolder(String zipFilePath, String targetFolderPath)
            throws IOException {
        // 参数校验
        validateInputParameters(zipFilePath, targetFolderPath);

        Path zipFile = Paths.get(zipFilePath).toAbsolutePath().normalize();
        Path targetFolder = Paths.get(targetFolderPath).toAbsolutePath().normalize();

        // 验证ZIP文件
        validateZipFile(zipFile);

        // 创建/验证目标文件夹
        createTargetFolder(targetFolder);

        long totalExtractedSize = 0;
        int extractedFileCount = 0;

        try (ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(Files.newInputStream(zipFile)))) {

            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                // 安全检查
                validateZipEntry(entry, targetFolder);

                Path entryPath = targetFolder.resolve(entry.getName());

                if (entry.isDirectory()) {
                    // 创建目录
                    Files.createDirectories(entryPath);
                } else {
                    // 解压文件
                    long fileSize = extractFile(zis, entryPath, entry.getSize(), totalExtractedSize);
                    totalExtractedSize += fileSize;
                    extractedFileCount++;
                }

                zis.closeEntry();

                // 检查总大小限制
                if (totalExtractedSize > MAX_TOTAL_SIZE) {
                    throw new IOException("解压总大小超过限制: " + totalExtractedSize);
                }
            }

        } catch (IOException e) {
            // 清理不完整的解压
            cleanupIncompleteExtraction(targetFolder);
            throw new IOException("解压文件失败: " + e.getMessage(), e);
        }

        System.out.println(String.format("解压完成: 共解压 %d 个文件, 总大小: %.2f MB",
                extractedFileCount, totalExtractedSize / (1024.0 * 1024.0)));
    }

    /**
     * 收集文件夹中的所有文件
     */
    private static List<Path> collectFiles(Path folder) throws IOException {
        List<Path> fileList = new ArrayList<>();

        Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                if (attrs.isRegularFile()) {
                    fileList.add(file);
                }
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc)
                    throws IOException {
                System.err.println("访问文件失败: " + file + " - " + exc.getMessage());
                return FileVisitResult.CONTINUE;
            }
        });

        return fileList;
    }

    /**
     * 压缩单个文件
     */
    private static long compressSingleFile(Path file, ZipOutputStream zos, Path rootPath,
                                           long currentTotalSize) throws IOException {
        BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);

        // 检查文件大小
        if (attrs.size() > MAX_FILE_SIZE) {
            throw new IOException("文件大小超过限制: " + file + " (" + attrs.size() + " bytes)");
        }

        // 检查总大小
        if (currentTotalSize + attrs.size() > MAX_TOTAL_SIZE) {
            throw new IOException("压缩总大小超过限制");
        }

        // 构建相对路径
        Path relativePath = rootPath.relativize(file);
        String entryName = relativePath.toString().replace("\\", "/");

        ZipEntry zipEntry = new ZipEntry(entryName);
        zipEntry.setSize(attrs.size());
        zipEntry.setLastModifiedTime(attrs.lastModifiedTime());
        zipEntry.setCreationTime(attrs.creationTime());

        zos.putNextEntry(zipEntry);

        try {
            Files.copy(file, zos);
        } finally {
            zos.closeEntry();
        }

        return attrs.size();
    }

    /**
     * 提取文件
     */
    private static long extractFile(ZipInputStream zis, Path targetPath, long entrySize,
                                    long currentTotalSize) throws IOException {
        // 安全检查
        if (entrySize > MAX_FILE_SIZE) {
            throw new IOException("解压文件大小超过限制: " + entrySize);
        }

        if (currentTotalSize + entrySize > MAX_TOTAL_SIZE) {
            throw new IOException("解压总大小超过限制");
        }

        // 创建父目录
        createParentDirectories(targetPath);

        long bytesRead = 0;
        try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(targetPath,
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {

            byte[] buffer = new byte[BUFFER_SIZE];
            int chunkSize;

            while ((chunkSize = zis.read(buffer)) != -1) {
                fos.write(buffer, 0, chunkSize);
                bytesRead += chunkSize;

                // 实时检查大小限制
                if (bytesRead > MAX_FILE_SIZE) {
                    throw new IOException("实际解压文件大小超过限制");
                }
            }
        }

        return bytesRead;
    }

    /**
     * 验证输入参数
     */
    private static void validateInputParameters(String source, String target) {
        if (source == null || source.trim().isEmpty()) {
            throw new IllegalArgumentException("源路径不能为空");
        }

        if (target == null || target.trim().isEmpty()) {
            throw new IllegalArgumentException("目标路径不能为空");
        }

        // 防止路径遍历攻击
        if (containsPathTraversal(source) || containsPathTraversal(target)) {
            throw new IllegalArgumentException("路径包含非法字符");
        }
    }

    /**
     * 验证源文件夹
     */
    private static void validateSourceFolder(Path folder) throws IOException {
        if (!Files.exists(folder)) {
            throw new FileNotFoundException("源文件夹不存在: " + folder);
        }

        if (!Files.isDirectory(folder)) {
            throw new IllegalArgumentException("源路径不是文件夹: " + folder);
        }

        // 检查文件夹是否可读
        if (!Files.isReadable(folder)) {
            throw new IOException("源文件夹不可读: " + folder);
        }
    }

    /**
     * 验证ZIP文件
     */
    private static void validateZipFile(Path zipFile) throws IOException {
        if (!Files.exists(zipFile)) {
            throw new FileNotFoundException("ZIP文件不存在: " + zipFile);
        }

        if (!Files.isRegularFile(zipFile)) {
            throw new IllegalArgumentException("ZIP路径不是文件: " + zipFile);
        }

        // 检查文件大小
        long fileSize = Files.size(zipFile);
        if (fileSize == 0) {
            throw new IOException("ZIP文件为空");
        }

        if (fileSize > MAX_FILE_SIZE) {
            throw new IOException("ZIP文件大小超过限制: " + fileSize + " bytes");
        }

        // 简单验证ZIP文件格式
        try (RandomAccessFile raf = new RandomAccessFile(zipFile.toFile(), "r")) {
            int fileSignature = raf.readInt();
            if (fileSignature != 0x504B0304 && fileSignature != 0x504B0506 &&
                    fileSignature != 0x504B0708) {
                throw new IOException("文件不是有效的ZIP格式");
            }
        }
    }

    /**
     * 验证ZIP条目安全性
     */
    private static void validateZipEntry(ZipEntry entry, Path targetDir) throws IOException {
        String entryName = entry.getName();

        // 检查空条目
        if (entryName == null || entryName.trim().isEmpty()) {
            throw new IOException("ZIP条目名称为空");
        }

        // 防止路径遍历攻击
        if (containsPathTraversal(entryName)) {
            throw new IOException("ZIP条目包含非法路径: " + entryName);
        }

        // 检查绝对路径
        if (Paths.get(entryName).isAbsolute()) {
            throw new IOException("ZIP条目包含绝对路径: " + entryName);
        }

        Path resolvedPath = targetDir.resolve(entryName).normalize();

        // 检查是否在目标目录内
        if (!resolvedPath.startsWith(targetDir)) {
            throw new IOException("ZIP条目试图逃逸目标目录: " + entryName);
        }
    }

    /**
     * 检查路径遍历攻击
     */
    private static boolean containsPathTraversal(String path) {
        return path.contains("..") || path.contains("//") || path.contains("\\\\");
    }

    /**
     * 创建父目录
     */
    private static void createParentDirectories(Path path) throws IOException {
        Path parent = path.getParent();
        if (parent != null && !Files.exists(parent)) {
            Files.createDirectories(parent);
        }
    }

    /**
     * 创建目标文件夹
     */
    private static void createTargetFolder(Path targetDir) throws IOException {
        if (Files.exists(targetDir)) {
            if (!Files.isDirectory(targetDir)) {
                throw new IOException("目标路径已存在但不是文件夹: " + targetDir);
            }

            // 检查目标文件夹是否为空
            try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDir)) {
                if (stream.iterator().hasNext()) {
                    throw new IOException("目标文件夹不为空: " + targetDir);
                }
            }
        } else {
            Files.createDirectories(targetDir);
        }
    }

    /**
     * 清理不完整的压缩文件
     */
    private static void cleanupIncompleteFile(Path file) {
        try {
            if (Files.exists(file)) {
                Files.delete(file);
                System.out.println("已清理不完整的压缩文件: " + file);
            }
        } catch (IOException e) {
            System.err.println("清理不完整文件失败: " + e.getMessage());
        }
    }

    /**
     * 清理不完整的解压文件
     */
    private static void cleanupIncompleteExtraction(Path targetDir) {
        try {
            if (Files.exists(targetDir) && Files.isDirectory(targetDir)) {
                deleteDirectoryRecursively(targetDir);
                System.out.println("已清理不完整的解压文件夹: " + targetDir);
            }
        } catch (IOException e) {
            System.err.println("清理不完整解压文件失败: " + e.getMessage());
        }
    }

    /**
     * 递归删除目录
     */
    private static void deleteDirectoryRecursively(Path dir) throws IOException {
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                    throws IOException {
                if (exc != null) {
                    throw exc;
                }
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

文件压缩解压工具类

package com.test.file.utils;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 文件压缩解压工具类
 */
@Slf4j
public class ZipUtility {
    private static final int BUFFER_SIZE = 8192;
    private static final int MAX_FILE_SIZE = 1024 * 1024 * 1024; // 1GB

    /**
     * 私有构造函数,防止实例化
     */
    private ZipUtility() {
        throw new AssertionError("工具类不允许实例化");
    }

    /**
     * 压缩文件或目录
     *
     * @param sourcePath 源文件或目录路径
     * @param targetZipPath 目标ZIP文件路径
     * @throws IOException 当IO操作失败时抛出
     * @throws IllegalArgumentException 当参数无效时抛出
     */
    public static void compress(String sourcePath, String targetZipPath) throws IOException {
        // 参数校验
        validateInputParameters(sourcePath, targetZipPath);

        Path source = Paths.get(sourcePath);
        Path target = Paths.get(targetZipPath);

        // 检查源文件是否存在
        if (!Files.exists(source)) {
            throw new FileNotFoundException("源文件或目录不存在: " + sourcePath);
        }

        // 创建目标目录(如果不存在)
        createParentDirectories(target);

        try (ZipOutputStream zos = new ZipOutputStream(
                new BufferedOutputStream(Files.newOutputStream(target)))) {

            if (Files.isDirectory(source)) {
                // 压缩目录
                compressDirectory(source, zos, source);
            } else {
                // 压缩单个文件
                compressSingleFile(source, zos, source.getParent());
            }

        } catch (IOException e) {
            // 清理可能创建的不完整文件
            cleanupIncompleteFile(target);
            throw new IOException("压缩文件失败: " + e.getMessage(), e);
        }
    }

    /**
     * 解压ZIP文件
     *
     * @param zipFilePath ZIP文件路径
     * @param targetDirectory 目标目录路径
     * @throws IOException 当IO操作失败时抛出
     * @throws IllegalArgumentException 当参数无效时抛出
     */
    public static void decompress(String zipFilePath, String targetDirectory) throws IOException {
        // 参数校验
        validateInputParameters(zipFilePath, targetDirectory);

        Path zipFile = Paths.get(zipFilePath);
        Path targetDir = Paths.get(targetDirectory);

        // 检查ZIP文件是否存在且为文件
        if (!Files.exists(zipFile) || !Files.isRegularFile(zipFile)) {
            throw new FileNotFoundException("ZIP文件不存在或不是文件: " + zipFilePath);
        }

        // 检查文件大小
        long fileSize = Files.size(zipFile);
        if (fileSize > MAX_FILE_SIZE) {
            throw new IOException("文件大小超过限制: " + fileSize + " bytes");
        }

        // 创建目标目录
        createTargetDirectory(targetDir);

        try (ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(Files.newInputStream(zipFile)))) {

            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                // 安全检查:防止ZIP滑动攻击
                validateZipEntry(entry, targetDir);

                Path entryPath = targetDir.resolve(entry.getName());

                if (entry.isDirectory()) {
                    // 创建目录
                    Files.createDirectories(entryPath);
                } else {
                    // 创建父目录
                    createParentDirectories(entryPath);

                    // 解压文件
                    extractFile(zis, entryPath, entry.getSize());
                }

                zis.closeEntry();
            }

        } catch (IOException e) {
            cleanupIncompleteExtraction(targetDir);
            throw new IOException("解压文件失败: " + e.getMessage(), e);
        }
    }

    /**
     * 递归压缩目录
     */
    private static void compressDirectory(Path directory, ZipOutputStream zos, Path rootPath)
            throws IOException {

        Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                // 跳过符号链接等非常规文件
                if (!attrs.isRegularFile()) {
                    return FileVisitResult.CONTINUE;
                }

                // 构建相对路径
                Path relativePath = rootPath.relativize(file);
                ZipEntry zipEntry = new ZipEntry(relativePath.toString().replace("\\", "/"));

                // 设置文件属性
                zipEntry.setSize(attrs.size());
                zipEntry.setLastModifiedTime(attrs.lastModifiedTime());

                zos.putNextEntry(zipEntry);

                try {
                    Files.copy(file, zos);
                } finally {
                    zos.closeEntry();
                }

                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                    throws IOException {

                if (!dir.equals(rootPath)) {
                    Path relativePath = rootPath.relativize(dir);
                    ZipEntry zipEntry = new ZipEntry(
                            relativePath.toString().replace("\\", "/") + "/");
                    zos.putNextEntry(zipEntry);
                    zos.closeEntry();
                }

                return FileVisitResult.CONTINUE;
            }
        });
    }

    /**
     * 压缩单个文件
     */
    private static void compressSingleFile(Path file, ZipOutputStream zos, Path rootPath)
            throws IOException {

        BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);

        if (!attrs.isRegularFile()) {
            throw new IOException("不是常规文件: " + file);
        }

        Path relativePath = rootPath.relativize(file);
        ZipEntry zipEntry = new ZipEntry(relativePath.toString().replace("\\", "/"));

        zipEntry.setSize(attrs.size());
        zipEntry.setLastModifiedTime(attrs.lastModifiedTime());

        zos.putNextEntry(zipEntry);

        try {
            Files.copy(file, zos);
        } finally {
            zos.closeEntry();
        }
    }

    /**
     * 提取文件
     */
    private static void extractFile(ZipInputStream zis, Path targetPath, long entrySize)
            throws IOException {

        // 安全检查
        if (entrySize > MAX_FILE_SIZE) {
            throw new IOException("解压文件大小超过限制: " + entrySize);
        }

        try (OutputStream fos = new BufferedOutputStream(Files.newOutputStream(targetPath))) {
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;
            long totalBytesRead = 0;

            while ((bytesRead = zis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;

                // 防止ZIP炸弹
                if (totalBytesRead > MAX_FILE_SIZE) {
                    throw new IOException("解压文件大小超过限制,可能为ZIP炸弹攻击");
                }
            }
        }
    }

    /**
     * 验证输入参数
     */
    private static void validateInputParameters(String source, String target) {
        if (source == null || source.trim().isEmpty()) {
            throw new IllegalArgumentException("源路径不能为空");
        }

        if (target == null || target.trim().isEmpty()) {
            throw new IllegalArgumentException("目标路径不能为空");
        }

        // 防止路径遍历攻击
        if (source.contains("..") || target.contains("..")) {
            throw new IllegalArgumentException("路径包含非法字符");
        }
    }

    /**
     * 验证ZIP条目安全性
     */
    private static void validateZipEntry(ZipEntry entry, Path targetDir) throws IOException {
        String entryName = entry.getName();

        // 检查空条目
        if (entryName == null || entryName.trim().isEmpty()) {
            throw new IOException("ZIP条目名称为空");
        }

        // 防止路径遍历攻击
        if (entryName.contains("..")) {
            throw new IOException("ZIP条目包含非法路径: " + entryName);
        }

        Path resolvedPath = targetDir.resolve(entryName).normalize();

        // 检查是否在目标目录内
        if (!resolvedPath.startsWith(targetDir)) {
            throw new IOException("ZIP条目试图逃逸目标目录: " + entryName);
        }
    }

    /**
     * 创建父目录
     */
    private static void createParentDirectories(Path path) throws IOException {
        Path parent = path.getParent();
        if (parent != null) {
            Files.createDirectories(parent);
        }
    }

    /**
     * 创建目标目录
     */
    private static void createTargetDirectory(Path targetDir) throws IOException {
        if (Files.exists(targetDir)) {
            if (!Files.isDirectory(targetDir)) {
                throw new IOException("目标路径已存在但不是目录: " + targetDir);
            }
        } else {
            Files.createDirectories(targetDir);
        }
    }

    /**
     * 清理不完整的压缩文件
     */
    private static void cleanupIncompleteFile(Path file) {
        try {
            if (Files.exists(file)) {
                Files.delete(file);
            }
        } catch (IOException e) {
            // 记录日志但不抛出异常
            System.err.println("清理不完整文件失败: " + e.getMessage());
        }
    }

    /**
     * 清理不完整的解压文件
     */
    private static void cleanupIncompleteExtraction(Path targetDir) {
        try {
            if (Files.exists(targetDir)) {
                Files.walkFileTree(targetDir, new SimpleFileVisitor<Path>() {
                    @Override
                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                            throws IOException {
                        Files.delete(file);
                        return FileVisitResult.CONTINUE;
                    }

                    @Override
                    public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                            throws IOException {
                        if (exc == null) {
                            Files.delete(dir);
                            return FileVisitResult.CONTINUE;
                        } else {
                            throw exc;
                        }
                    }
                });
            }
        } catch (IOException e) {
            System.err.println("清理不完整解压文件失败: " + e.getMessage());
        }
    }
}

异步文件压缩解压工具类

package com.test.file.utils;

import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 异步文件压缩解压工具类
 */
public class AsyncZipProcessor {
    private static final org.slf4j.Logger LOGGER =
            org.slf4j.LoggerFactory.getLogger(AsyncZipProcessor.class);

    // 配置常量
    private static final int BUFFER_SIZE = 8192; // 8KB缓冲区
    private static final long MAX_FILE_SIZE = 1024L * 1024 * 1024 * 2; // 2GB
    private static final long MAX_TOTAL_SIZE = 1024L * 1024 * 1024 * 10; // 10GB
    private static final int MAX_FILE_COUNT = 50000;
    private static final int CORE_POOL_SIZE = 2;
    private static final int MAX_POOL_SIZE = 4;
    private static final int QUEUE_CAPACITY = 100;
    private static final long KEEP_ALIVE_TIME = 60L;

    // 线程池 - 使用有界队列避免内存溢出
    private final ThreadPoolExecutor executor;

    /**
     * 压缩任务状态
     */
    public enum TaskStatus {
        PENDING, PROCESSING, COMPLETED, FAILED, CANCELLED
    }

    /**
     * 任务结果
     */
    public static class TaskResult {
        private final String taskId;
        private final TaskStatus status;
        private final String message;
        private final String filePath;
        private final long processedBytes;
        private final int processedFiles;

        public TaskResult(String taskId, TaskStatus status, String message,
                          String filePath, long processedBytes, int processedFiles) {
            this.taskId = taskId;
            this.status = status;
            this.message = message;
            this.filePath = filePath;
            this.processedBytes = processedBytes;
            this.processedFiles = processedFiles;
        }

        // Getters
        public String getTaskId() { return taskId; }
        public TaskStatus getStatus() { return status; }
        public String getMessage() { return message; }
        public String getFilePath() { return filePath; }
        public long getProcessedBytes() { return processedBytes; }
        public int getProcessedFiles() { return processedFiles; }
    }

    /**
     * 进度回调接口
     */
    public interface ProgressCallback {
        void onProgress(String taskId, String currentFile, long processedBytes,
                        long totalBytes, int processedFiles, int totalFiles);
        void onComplete(TaskResult result);
        void onError(String taskId, String errorMessage);
    }

    /**
     * 默认构造函数
     */
    public AsyncZipProcessor() {
        this.executor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(QUEUE_CAPACITY),
                new ThreadFactory() {
                    private final AtomicInteger counter = new AtomicInteger(1);

                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r,
                                "AsyncZipProcessor-" + counter.getAndIncrement());
                        thread.setDaemon(true);
                        return thread;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy() // 饱和策略
        );
    }

    /**
     * 自定义构造函数
     *
     * @param corePoolSize 核心线程数
     * @param maxPoolSize 最大线程数
     * @param queueCapacity 队列容量
     */
    public AsyncZipProcessor(int corePoolSize, int maxPoolSize, int queueCapacity) {
        this.executor = new ThreadPoolExecutor(
                corePoolSize,
                maxPoolSize,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(queueCapacity),
                new ThreadFactory() {
                    private final AtomicInteger counter = new AtomicInteger(1);

                    @Override
                    public Thread newThread(Runnable r) {
                        Thread thread = new Thread(r,
                                "AsyncZipProcessor-" + counter.getAndIncrement());
                        thread.setDaemon(true);
                        return thread;
                    }
                },
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }

    /**
     * 异步压缩文件夹
     *
     * @param taskId 任务ID
     * @param sourceFolder 源文件夹路径
     * @param targetZip 目标ZIP文件路径
     * @param includeRootFolder 是否包含根文件夹
     * @param callback 进度回调
     * @return CompletableFuture 异步任务结果
     * @throws IllegalArgumentException 参数非法时抛出
     */
    public CompletableFuture<TaskResult> compressFolderAsync(
            String taskId,
            String sourceFolder,
            String targetZip,
            boolean includeRootFolder,
            ProgressCallback callback) {

        // 参数校验
        validateInputParameters(sourceFolder, targetZip);
        if (taskId == null || taskId.trim().isEmpty()) {
            throw new IllegalArgumentException("任务ID不能为空");
        }

        LOGGER.info("开始异步压缩任务: taskId={}, source={}, target={}",
                taskId, sourceFolder, targetZip);

        return CompletableFuture.supplyAsync(() -> {
            long totalProcessedBytes = 0;
            int processedFileCount = 0;
            int totalFileCount = 0;

            try {
                Path sourcePath = Paths.get(sourceFolder).toAbsolutePath().normalize();
                Path targetPath = Paths.get(targetZip).toAbsolutePath().normalize();

                // 验证源文件夹
                validateSourceFolder(sourcePath);

                // 创建目标目录
                createParentDirectories(targetPath);

                // 检查目标文件是否已存在
                if (Files.exists(targetPath)) {
                    throw new FileAlreadyExistsException("目标文件已存在: " + targetZip);
                }

                // 统计文件总数和总大小(用于进度计算)
                FileStats fileStats = calculateFileStats(sourcePath);
                totalFileCount = fileStats.getFileCount();
                long totalBytes = fileStats.getTotalSize();

                LOGGER.debug("任务{}: 共{}个文件, 总大小{}字节",
                        taskId, totalFileCount, totalBytes);

                // 执行压缩
                try (ZipOutputStream zos = new ZipOutputStream(
                        new BufferedOutputStream(Files.newOutputStream(targetPath)))) {

                    zos.setLevel(6); // 默认压缩级别

                    Path rootPath = includeRootFolder ? sourcePath.getParent() : sourcePath;
                    String rootFolderName = includeRootFolder ?
                            sourcePath.getFileName().toString() : "";

                    // 收集并处理文件
                    List<Path> files = collectFiles(sourcePath);
                    for (Path file : files) {
                        if (Thread.currentThread().isInterrupted()) {
                            throw new InterruptedException("任务被取消");
                        }

                        long fileSize = compressSingleFile(
                                file, zos, rootPath, rootFolderName, totalProcessedBytes);

                        totalProcessedBytes += fileSize;
                        processedFileCount++;

                        // 报告进度
                        if (callback != null) {
                            callback.onProgress(
                                    taskId,
                                    file.toString(),
                                    totalProcessedBytes,
                                    totalBytes,
                                    processedFileCount,
                                    totalFileCount
                            );
                        }

                        // 安全检查
                        if (totalProcessedBytes > MAX_TOTAL_SIZE) {
                            throw new IOException("压缩总大小超过限制");
                        }
                    }
                }

                TaskResult result = new TaskResult(
                        taskId, TaskStatus.COMPLETED, "压缩完成",
                        targetZip, totalProcessedBytes, processedFileCount
                );

                LOGGER.info("异步压缩任务完成: taskId={}, 处理文件{}个, 总大小{}字节",
                        taskId, processedFileCount, totalProcessedBytes);

                if (callback != null) {
                    callback.onComplete(result);
                }

                return result;

            } catch (Exception e) {
                LOGGER.error("异步压缩任务失败: taskId={}, error={}", taskId, e.getMessage(), e);

                // 清理不完整文件
                cleanupIncompleteFile(Paths.get(targetZip));

                TaskResult errorResult = new TaskResult(
                        taskId, TaskStatus.FAILED, e.getMessage(),
                        null, totalProcessedBytes, processedFileCount
                );

                if (callback != null) {
                    callback.onError(taskId, e.getMessage());
                }

                throw new CompletionException(e);
            }
        }, executor);
    }

    /**
     * 异步解压ZIP文件
     *
     * @param taskId 任务ID
     * @param zipFile ZIP文件路径
     * @param targetFolder 目标文件夹路径
     * @param callback 进度回调
     * @return CompletableFuture 异步任务结果
     * @throws IllegalArgumentException 参数非法时抛出
     */
    public CompletableFuture<TaskResult> decompressFileAsync(
            String taskId,
            String zipFile,
            String targetFolder,
            ProgressCallback callback) {

        return decompressFileAsync(taskId, zipFile, targetFolder, false, false, callback);
    }

    /**
     * 异步解压ZIP文件(高级选项)
     *
     * @param taskId 任务ID
     * @param zipFile ZIP文件路径
     * @param targetFolder 目标文件夹路径
     * @param skipRootFolder 是否跳过根文件夹
     * @param flattenStructure 是否平铺结构
     * @param callback 进度回调
     * @return CompletableFuture 异步任务结果
     * @throws IllegalArgumentException 参数非法时抛出
     */
    public CompletableFuture<TaskResult> decompressFileAsync(
            String taskId,
            String zipFile,
            String targetFolder,
            boolean skipRootFolder,
            boolean flattenStructure,
            ProgressCallback callback) {

        // 参数校验
        validateInputParameters(zipFile, targetFolder);
        if (taskId == null || taskId.trim().isEmpty()) {
            throw new IllegalArgumentException("任务ID不能为空");
        }

        LOGGER.info("开始异步解压任务: taskId={}, zipFile={}, target={}",
                taskId, zipFile, targetFolder);

        return CompletableFuture.supplyAsync(() -> {
            long totalExtractedBytes = 0;
            int extractedFileCount = 0;

            try {
                Path zipPath = Paths.get(zipFile).toAbsolutePath().normalize();
                Path targetPath = Paths.get(targetFolder).toAbsolutePath().normalize();

                // 验证ZIP文件
                validateZipFile(zipPath);

                // 创建目标文件夹
                createTargetFolder(targetPath);

                // 获取ZIP文件信息(用于进度计算)
                ZipFileInfo zipInfo = getZipFileInfo(zipPath);
                int totalFileCount = zipInfo.getFileCount();
                long totalBytes = zipInfo.getTotalSize();

                LOGGER.debug("任务{}: ZIP包含{}个文件, 总大小{}字节",
                        taskId, totalFileCount, totalBytes);

                // 分析根目录前缀(如果需要跳过根目录)
                String rootPrefix = null;
                if (skipRootFolder) {
                    rootPrefix = findRootPrefix(zipPath);
                }

                // 执行解压
                try (ZipInputStream zis = new ZipInputStream(
                        new BufferedInputStream(Files.newInputStream(zipPath)))) {

                    ZipEntry entry;
                    while ((entry = zis.getNextEntry()) != null) {
                        if (Thread.currentThread().isInterrupted()) {
                            throw new InterruptedException("任务被取消");
                        }

                        // 安全检查
                        validateZipEntry(entry, targetPath);

                        String entryName = entry.getName();

                        // 处理根目录跳过
                        if (skipRootFolder && rootPrefix != null &&
                                entryName.startsWith(rootPrefix)) {
                            entryName = entryName.substring(rootPrefix.length());
                            if (entryName.isEmpty()) {
                                zis.closeEntry();
                                continue;
                            }
                        }

                        Path entryPath;
                        if (flattenStructure && !entry.isDirectory()) {
                            // 平铺模式
                            String fileName = getSafeFileName(entryName, extractedFileCount);
                            entryPath = targetPath.resolve(fileName);
                        } else {
                            // 保持目录结构
                            entryPath = targetPath.resolve(entryName);
                        }

                        if (entry.isDirectory()) {
                            if (!flattenStructure) {
                                Files.createDirectories(entryPath);
                            }
                        } else {
                            long fileSize = extractFile(
                                    zis, entryPath, entry.getSize(), totalExtractedBytes);

                            totalExtractedBytes += fileSize;
                            extractedFileCount++;

                            // 报告进度
                            if (callback != null) {
                                callback.onProgress(
                                        taskId,
                                        entryName,
                                        totalExtractedBytes,
                                        totalBytes,
                                        extractedFileCount,
                                        totalFileCount
                                );
                            }

                            // 安全检查
                            if (totalExtractedBytes > MAX_TOTAL_SIZE) {
                                throw new IOException("解压总大小超过限制");
                            }
                        }

                        zis.closeEntry();
                    }
                }

                TaskResult result = new TaskResult(
                        taskId, TaskStatus.COMPLETED, "解压完成",
                        targetFolder, totalExtractedBytes, extractedFileCount
                );

                LOGGER.info("异步解压任务完成: taskId={}, 解压文件{}个, 总大小{}字节",
                        taskId, extractedFileCount, totalExtractedBytes);

                if (callback != null) {
                    callback.onComplete(result);
                }

                return result;

            } catch (Exception e) {
                LOGGER.error("异步解压任务失败: taskId={}, error={}", taskId, e.getMessage(), e);

                // 清理不完整解压
                cleanupIncompleteExtraction(Paths.get(targetFolder));

                TaskResult errorResult = new TaskResult(
                        taskId, TaskStatus.FAILED, e.getMessage(),
                        null, totalExtractedBytes, extractedFileCount
                );

                if (callback != null) {
                    callback.onError(taskId, e.getMessage());
                }

                throw new CompletionException(e);
            }
        }, executor);
    }

    /**
     * 取消指定任务
     *
     * @param future 要取消的Future任务
     */
    public void cancelTask(CompletableFuture<TaskResult> future) {
        if (future != null && !future.isDone()) {
            future.cancel(true);
            LOGGER.info("任务已取消");
        }
    }

    /**
     * 优雅关闭线程池
     */
    public void shutdown() {
        LOGGER.info("开始关闭AsyncZipProcessor线程池");
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
                if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                    LOGGER.error("线程池未能正常关闭");
                }
            }
        } catch (InterruptedException ie) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
        LOGGER.info("AsyncZipProcessor线程池已关闭");
    }

    /**
     * 立即关闭线程池
     */
    public void shutdownNow() {
        LOGGER.info("立即关闭AsyncZipProcessor线程池");
        executor.shutdownNow();
    }

    // =============== 私有辅助方法 ===============

    /**
     * 文件统计信息
     */
    private static class FileStats {
        private final int fileCount;
        private final long totalSize;

        public FileStats(int fileCount, long totalSize) {
            this.fileCount = fileCount;
            this.totalSize = totalSize;
        }

        public int getFileCount() { return fileCount; }
        public long getTotalSize() { return totalSize; }
    }

    /**
     * ZIP文件信息
     */
    private static class ZipFileInfo {
        private final int fileCount;
        private final long totalSize;

        public ZipFileInfo(int fileCount, long totalSize) {
            this.fileCount = fileCount;
            this.totalSize = totalSize;
        }

        public int getFileCount() { return fileCount; }
        public long getTotalSize() { return totalSize; }
    }

    /**
     * 计算文件夹统计信息
     */
    private FileStats calculateFileStats(Path folder) throws IOException {
        final AtomicInteger fileCount = new AtomicInteger(0);
        final AtomicLong totalSize = new AtomicLong(0);

        Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                if (attrs.isRegularFile()) {
                    fileCount.incrementAndGet();
                    totalSize.addAndGet(attrs.size());
                }
                return FileVisitResult.CONTINUE;
            }
        });

        return new FileStats(fileCount.get(), totalSize.get());
    }

    /**
     * 获取ZIP文件信息
     */
    private ZipFileInfo getZipFileInfo(Path zipFile) throws IOException {
        int fileCount = 0;
        long totalSize = 0;

        try (ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(Files.newInputStream(zipFile)))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    fileCount++;
                    totalSize += entry.getSize();
                }
                zis.closeEntry();
            }
        }

        return new ZipFileInfo(fileCount, totalSize);
    }

    /**
     * 收集文件列表
     */
    private List<Path> collectFiles(Path folder) throws IOException {
        List<Path> fileList = new ArrayList<>();

        Files.walkFileTree(folder, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                if (attrs.isRegularFile()) {
                    fileList.add(file);
                }
                return FileVisitResult.CONTINUE;
            }
        });

        return fileList;
    }

    /**
     * 压缩单个文件
     */
    private long compressSingleFile(Path file, ZipOutputStream zos, Path rootPath,
                                    String rootFolderName, long currentTotalSize) throws IOException {
        BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);

        // 安全检查
        if (attrs.size() > MAX_FILE_SIZE) {
            throw new IOException("文件大小超过限制: " + file);
        }
        if (currentTotalSize + attrs.size() > MAX_TOTAL_SIZE) {
            throw new IOException("压缩总大小超过限制");
        }

        // 构建相对路径
        Path relativePath = rootPath.relativize(file);
        String entryName = relativePath.toString().replace("\\", "/");

        // 添加根文件夹名称
        if (!rootFolderName.isEmpty() && !entryName.startsWith(rootFolderName)) {
            entryName = rootFolderName + "/" + entryName;
        }

        ZipEntry zipEntry = new ZipEntry(entryName);
        zipEntry.setSize(attrs.size());
        zipEntry.setLastModifiedTime(attrs.lastModifiedTime());

        zos.putNextEntry(zipEntry);

        try {
            Files.copy(file, zos);
        } finally {
            zos.closeEntry();
        }

        return attrs.size();
    }

    /**
     * 提取文件
     */
    private long extractFile(ZipInputStream zis, Path targetPath, long entrySize,
                             long currentTotalSize) throws IOException {
        // 安全检查
        if (entrySize > MAX_FILE_SIZE) {
            throw new IOException("解压文件大小超过限制: " + entrySize);
        }
        if (currentTotalSize + entrySize > MAX_TOTAL_SIZE) {
            throw new IOException("解压总大小超过限制");
        }

        createParentDirectories(targetPath);

        long bytesRead = 0;
        try (OutputStream fos = new BufferedOutputStream(
                Files.newOutputStream(targetPath,
                        StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {

            byte[] buffer = new byte[BUFFER_SIZE];
            int chunkSize;

            while ((chunkSize = zis.read(buffer)) != -1) {
                fos.write(buffer, 0, chunkSize);
                bytesRead += chunkSize;

                if (bytesRead > MAX_FILE_SIZE) {
                    throw new IOException("实际解压文件大小超过限制");
                }
            }
        }

        return bytesRead;
    }

    /**
     * 验证输入参数
     */
    private void validateInputParameters(String source, String target) {
        if (source == null || source.trim().isEmpty()) {
            throw new IllegalArgumentException("源路径不能为空");
        }
        if (target == null || target.trim().isEmpty()) {
            throw new IllegalArgumentException("目标路径不能为空");
        }
        if (containsPathTraversal(source) || containsPathTraversal(target)) {
            throw new IllegalArgumentException("路径包含非法字符");
        }
    }

    /**
     * 验证源文件夹
     */
    private void validateSourceFolder(Path folder) throws IOException {
        if (!Files.exists(folder)) {
            throw new FileNotFoundException("源文件夹不存在: " + folder);
        }
        if (!Files.isDirectory(folder)) {
            throw new IllegalArgumentException("源路径不是文件夹: " + folder);
        }
        if (!Files.isReadable(folder)) {
            throw new IOException("源文件夹不可读: " + folder);
        }
    }

    /**
     * 验证ZIP文件
     */
    private void validateZipFile(Path zipFile) throws IOException {
        if (!Files.exists(zipFile)) {
            throw new FileNotFoundException("ZIP文件不存在: " + zipFile);
        }
        if (!Files.isRegularFile(zipFile)) {
            throw new IllegalArgumentException("ZIP路径不是文件: " + zipFile);
        }

        long fileSize = Files.size(zipFile);
        if (fileSize == 0) {
            throw new IOException("ZIP文件为空");
        }
        if (fileSize > MAX_FILE_SIZE) {
            throw new IOException("ZIP文件大小超过限制: " + fileSize);
        }
    }

    /**
     * 验证ZIP条目安全性
     */
    private void validateZipEntry(ZipEntry entry, Path targetDir) throws IOException {
        String entryName = entry.getName();

        if (entryName == null || entryName.trim().isEmpty()) {
            throw new IOException("ZIP条目名称为空");
        }
        if (containsPathTraversal(entryName)) {
            throw new IOException("ZIP条目包含非法路径: " + entryName);
        }
        if (Paths.get(entryName).isAbsolute()) {
            throw new IOException("ZIP条目包含绝对路径: " + entryName);
        }

        Path resolvedPath = targetDir.resolve(entryName).normalize();
        if (!resolvedPath.startsWith(targetDir)) {
            throw new IOException("ZIP条目试图逃逸目标目录: " + entryName);
        }
    }

    /**
     * 检查路径遍历攻击
     */
    private boolean containsPathTraversal(String path) {
        return path.contains("..") || path.contains("//") || path.contains("\\\\");
    }

    /**
     * 创建父目录
     */
    private void createParentDirectories(Path path) throws IOException {
        Path parent = path.getParent();
        if (parent != null && !Files.exists(parent)) {
            Files.createDirectories(parent);
        }
    }

    /**
     * 创建目标文件夹
     */
    private void createTargetFolder(Path targetDir) throws IOException {
        if (Files.exists(targetDir)) {
            if (!Files.isDirectory(targetDir)) {
                throw new IOException("目标路径已存在但不是文件夹: " + targetDir);
            }
        } else {
            Files.createDirectories(targetDir);
        }
    }

    /**
     * 分析ZIP文件找到根目录前缀
     */
    private String findRootPrefix(Path zipFile) throws IOException {
        List<String> entryNames = new ArrayList<>();

        try (ZipInputStream zis = new ZipInputStream(
                new BufferedInputStream(Files.newInputStream(zipFile)))) {

            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    entryNames.add(entry.getName());
                }
                zis.closeEntry();
            }
        }

        if (entryNames.isEmpty()) {
            return null;
        }

        // 找到共同前缀
        String commonPrefix = entryNames.get(0);
        for (int i = 1; i < entryNames.size(); i++) {
            while (!entryNames.get(i).startsWith(commonPrefix)) {
                commonPrefix = commonPrefix.substring(0, commonPrefix.length() - 1);
                if (commonPrefix.isEmpty()) {
                    return null;
                }
            }
        }

        // 确保前缀以目录分隔符结束
        int lastSeparator = commonPrefix.lastIndexOf('/');
        if (lastSeparator > 0) {
            commonPrefix = commonPrefix.substring(0, lastSeparator + 1);
        }

        return commonPrefix;
    }

    /**
     * 获取安全的文件名
     */
    private String getSafeFileName(String originalName, int fileIndex) {
        String fileName = Paths.get(originalName).getFileName().toString();
        if (fileName == null || fileName.isEmpty()) {
            return "file_" + fileIndex + ".dat";
        }
        return fileName;
    }

    /**
     * 清理不完整的压缩文件
     */
    private void cleanupIncompleteFile(Path file) {
        try {
            if (Files.exists(file)) {
                Files.delete(file);
                LOGGER.info("已清理不完整的压缩文件: {}", file);
            }
        } catch (IOException e) {
            LOGGER.warn("清理不完整文件失败: {}", e.getMessage());
        }
    }

    /**
     * 清理不完整的解压
     */
    private void cleanupIncompleteExtraction(Path targetDir) {
        try {
            if (Files.exists(targetDir) && Files.isDirectory(targetDir)) {
                deleteDirectoryRecursively(targetDir);
                LOGGER.info("已清理不完整的解压文件夹: {}", targetDir);
            }
        } catch (IOException e) {
            LOGGER.warn("清理不完整解压文件失败: {}", e.getMessage());
        }
    }

    /**
     * 递归删除目录
     */
    private void deleteDirectoryRecursively(Path dir) throws IOException {
        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                    throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc)
                    throws IOException {
                if (exc != null) {
                    throw exc;
                }
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

实现类

    @Override
    public ResponseEntity uploadByThread(MultipartFile file) {
        CompletableFuture.supplyAsync(() -> {
            long startTime = System.currentTimeMillis();
            try {
                // 1. 使用内存映射或流式处理大文件
                String originalFilename = file.getOriginalFilename();
                String fileExtension = fileUtils.getFileExtension(originalFilename);

                // 2. 异步处理文件保存
                String filePath = saveFileAsync(file);

                // 3. 快速响应,后台处理其他逻辑
                return ResponseEntity.ok("success");

            } catch (Exception e) {
                log.error("文件上传失败", e);
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件上传失败:" + e.getMessage());
            }
        }, fileProcessExecutor);
        return ResponseEntity.ok("success");
    }

     private String saveFileAsync(MultipartFile file) {
        // 使用NIO进行文件操作,提高性能
        Path tempPath = Paths.get(UPLOAD_DIR, fileUtils.generateFileName(file));

        try (InputStream inputStream = file.getInputStream();
             FileChannel channel = FileChannel.open(tempPath,
                     StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            // 使用通道传输,提高大文件写入效率
            channel.transferFrom(Channels.newChannel(inputStream), 0, file.getSize());

        } catch (IOException e) {
            throw new RuntimeException("文件保存失败", e);
        }
        return tempPath.toString();
    }


    @Override
    public ResponseEntity zipFile(String sourceDir, String zipFile) {
        try {
            log.info("开始压缩目录,sourceDir:{},zipFile:{}",sourceDir,zipFile);
            ZipUtility.compress(sourceDir, zipFile);
            log.info("压缩完成: " + zipFile);
        } catch (IllegalArgumentException e) {
            System.err.println("参数错误: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("参数错误: " + e.getMessage());
        } catch (FileNotFoundException e) {
            log.error("文件未找到:",e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        } catch (IOException e) {
            log.error("IO错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("IO错误: " + e.getMessage());
        } catch (Exception e) {
            log.error("未知错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        }
        return ResponseEntity.ok("success");
    }

    @Override
    public ResponseEntity unzipFile(String zipFile, String targetDir) {
        try {
            log.info("开始解压目录,zipFile:{},targetDir:{}",zipFile,targetDir);
            ZipUtility.decompress(zipFile, targetDir);
            log.info("解压完成: " + zipFile);
        } catch (IllegalArgumentException e) {
            System.err.println("参数错误: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("参数错误: " + e.getMessage());
        } catch (FileNotFoundException e) {
            log.error("文件未找到:",e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        } catch (IOException e) {
            log.error("IO错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("IO错误: " + e.getMessage());
        } catch (Exception e) {
            log.error("未知错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        }
        return ResponseEntity.ok("success");
    }

    @Override
    public ResponseEntity zipFolder(String sourceFolder, String zipFile ,boolean containRoot) {
        try {
            log.info("开始压缩目录,sourceFolder:{},zipFile:{},containRoot:{}",sourceFolder,zipFile,containRoot);
            FolderZipUtility.compressFolder(sourceFolder, zipFile, containRoot);
            log.info("解压完成: " + zipFile);
        } catch (IllegalArgumentException e) {
            System.err.println("参数错误: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("参数错误: " + e.getMessage());
        } catch (FileNotFoundException e) {
            log.error("文件未找到:",e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        } catch (IOException e) {
            log.error("IO错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("IO错误: " + e.getMessage());
        } catch (Exception e) {
            log.error("未知错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        }
        return ResponseEntity.ok("success");
    }

    @Override
    public ResponseEntity unzipFolder(String zipFile, String targetDir) {
        try {
            log.info("开始解压目录,zipFile:{},targetDir:{}",zipFile,targetDir);
            FolderZipUtility.decompressToFolder(zipFile, targetDir);
            log.info("解压完成: " + zipFile);
        } catch (IllegalArgumentException e) {
            System.err.println("参数错误: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("参数错误: " + e.getMessage());
        } catch (FileNotFoundException e) {
            log.error("文件未找到:",e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        } catch (IOException e) {
            log.error("IO错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("IO错误: " + e.getMessage());
        } catch (Exception e) {
            log.error("未知错误: ", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件未找到: " + e.getMessage());
        }
        return ResponseEntity.ok("success");
    }

@Override
    public ResponseEntity zipFolderByThread(String sourceDir, String zipFile, boolean containRoot) {
        String taskId = generateTaskId();

        asyncZipProcessor.compressFolderAsync(
                taskId, sourceDir, zipFile, containRoot,
                new AsyncZipProcessor.ProgressCallback() {
                    @Override
                    public void onProgress(String taskId, String currentFile,
                                           long processedBytes, long totalBytes,
                                           int processedFiles, int totalFiles) {
                        log.info("压缩进度 - 任务ID: {}, 进度: {}/{}, 文件: {}/{}",
                                taskId, processedBytes, totalBytes, processedFiles, totalFiles);
                    }

                    @Override
                    public void onComplete(AsyncZipProcessor.TaskResult result) {
                        log.info("压缩任务完成: {}", result.getTaskId());
                        // 可以在这里发送通知或更新数据库状态
                    }

                    @Override
                    public void onError(String taskId, String errorMessage) {
                        log.error("压缩任务失败: {}, 错误: {}", taskId, errorMessage);
                        // 错误处理逻辑
                    }
                }
        );
        return ResponseEntity.ok(taskId);
    }

    @Override
    public ResponseEntity unzipFolderByThread(String zipFile, String targetDir) {
        String taskId = generateTaskId();

        asyncZipProcessor.decompressFileAsync(
                taskId, zipFile, targetDir,
                new AsyncZipProcessor.ProgressCallback() {
                    @Override
                    public void onProgress(String taskId, String currentFile,
                                           long processedBytes, long totalBytes,
                                           int processedFiles, int totalFiles) {
                        log.info("解压进度 - 任务ID: {}, 进度: {}/{}, 文件: {}/{}",
                                taskId, processedBytes, totalBytes, processedFiles, totalFiles);
                    }

                    @Override
                    public void onComplete(AsyncZipProcessor.TaskResult result) {
                        log.info("解压任务完成: {}", result.getTaskId());
                    }

                    @Override
                    public void onError(String taskId, String errorMessage) {
                        log.error("解压任务失败: {}, 错误: {}", taskId, errorMessage);
                    }
                }
        );

        return ResponseEntity.ok(taskId);
    }

    private String generateTaskId() {
        return "task-" + System.currentTimeMillis() + "-" +
                ThreadLocalRandom.current().nextInt(1000, 9999);
    }

    @PreDestroy
    public void destroy() {
        asyncZipProcessor.shutdown();
    }

完整代码:文件操作合集

效果展示

压缩文件

请求参数

在这里插入图片描述
文件效果
在这里插入图片描述

解压文件

请求参数
在这里插入图片描述
效果
在这里插入图片描述

压缩文件夹

请求参数
在这里插入图片描述
效果
在这里插入图片描述
注意:压缩较大文件夹时,耗时较长,如果是页面请求,可能会超出20000ms。

解压文件夹

请求参数
在这里插入图片描述
注意:压缩文件过大,返回响应的时间会过长,可能会超过20000ms,如果是页面请求,可能会报错超时异常

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值