组态新增压缩包文件上传下载功能

This commit is contained in:
xy
2026-01-09 16:57:22 +08:00
parent cccc73f211
commit 5ff8c946aa
5 changed files with 707 additions and 11 deletions

View File

@@ -5,7 +5,7 @@ import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.File;
/**
* 类的介绍:
@@ -35,11 +35,13 @@ public class ElementParam {
private String elementMark;
@ApiModelProperty(value = "图元文件")
@NotNull(message="图元文件不能为空!")
private MultipartFile multipartFile;
@ApiModelProperty(value = "图元类型")
@NotBlank(message="图元类型不能为空!")
private String elementForm;
@ApiModelProperty(value = "压缩包文件")
private MultipartFile zipFile;
}

View File

@@ -0,0 +1,566 @@
package com.njcn.cssystem.utils;
import com.njcn.common.pojo.exception.BusinessException;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* SVG压缩包处理器
* 专门处理ZIP压缩包中的SVG文件
*/
@Component
public class SvgZipProcessor {
// SVG文件头检查
private static final String SVG_HEADER = "<?xml";
private static final String SVG_START_TAG = "<svg";
private static final Set<String> ALLOWED_EXTENSIONS = new HashSet<>(
Arrays.asList(".svg", ".svgz")
);
/**
* 方法1验证并提取所有SVG文件
* 返回格式Map<文件名, InputStream>
*/
public static Map<String, InputStream> extractValidSvgFiles(MultipartFile zipFile) throws IOException, InvalidSvgException {
Map<String, InputStream> svgFiles = new LinkedHashMap<>();
ZipArchiveInputStream zis = null;
ZipArchiveEntry entry = null;
try {
// 使用支持编码检测的ZipArchiveInputStream
zis = new ZipArchiveInputStream(zipFile.getInputStream(), "UTF-8", true);
byte[] buffer = new byte[8192];
while ((entry = zis.getNextZipEntry()) != null) {
if (!entry.isDirectory() && !isSvgFile(entry.getName())) {
throw new BusinessException("文件格式错误: " + entry.getName());
}
if (!entry.isDirectory() && isSvgFile(entry.getName())) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
while ((len = zis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
byte[] fileData = baos.toByteArray();
String fileName = entry.getName();
// 验证SVG文件
if (validateSvgFile(fileName, fileData)) {
ByteArrayInputStream svgStream = new ByteArrayInputStream(fileData);
svgFiles.put(fileName, svgStream);
} else {
throw new InvalidSvgException("无效的SVG文件: " + fileName);
}
baos.close();
}
}
} catch (BusinessException e) {
throw new BusinessException("文件格式错误: " + entry.getName());
} catch (Exception e) {
throw new IOException("处理ZIP文件时出错", e);
} finally {
// 确保流被关闭
if (zis != null) {
try {
zis.close();
} catch (IOException e) {
// 记录日志但不影响主要逻辑
System.err.println("关闭ZipArchiveInputStream时出错: " + e.getMessage());
}
}
}
if (svgFiles.isEmpty()) {
throw new InvalidSvgException("压缩包中没有找到有效的SVG文件");
}
return svgFiles;
}
/**
* 方法2流式处理SVG文件
*/
public void processSvgFilesStreaming(MultipartFile zipFile,
BiConsumer<String, InputStream> processor)
throws IOException, InvalidSvgException {
List<String> invalidFiles = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory() && isSvgFile(entry.getName())) {
// 读取并验证
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
while ((len = zis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
byte[] fileData = baos.toByteArray();
if (validateSvgFile(entry.getName(), fileData)) {
ByteArrayInputStream svgStream = new ByteArrayInputStream(fileData);
processor.accept(entry.getName(), svgStream);
svgStream.close();
} else {
invalidFiles.add(entry.getName());
}
baos.close();
}
zis.closeEntry();
}
}
// 如果有无效文件,抛出异常
if (!invalidFiles.isEmpty()) {
throw new InvalidSvgException("发现无效的SVG文件: " + invalidFiles);
}
}
/**
* 方法3严格的SVG验证和处理
*/
public List<SvgFileInfo> processAndValidateSvgFiles(MultipartFile zipFile,
ValidationOptions options)
throws IOException, InvalidSvgException {
List<SvgFileInfo> svgFiles = new ArrayList<>();
List<String> validationErrors = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zis.getNextEntry()) != null) {
String fileName = entry.getName();
// 跳过目录和非SVG文件
if (entry.isDirectory() || !isSvgFile(fileName)) {
if (!entry.isDirectory() && options.isStrictMode()) {
validationErrors.add("非SVG文件: " + fileName);
}
continue;
}
// 读取文件内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
while ((len = zis.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
byte[] fileData = baos.toByteArray();
// 执行验证
ValidationResult result = validateSvgWithDetails(fileName, fileData, options);
if (result.isValid()) {
SvgFileInfo svgInfo = new SvgFileInfo(
fileName,
new ByteArrayInputStream(fileData),
fileData.length,
entry.getTime(),
result.getSvgAttributes()
);
svgFiles.add(svgInfo);
} else {
validationErrors.add(fileName + ": " + result.getErrorMessage());
}
baos.close();
zis.closeEntry();
}
}
// 检查验证结果
if (!validationErrors.isEmpty()) {
throw new InvalidSvgException("SVG验证失败:\n" +
String.join("\n", validationErrors));
}
if (svgFiles.isEmpty()) {
throw new InvalidSvgException("压缩包中没有有效的SVG文件");
}
return svgFiles;
}
/**
* 方法4仅获取SVG文件名
*/
public List<String> getSvgFileNames(MultipartFile zipFile) throws IOException {
List<String> svgNames = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
if (!entry.isDirectory() && isSvgFile(entry.getName())) {
svgNames.add(entry.getName());
}
zis.closeEntry();
}
}
return svgNames;
}
/**
* 验证是否为SVG文件基于扩展名
*/
public static boolean isSvgFile(String fileName) {
if (fileName == null) return false;
String lowerName = fileName.toLowerCase();
return ALLOWED_EXTENSIONS.stream()
.anyMatch(lowerName::endsWith);
}
/**
* 基本的SVG文件验证
*/
private static boolean validateSvgFile(String fileName, byte[] fileData) {
try {
// 转换为字符串进行检查
String content = new String(fileData, StandardCharsets.UTF_8);
content = content.trim();
// 检查文件大小
if (fileData.length == 0) {
return false;
}
// 检查是否包含XML声明和SVG标签
boolean hasXmlHeader = content.startsWith(SVG_HEADER);
boolean hasSvgTag = content.contains(SVG_START_TAG);
// 简单验证要么有XML声明+SVG标签要么直接以<svg开头
if ((hasXmlHeader && hasSvgTag) || content.startsWith(SVG_START_TAG)) {
return true;
}
// 对于压缩的SVGZ文件尝试解压验证
if (fileName.toLowerCase().endsWith(".svgz")) {
// 这里可以添加SVGZ解压验证逻辑
return true; // 暂时返回true实际使用时需要解压验证
}
return false;
} catch (Exception e) {
return false;
}
}
/**
* 详细的SVG验证
*/
private ValidationResult validateSvgWithDetails(String fileName, byte[] fileData,
ValidationOptions options) {
ValidationResult result = new ValidationResult(fileName);
try {
String content = new String(fileData, StandardCharsets.UTF_8).trim();
// 1. 检查文件大小
if (fileData.length > options.getMaxFileSize()) {
result.setError("文件大小超过限制: " + fileData.length + " bytes");
return result;
}
if (fileData.length == 0) {
result.setError("文件为空");
return result;
}
// 2. 检查基本结构
boolean hasXmlHeader = content.startsWith(SVG_HEADER);
boolean hasSvgTag = content.contains(SVG_START_TAG);
if (!hasSvgTag) {
result.setError("缺少SVG标签");
return result;
}
// 3. 如果开启了XML验证进行严格的XML解析
if (options.isValidateXml()) {
if (!isValidXml(content)) {
result.setError("无效的XML结构");
return result;
}
// 提取SVG属性
Map<String, String> attributes = extractSvgAttributes(content);
result.setSvgAttributes(attributes);
// 检查SVG尺寸
if (options.isCheckDimensions()) {
String width = attributes.get("width");
String height = attributes.get("height");
if (width != null && height != null) {
try {
// 移除单位px, em, %等)
width = width.replaceAll("[^0-9.]", "");
height = height.replaceAll("[^0-9.]", "");
double w = Double.parseDouble(width);
double h = Double.parseDouble(height);
if (w > options.getMaxWidth() || h > options.getMaxHeight()) {
result.setError(String.format("尺寸过大: %.0fx%.0f", w, h));
return result;
}
if (w < options.getMinWidth() || h < options.getMinHeight()) {
result.setError(String.format("尺寸过小: %.0fx%.0f", w, h));
return result;
}
} catch (NumberFormatException e) {
// 如果无法解析尺寸,可能是使用百分比等单位
}
}
}
}
// 4. 检查是否有潜在危险元素(安全考虑)
if (options.isCheckSecurity()) {
if (containsDangerousElements(content)) {
result.setError("包含潜在危险元素");
return result;
}
}
result.setValid(true);
} catch (Exception e) {
result.setError("验证异常: " + e.getMessage());
}
return result;
}
/**
* 检查是否为有效的XML
*/
private boolean isValidXml(String content) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 安全配置防止XXE攻击
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.parse(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
return true;
} catch (ParserConfigurationException | SAXException | IOException e) {
return false;
}
}
/**
* 提取SVG属性
*/
private Map<String, String> extractSvgAttributes(String content) {
Map<String, String> attributes = new HashMap<>();
try {
int startIndex = content.indexOf("<svg");
if (startIndex == -1) return attributes;
int endIndex = content.indexOf('>', startIndex);
if (endIndex == -1) return attributes;
String svgTag = content.substring(startIndex, endIndex);
// 简单解析属性
String[] parts = svgTag.split("\\s+");
for (String part : parts) {
if (part.contains("=")) {
String[] keyValue = part.split("=", 2);
if (keyValue.length == 2) {
String key = keyValue[0].trim();
String value = keyValue[1].trim().replaceAll("[\"']", "");
attributes.put(key, value);
}
}
}
} catch (Exception e) {
// 解析失败返回空map
}
return attributes;
}
/**
* 检查是否包含危险元素
*/
private boolean containsDangerousElements(String content) {
String lowerContent = content.toLowerCase();
// 检查潜在的脚本标签
if (lowerContent.contains("<script") ||
lowerContent.contains("javascript:") ||
lowerContent.contains("onload=") ||
lowerContent.contains("onclick=") ||
lowerContent.contains("xlink:href=")) {
return true;
}
return false;
}
/**
* 获取文件扩展名
*/
public String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
return dotIndex > 0 ? fileName.substring(dotIndex + 1).toLowerCase() : "";
}
// =============== 内部类和异常 ===============
/**
* SVG文件信息类
*/
public class SvgFileInfo {
private final String fileName;
private final InputStream inputStream;
private final long fileSize;
private final long lastModified;
private final Map<String, String> svgAttributes;
public SvgFileInfo(String fileName, InputStream inputStream, long fileSize,
long lastModified, Map<String, String> svgAttributes) {
this.fileName = fileName;
this.inputStream = inputStream;
this.fileSize = fileSize;
this.lastModified = lastModified;
this.svgAttributes = svgAttributes;
}
// Getters
public String getFileName() { return fileName; }
public InputStream getInputStream() { return inputStream; }
public long getFileSize() { return fileSize; }
public long getLastModified() { return lastModified; }
public Map<String, String> getSvgAttributes() { return svgAttributes; }
public String getBaseName() {
int dotIndex = fileName.lastIndexOf('.');
return dotIndex > 0 ? fileName.substring(0, dotIndex) : fileName;
}
public String getExtension() {
return getFileExtension(fileName);
}
}
/**
* 验证选项类
*/
public class ValidationOptions {
private boolean strictMode = true; // 严格模式不允许非SVG文件
private boolean validateXml = true; // 验证XML结构
private boolean checkDimensions = true; // 检查尺寸
private boolean checkSecurity = true; // 安全检查
private long maxFileSize = 10 * 1024 * 1024; // 最大文件大小10MB
private double maxWidth = 5000; // 最大宽度
private double maxHeight = 5000; // 最大高度
private double minWidth = 10; // 最小宽度
private double minHeight = 10; // 最小高度
// Getters and Setters
public boolean isStrictMode() { return strictMode; }
public void setStrictMode(boolean strictMode) { this.strictMode = strictMode; }
public boolean isValidateXml() { return validateXml; }
public void setValidateXml(boolean validateXml) { this.validateXml = validateXml; }
public boolean isCheckDimensions() { return checkDimensions; }
public void setCheckDimensions(boolean checkDimensions) { this.checkDimensions = checkDimensions; }
public boolean isCheckSecurity() { return checkSecurity; }
public void setCheckSecurity(boolean checkSecurity) { this.checkSecurity = checkSecurity; }
public long getMaxFileSize() { return maxFileSize; }
public void setMaxFileSize(long maxFileSize) { this.maxFileSize = maxFileSize; }
public double getMaxWidth() { return maxWidth; }
public void setMaxWidth(double maxWidth) { this.maxWidth = maxWidth; }
public double getMaxHeight() { return maxHeight; }
public void setMaxHeight(double maxHeight) { this.maxHeight = maxHeight; }
public double getMinWidth() { return minWidth; }
public void setMinWidth(double minWidth) { this.minWidth = minWidth; }
public double getMinHeight() { return minHeight; }
public void setMinHeight(double minHeight) { this.minHeight = minHeight; }
}
/**
* 验证结果类
*/
public class ValidationResult {
private final String fileName;
private boolean valid;
private String errorMessage;
private Map<String, String> svgAttributes = new HashMap<>();
public ValidationResult(String fileName) {
this.fileName = fileName;
this.valid = false;
}
// Getters and Setters
public String getFileName() { return fileName; }
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public String getErrorMessage() { return errorMessage; }
public void setError(String errorMessage) {
this.errorMessage = errorMessage;
this.valid = false;
}
public Map<String, String> getSvgAttributes() { return svgAttributes; }
public void setSvgAttributes(Map<String, String> svgAttributes) {
this.svgAttributes = svgAttributes;
}
}
/**
* 自定义异常类
*/
public static class InvalidSvgException extends Exception {
public InvalidSvgException(String message) {
super(message);
}
public InvalidSvgException(String message, Throwable cause) {
super(message, cause);
}
}
}