com.yixan.base.web.utils.WebFileUtil Maven / Gradle / Ivy
package com.yixan.base.web.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yixan.base.common.api.file.model.FolderOptions;
import com.yixan.base.common.api.file.model.IFolderName;
import com.yixan.base.common.api.file.service.IFileService;
import com.yixan.base.common.api.system.model.IUser;
import com.yixan.base.common.error.ErrorCode;
import com.yixan.base.common.svc.file.service.LocalFileService;
import com.yixan.base.core.exception.ResultCode;
import com.yixan.base.core.exception.ServiceException;
import com.yixan.tools.common.util.FileUtil;
import com.yixan.tools.common.util.StringUtil;
import com.yixan.tools.common.util.VerifyUtil;
/**
* WEB文件上传下载处理工具
*
* @author zhaohuihua
* @version V1.0 2017-11-30
*/
public class WebFileUtil {
private static final Logger log = LoggerFactory.getLogger(WebFileUtil.class);
private IFileService fileService;
public WebFileUtil(IFileService fileService) {
this.fileService = fileService;
}
/**
* 生成保存文件的参数对象
*
* @param folder 根文件夹, 可为空(为空时由fileService.groupUseDefIfNull决定默认用files还是不要根文件夹)
* @param extra 附加参数, 用于替换文件路径和文件名中的占位符
* @return
*/
public static IFolderName generateFolderParams(String folder, Map extra) {
IUser user = UserUtil.checkLoginUser();
Map map = new HashMap<>();
if (user != null) {
map.put("factory-code", user.getFactoryCode());
map.put("user-id", user.getUserId());
} else {
map.put("factory-code", "000000");
map.put("user-id", "null");
}
if (VerifyUtil.isNotBlank(extra)) {
map.putAll(extra);
}
FolderOptions options = new FolderOptions();
options.setFolder(folder);
options.setExtra(map);
return options;
}
/**
* 生成保存文件的参数对象
*
* @param folder 根文件夹, 可为空(为空时由fileService.groupUseDefIfNull决定默认用files还是不要根文件夹)
* @param extra 附加参数, 用于替换文件路径和文件名中的占位符
* @return
*/
public static IFolderName generateFolderParams(String folder, String extra) {
JSONObject json = null;
if (VerifyUtil.isNotBlank(extra)) {
try {
json = (JSONObject) JSON.parse(extra);
} catch (Exception e) {
log.error("File upload, extra params can't parse, {}, {}", e.toString(), extra);
}
}
return generateFolderParams(folder, json);
}
/**
* 发现并上传文件
* 每个需要上传的文件, 在前端构建两个input
* 一个type="file"用于上传文件流, 一个type="hidden", 用于保存修改前的图片地址; 用序号一一对应
* <p> <input type="file" name="files0"/> <input type="hidden" name="images0"/> <p>
* <p> <input type="file" name="files1"/> <input type="hidden" name="images1"/> <p>
* <p> <input type="file" name="files2"/> <input type="hidden" name="images2"/> <p>
* <p> <input type="file" name="files3"/> <input type="hidden" name="images3"/> <p>
* <p> <input type="file" name="files4"/> <input type="hidden" name="images4"/> <p>
* 后台调用findAndUploadFiles(request, "images", "files")查找图片
* 图片列表是有顺序的
* 因此如果5张图片, 其中1/2(序号从0开始)修改了, 那么仍然应该是5条记录
* 其中0/3/4的type="hidden"是原图片URL,type="file"为null, 1/2的type="hidden"是新图片的原URL,type="file"为新图片输入流
* images0/images1/images2/images3/images4=原图片URL; files1/files2=文件流
* 此时应将files1/files2上传至文件服务器, 取得URL, 替换到images1/images2的位置
*
* 另外, 如果文件直接上传至文件服务器而不通过JAVA服务器中转
* 那么在前端就已已经把images1/images2替换成新图片地址了
* 应将type="file"设为disabled, 相当于每次type="file"都为null, 然后这个处理逻辑也就没有问题, 可以兼容了
*
* @param request
* @param imageFieldPrefix 图片字段的前缀
* @param fileFieldPrefix 文件字段的前缀
* @return 上传后的图片地址
* @throws ServiceException
*/
public List findAndUploadFiles(HttpServletRequest request, String imageFieldPrefix, String fileFieldPrefix)
throws ServiceException {
String folder = request.getParameter("folder");
String filename = request.getParameter("filename");
String extra = request.getParameter("extra");
int size = 100;
int max = -1;
String[] urls = new String[size];
for (int i = 0; i < size; i++) {
// images0, images3, images4 原图片URL
String url = request.getParameter(imageFieldPrefix + i);
if (VerifyUtil.isNotBlank(url)) {
urls[i] = url;
if (max < i) {
max = i;
}
}
}
if (request instanceof MultipartHttpServletRequest) {
MultipartHttpServletRequest mrequest = (MultipartHttpServletRequest) request;
for (Entry entry : mrequest.getFileMap().entrySet()) {
if (entry.getKey().startsWith(fileFieldPrefix)) {
// files1, files2 文件流
String suffix = entry.getKey().substring(fileFieldPrefix.length());
if (VerifyUtil.isNotBlank(suffix) && StringUtil.isDigit(suffix)) {
int i = Integer.valueOf(suffix);
if (i < size && entry.getValue().getSize() > 0) {
urls[i] = upload(entry.getValue(), folder, filename, extra);
}
if (max < i) {
max = i;
}
}
}
}
}
List list = new ArrayList<>();
for (int i = 0; i <= max; i++) {
list.add(urls[i]);
}
return list;
}
/**
* 文件上传
* 文件扩展名必须在allow.upload.suffix配置中
*
* @param file 上传的文件内容
* @param folder 根文件夹, 可为空(为空时由fileService.groupUseDefIfNull决定默认用files还是不要根文件夹)
* @param filename 文件名, 用来获取文件扩展名
* @param extra 附加参数, 用于替换文件路径和文件名中的占位符
* @return 上传后的URL路径
*/
public String upload(MultipartFile file, String folder, String filename, Map extra)
throws ServiceException {
IFolderName options = generateFolderParams(folder, extra);
return upload(file, folder, filename, options);
}
/**
* 文件上传
* 文件扩展名必须在allow.upload.suffix配置中
*
* @param file 上传的文件内容
* @param folder 根文件夹, 可为空(为空时由fileService.groupUseDefIfNull决定默认用files还是不要根文件夹)
* @param filename 文件名, 用来获取文件扩展名
* @param extra 附加参数, 用于替换文件路径和文件名中的占位符
* @return 上传后的URL路径
*/
public String upload(MultipartFile file, String folder, String filename, String extra) throws ServiceException {
IFolderName options = generateFolderParams(folder, extra);
return upload(file, folder, filename, options);
}
/**
* 文件上传
* 文件扩展名必须在allow.upload.suffix配置中
*
* @param file 上传的文件内容
* @param folder 根文件夹, 可为空(为空时由fileService.groupUseDefIfNull决定默认用files还是不要根文件夹)
* @param filename 文件名, 用来获取文件扩展名
* @param options 附加参数, 用于替换文件路径和文件名中的占位符
* @return 上传后的URL路径
*/
public String upload(MultipartFile file, String folder, String filename, IFolderName options)
throws ServiceException {
if (filename == null) {
filename = file.getName();
}
if (!isAllowExtension(filename, "allow.upload.suffix", "allow.download.suffix")) {
throw new ServiceException(ErrorCode.FILE_FORMAT_NONSUPPORT);
}
InputStream input;
try {
input = file.getInputStream();
} catch (IOException e) {
throw new ServiceException(ErrorCode.FILE_UPLOAD_ERROR, e);
}
long size = file.getSize();
return fileService.upload(options, filename, input, size);
}
/**
* 下载文件
* 文件扩展名必须在allow.download.suffix配置中
* 文件必须位于文件服务配置的文件夹或allow.download.folder配置中
*
* @param path 文件的保存路径(不可为空)
* @param filename 下载时浏览器提示的文件名(可为空,从保存路径中截取)
* @throws IOException
* @throws ServiceException
*/
public void download(HttpServletRequest request, HttpServletResponse response, String path, String filename)
throws ServiceException {
if (VerifyUtil.isBlank(path)) {
throw new ServiceException(ResultCode.PARAMTER_NOT_NULL);
}
if (VerifyUtil.isBlank(filename)) {
filename = new File(path).getName();
}
// 1.获取要下载的文件的绝对路径
String realPath = getRealPath(path);
if (!new File(realPath).exists()) {
throw new ServiceException(ErrorCode.FILE_NOT_FOUND);
}
try {
// 2.设置content-disposition响应头控制浏览器以下载的形式打开文件
response.setHeader("content-disposition", FileIoUtil.toAttachmentDesc(request, filename));
} catch (Exception e) {
String extension = VerifyUtil.nvl(FileUtil.getExtension(filename), ".tmp");
response.setHeader("content-disposition", "attachment;filename=file" + extension);
}
// 3.获取要下载的文件输入流
try (InputStream in = new FileInputStream(realPath);) {
// 3.1.通过response对象获取OutputStream流
OutputStream out = response.getOutputStream();
// 3.2.复制文件流数据
FileUtil.copy(in, out);
} catch (IOException e) {
throw new ServiceException(ErrorCode.FILE_DOWNLOAD_ERROR, e);
}
}
// 获取文件实际保存路径
// 如果是download.location.xxx指定的路径, 根据配置获取文件
// 如果是文件服务保存的, 根据文件服务配置获取文件
// 否则获取WEB服务根路径下的文件
private String getRealPath(String path) throws ServiceException {
if (!StringUtil.isUrl(path)) {
String savePath = getSpecialLocationPath(path);
if (savePath != null) {
return savePath;
}
}
String serverPath = WebUtil.getInstance().getWebRoot();
// 先判断文件路径是不是文件服务
LocalFileService fs = null;
if (fileService instanceof LocalFileService) {
LocalFileService lfs = (LocalFileService) fileService;
if (fileService.isInsideFile(path)) {
fs = lfs;
}
}
if (fs == null) { // WebRoot
if (StringUtil.isUrl(path)) {
throw new ServiceException(ResultCode.PARAMTER_VALUE_ERROR);
}
if (!isAllowDownload(path, "web", true)) {
throw new ServiceException(ResultCode.FORBIDDEN);
}
return FileUtil.concat(serverPath, path);
} else { // FileService
if (!isAllowDownload(path, "filecenter", false)) {
throw new ServiceException(ResultCode.FORBIDDEN);
}
// baseUrl = http://img.xx.com/ 或 /filecenter/
String baseUrl = fs.getBaseUrl();
String relativePath = path.substring(baseUrl.length());
String savePath = fs.getSavePath();
if (FileUtil.isAbsolutePath(savePath)) { // 绝对路径
// savePath = /home/filecenter/
return FileUtil.concat(savePath, relativePath);
} else { // 相对路径
// savePath = ../filecenter/
return FileUtil.concat(serverPath, savePath, relativePath);
}
}
}
private static final Pattern FIRST_FOLDER_NAME = Pattern.compile("\\s*[/\\\\]?([^/\\\\]+)[/\\\\](.*+)\\s*");
private static final Pattern DOWNLOAD_LOCATION_CONFIG = Pattern.compile("\\s*(?:(alias|root)\\s+)?(.*+)\\s*");
// 通过配置指定允许下载指定文件夹中的文件
// 如果: download.location.xxx = alias /home/tomcat/runtimelogs/
// 那么: path=xxx/web-user-error.log = /home/tomcat/runtimelogs/web/user-error.log文件
// 如果: download.location.xxx = root /home/tomcat/runtimelogs/
// 那么: path=xxx/web/user-error.log = /home/tomcat/runtimelogs/xxx/web/user-error.log文件
// 完整配置
// download.location.xxx = alias /home/tomcat/runtimelogs/
// allow.download.suffix.location.xxx = .log|.err|.txt
private String getSpecialLocationPath(String path) throws ServiceException {
String folder;
String file;
{
Matcher matcher = FIRST_FOLDER_NAME.matcher(path);
if (matcher.matches()) {
folder = matcher.group(1);
file = matcher.group(2);
} else {
return null;
}
}
String type;
String root;
{
WebConfig config = WebConfig.getInstance();
String string = config.getString("download.location." + folder);
if (VerifyUtil.isBlank(string)) {
return null;
}
Matcher matcher = DOWNLOAD_LOCATION_CONFIG.matcher(string);
if (matcher.matches()) {
type = matcher.group(1);
root = matcher.group(2);
} else {
return null;
}
}
if (!isAllowDownload(path, "location." + folder, false)) {
throw new ServiceException(ResultCode.FORBIDDEN);
}
if (VerifyUtil.isBlank(type) || "alias".equals(type)) {
return FileUtil.concat(root, file);
} else if ("root".equals(type)) {
return FileUtil.concat(root, folder, file);
} else {
return null;
}
}
private boolean isAllowExtension(String path, String key, String... keys) {
WebConfig config = WebConfig.getInstance();
String string = config.getStringUseDefKeys(key, keys);
return isAllowExtension(path, StringUtil.split(string));
}
/** 判断是否允许下载 **/
// allow.download.suffix = default
// allow.download.suffix.web = WebRoot
// allow.download.suffix.filecenter = FileService
// allow.download.suffix.location = download.location
// allow.download.suffix.location.xxx = download.location.xxx
private boolean isAllowDownload(String path, String type, boolean checkFolder) {
WebConfig config = WebConfig.getInstance();
String suffixes = config.getStringUseSuffix("allow.download.suffix", type);
boolean allowSuffix = isAllowExtension(path, StringUtil.split(suffixes));
boolean allowFolder = true;
// WebRoot下的文件需要判断文件夹
if (checkFolder) {
String string = config.getStringUseSuffix("allow.download.folder", type);
String[] folders = StringUtil.split(string);
if (folders == null || folders.length == 0) {
folders = new String[] { "assets" }; // 兼容旧版本
}
allowFolder = isAllowFolder(path, folders);
}
return allowSuffix && allowFolder;
}
private boolean isAllowExtension(String path, String[] suffixes) {
// 判断文件名后缀
if (VerifyUtil.isNotBlank(suffixes)) {
for (String suffix : suffixes) {
if ("*".equals(suffix)) {
return true;
}
if (path.endsWith(suffix)) {
return true;
}
}
}
return false;
}
private boolean isAllowFolder(String path, String[] folders) {
if (VerifyUtil.isNotBlank(folders)) {
for (String folder : folders) {
if ("*".equals(folder)) {
return true;
}
if (path.startsWith(folder + "/") || path.startsWith("/" + folder + "/")) {
return true;
}
}
}
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy