init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s
ci-userbase-service / build-and-push (push) Successful in 40s
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
package com.yunhai.userbase.utils;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.bestsch.utils.DException;
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user