Files
UserBaseService/src/main/java/com/yunhai/userbase/utils/FileUtil.java
T
chenxing 0bd99f9aa4
ci-userbase-service / build-and-push (push) Successful in 42s
修复异常引用
2026-08-11 16:46:11 +08:00

413 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.userbase.utils;
import com.yunhai.platform.utils.exception.DException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author min
* @description File工具类,扩展 hutool 工具包
* @date 2019-07-05
*/
public class FileUtil {
/**
* 定义GB的计算常量(改为long避免int溢出)
*/
private static final long GB = 1024L * 1024 * 1024;
/**
* 定义MB的计算常量
*/
private static final long MB = 1024L * 1024;
/**
* 定义KB的计算常量
*/
private static final long KB = 1024L;
/**
* 格式化小数
*/
private static final DecimalFormat DF = new DecimalFormat("0.00");
/**
* 上传文件存储根目录(由 FileStorageConfig 在启动时注入)。<br>
*/
private static String BASE_STORE_PATH;
/**
* 上传文件对外访问的 Web 根地址(由 FileStorageConfig 在启动时注入)。<br>
*/
private static String BASE_WEB_PATH;
/**
* 危险后缀黑名单:命中则禁止上传,防止恶意文件被执行/访问。<br>
*/
private static final Set<String> DANGEROUS_EXT = new HashSet<>(Arrays.asList(
"jsp", "jspx", "html", "htm", "js", "exe", "sh", "bat",
"php", "asp", "aspx", "jar", "war", "class", "htaccess"
));
/**
* 由 Spring 启动时注入存储根目录与 Web 访问前缀。<br>
*
* @param storePath 磁盘存储根目录
* @param webPath 对外访问的 Web 根地址
*/
public static void init(String storePath, String webPath) {
BASE_STORE_PATH = storePath;
BASE_WEB_PATH = webPath;
}
/**
* 校验文件后缀是否在危险黑名单中。<br>
*
* @param ext 文件后缀(可带或不带点)
* @throws DException 命中黑名单时抛出
*/
private static void validateExt(String ext) {
if (ext == null || ext.isEmpty()) {
return;
}
String lower = ext.toLowerCase().replaceFirst("^\\.", "");
if (DANGEROUS_EXT.contains(lower)) {
throw new DException("不支持的文件类型: " + ext);
}
}
/**
* 净化文件名,防止路径穿越(../ 等)。<br>
*
* @param name 原始文件名
* @return 净化后的安全文件名
*/
private static String sanitizeFileName(String name) {
if (name == null || name.trim().isEmpty()) {
return "file";
}
return name.replaceAll("[\\\\/]", "_")
.replaceAll("\\.{2,}", "_")
.replaceAll("\\s+", "_");
}
/**
* 拼接完整可访问的 Web URL(根地址 + 子目录 + 文件名)。<br>
*
* @param subDir 子目录(如 image/file/icon)
* @param fileName 文件名
* @return 完整访问 URL
*/
private static String buildUrl(String subDir, String fileName) {
String web = (BASE_WEB_PATH == null) ? "" : BASE_WEB_PATH;
web = web.endsWith("/") ? web : web + "/";
return web + subDir + "/" + fileName;
}
/**
* 校验存储配置已注入。<br>
*
* @throws DException 未注入时抛出
*/
private static void ensureStoreBase() {
if (BASE_STORE_PATH == null || BASE_WEB_PATH == null) {
throw new DException("文件存储路径未初始化,请检查 FILE_PATH / FILE_WEB_PATH 配置");
}
}
/**
* MultipartFile转File(临时文件)
* @param multipartFile 上传文件
* @return 临时文件对象,失败返回null
*/
public static File toFile(MultipartFile multipartFile) {
if (multipartFile == null || multipartFile.isEmpty()) {
return null;
}
// 获取文件后缀
String ext = getExtensionName(multipartFile.getOriginalFilename());
String prefix = "." + ext;
File file = null;
try {
// 用uuid作为文件名,防止生成的临时文件重复
file = File.createTempFile(IdUtil.simpleUUID(), prefix);
multipartFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
return file;
}
/**
* 删除文件/目录
* @param files 待删除文件数组
*/
public static void deleteFile(File... files) {
if (files == null || files.length == 0) {
return;
}
for (File file : files) {
if (file != null && file.exists()) {
cn.hutool.core.io.FileUtil.del(file);
}
}
}
/**
* 获取文件扩展名(不带点)
* @param filename 文件名
* @return 扩展名,无后缀返回原文件名
*/
public static String getExtensionName(String filename) {
return cn.hutool.core.io.FileUtil.extName(filename);
}
/**
* 获取不带扩展名的文件名
* @param filename 文件名
* @return 主文件名
*/
public static String getFileNameNoEx(String filename) {
return cn.hutool.core.io.FileUtil.mainName(filename);
}
/**
* 文件大小转换(修复int溢出问题)
* @param size 文件字节数
* @return 格式化后的大小字符串
*/
public static String getSize(int size) {
long sizeLong = size;
String resultSize;
if (sizeLong / GB >= 1) {
resultSize = DF.format(sizeLong / (double) GB) + "GB ";
} else if (sizeLong / MB >= 1) {
resultSize = DF.format(sizeLong / (double) MB) + "MB ";
} else if (sizeLong / KB >= 1) {
resultSize = DF.format(sizeLong / (double) KB) + "KB ";
} else {
resultSize = sizeLong + "B ";
}
return resultSize;
}
/**
* 字节数组上传图片(固定.png后缀,存 image 子目录)。<br>
*
* @param imgData 图片字节数组
* @return 完整可访问的 Web URL
*/
public static String uploadImg(byte[] imgData) {
return uploadBytes(imgData, "image", ".png");
}
/**
* MultipartFile上传图片(按原后缀,存 image 子目录)。<br>
*
* @param file 上传文件
* @return 完整可访问的 Web URL
*/
public static String uploadImg(MultipartFile file) {
return uploadMultipart(file, "image");
}
/**
* 字节数组上传文件(固定.png后缀,存 file 子目录)。<br>
*
* @param imgData 文件字节数组
* @return 完整可访问的 Web URL
*/
public static String uploadFile(byte[] imgData) {
return uploadBytes(imgData, "file", ".png");
}
/**
* MultipartFile上传文件(按原后缀,存 file 子目录)。<br>
*
* @param file 上传文件
* @return 完整可访问的 Web URL
*/
public static String uploadFile(MultipartFile file) {
return uploadMultipart(file, "file");
}
/**
* 通用上传:按原后缀保存到指定子目录,返回完整可访问的 Web URL。<br>
*
* @param file 上传文件
* @param subDir 子目录(如 image/file/icon/media/word/html)
* @return 完整可访问的 Web URL
*/
public static String upload(MultipartFile file, String subDir) {
return uploadMultipart(file, subDir);
}
/**
* URL文件代理中转下载
* @param response 响应对象
* @param address 远程文件地址
* @param contentType 响应Content-Type,传null自动识别
* @param fileName 下载文件名,传null取远程响应头
* @throws IOException 网络/IO异常
*/
public static void proxyUrlFile(HttpServletResponse response, String address, String contentType, String fileName) throws IOException {
// 原有https转http逻辑保留
if (address != null && address.toLowerCase().startsWith("https:")) {
address = address.replaceFirst("(?i)^https:", "http:")
.replaceAll(":443(/|$)", ":80$1");
}
HttpURLConnection httpURLConnection = null;
InputStream inputStream = null;
ServletOutputStream outputStream = null;
try {
URL url = new URL(address);
httpURLConnection = (HttpURLConnection) url.openConnection();
httpURLConnection.setConnectTimeout(5000);
httpURLConnection.setReadTimeout(30000);
httpURLConnection.connect();
inputStream = httpURLConnection.getInputStream();
outputStream = response.getOutputStream();
// 设置Content-Type
if (contentType != null) {
response.setContentType(contentType);
} else {
response.setContentType("multipart/form-data");
}
// 设置下载文件名
if (fileName != null) {
response.setHeader("Content-disposition", "attachment; filename=\""
+ new String(fileName.getBytes("utf-8"), "ISO8859-1") + "\"");
} else {
String headerFileName = "";
String disposition = httpURLConnection.getHeaderField("Content-Disposition");
if (disposition != null) {
headerFileName = new String(disposition.getBytes("ISO-8859-1"), "UTF-8");
}
if (headerFileName.contains("filename")) {
headerFileName = headerFileName.substring(headerFileName.lastIndexOf("filename") + "filename".length());
// 去除可能的=号和引号
headerFileName = headerFileName.replaceFirst("=", "").replace("\"", "").trim();
}
response.setHeader("Content-disposition", "attachment; filename=" + headerFileName);
}
// 流式拷贝,8KB缓冲区提升性能
IoUtil.copy(inputStream, outputStream, IoUtil.DEFAULT_BUFFER_SIZE);
} catch (Exception e) {
throw e;
} finally {
IoUtil.close(inputStream);
if (outputStream != null) {
outputStream.flush();
IoUtil.close(outputStream);
}
if (httpURLConnection != null) {
httpURLConnection.disconnect();
}
}
}
/**
* 保存上传文件到指定完整路径
* @param mf 上传文件
* @param path 完整保存路径(含文件名)
*/
public static void saveResource(MultipartFile mf, String path) {
if (mf == null || path == null || path.trim().length() == 0) {
throw new DException(DicUtil.ERROR_MSG_IMPORT_WRONG);
}
// 净化文件名,防止路径穿越(如 ../../evil)
File target = new File(path);
String safeName = sanitizeFileName(target.getName());
File safeFile = new File(target.getParentFile(), safeName);
// 自动创建父目录
cn.hutool.core.io.FileUtil.mkParentDirs(safeFile.getPath());
try (InputStream is = mf.getInputStream();
FileOutputStream fos = new FileOutputStream(safeFile)) {
IoUtil.copy(is, fos);
} catch (Exception e) {
throw new DException(DicUtil.ERROR_MSG_IMPORT_WRONG);
}
}
// ==================== 私有通用方法,消除重复代码 ====================
/**
* 字节数组通用上传(流式写入,返回完整 Web URL)。<br>
*
* @param data 字节数组
* @param subDir 子目录
* @param ext 固定后缀(带点)
* @return 完整可访问的 Web URL
*/
private static String uploadBytes(byte[] data, String subDir, String ext) {
if (data == null || data.length == 0 || subDir == null || subDir.trim().length() == 0) {
throw new DException("上传内容为空");
}
// 后缀黑名单校验(固定后缀也走同一规则)
validateExt(ext);
ensureStoreBase();
try {
// 自动创建多级目录
String dirPath = BASE_STORE_PATH + File.separator + subDir;
cn.hutool.core.io.FileUtil.mkdir(dirPath);
String fileName = IdUtil.fastSimpleUUID() + ext;
File targetFile = new File(dirPath, fileName);
// Hutool写入,自动关流
cn.hutool.core.io.FileUtil.writeBytes(data, targetFile);
return buildUrl(subDir, fileName);
} catch (DException e) {
throw e;
} catch (Exception e) {
throw new DException("文件上传失败: " + e.getMessage());
}
}
/**
* MultipartFile通用上传(流式写入,避免大文件OOM,返回完整 Web URL)。<br>
*
* @param file 上传文件
* @param subDir 子目录
* @return 完整可访问的 Web URL
*/
private static String uploadMultipart(MultipartFile file, String subDir) {
if (file == null || file.isEmpty() || subDir == null || subDir.trim().length() == 0) {
throw new DException("上传文件不能为空");
}
// 提取后缀
String originalFilename = file.getOriginalFilename();
String ext = cn.hutool.core.io.FileUtil.extName(originalFilename);
ext = (ext == null || ext.trim().length() == 0) ? "" : "." + ext.toLowerCase();
// 后缀黑名单校验
validateExt(ext);
ensureStoreBase();
try {
String dirPath = BASE_STORE_PATH + File.separator + subDir;
cn.hutool.core.io.FileUtil.mkdir(dirPath);
String fileName = IdUtil.fastSimpleUUID() + ext;
File targetFile = new File(dirPath, fileName);
// 流式写入,不加载全文件到内存
cn.hutool.core.io.FileUtil.writeFromStream(file.getInputStream(), targetFile);
return buildUrl(subDir, fileName);
} catch (DException e) {
throw e;
} catch (Exception e) {
throw new DException("文件上传失败: " + e.getMessage());
}
}
}