异步上传文件、压缩解压文件、文件夹
项目链接
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,如果是页面请求,可能会报错超时异常

被折叠的 条评论
为什么被折叠?



