mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-08 09:17:36 +00:00
5.0版本 发布 优化下载方法 修复office 重复转换方法错误
This commit is contained in:
@@ -67,7 +67,10 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
||||
String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径
|
||||
|
||||
// 查询转换状态
|
||||
checkAndHandleConvertStatus(model, fileName, cacheName,fileAttribute);
|
||||
String convertStatusResult = checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
|
||||
if (convertStatusResult != null) {
|
||||
return convertStatusResult;
|
||||
}
|
||||
|
||||
if (!officePreviewType.equalsIgnoreCase("html")) {
|
||||
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
|
||||
@@ -307,9 +310,11 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
||||
/**
|
||||
* 异步方法
|
||||
*/
|
||||
public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute){
|
||||
public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute) {
|
||||
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
|
||||
int refreshSchedule = ConfigConstants.getTime();
|
||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||
|
||||
if (status != null) {
|
||||
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
|
||||
// 正在转换中,返回等待页面
|
||||
@@ -318,11 +323,27 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
||||
model.addAttribute("message", status.getRealTimeMessage());
|
||||
return WAITING_FILE_PREVIEW_PAGE;
|
||||
} else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) {
|
||||
// 超时状态,不允许重新转换
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
|
||||
// 超时状态,检查是否有强制更新命令
|
||||
if (forceUpdatedCache) {
|
||||
// 强制更新命令,清除状态,允许重新转换
|
||||
FileConvertStatusManager.convertSuccess(cacheName);
|
||||
logger.info("强制更新命令跳过超时状态,允许重新转换: {}", cacheName);
|
||||
return null; // 返回null表示继续执行
|
||||
} else {
|
||||
// 没有强制更新,不允许重新转换
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
|
||||
}
|
||||
} else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) {
|
||||
// 失败状态,不允许重新转换
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换");
|
||||
// 失败状态,检查是否有强制更新命令
|
||||
if (forceUpdatedCache) {
|
||||
// 强制更新命令,清除状态,允许重新转换
|
||||
FileConvertStatusManager.convertSuccess(cacheName);
|
||||
logger.info("强制更新命令跳过失败状态,允许重新转换: {}", cacheName);
|
||||
return null; // 返回null表示继续执行
|
||||
} else {
|
||||
// 没有强制更新,不允许重新转换
|
||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换");
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -7,8 +7,18 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||
import io.mola.galimatias.GalimatiasParseException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.core5.http.HttpResponse;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -27,6 +37,7 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static cn.keking.utils.KkFileUtils.*;
|
||||
|
||||
@@ -41,10 +52,56 @@ public class DownloadUtils {
|
||||
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
|
||||
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
|
||||
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
|
||||
private static final RestTemplate restTemplate = new RestTemplate();
|
||||
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
private static final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// 使用静态单例的HttpClient和RestTemplate,实现连接复用
|
||||
private static volatile CloseableHttpClient httpClient;
|
||||
private static volatile RestTemplate restTemplate;
|
||||
|
||||
// 获取单例HttpClient(线程安全)
|
||||
private static CloseableHttpClient getHttpClient() throws Exception {
|
||||
if (httpClient == null) {
|
||||
synchronized (DownloadUtils.class) {
|
||||
if (httpClient == null) {
|
||||
httpClient = createConfiguredHttpClient();
|
||||
logger.info("HttpClient初始化完成,已启用连接池和超时配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
// 获取单例RestTemplate
|
||||
private static RestTemplate getRestTemplate() throws Exception {
|
||||
if (restTemplate == null) {
|
||||
synchronized (DownloadUtils.class) {
|
||||
if (restTemplate == null) {
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||
factory.setHttpClient(getHttpClient());
|
||||
|
||||
// 设置连接和读取超时(毫秒)
|
||||
factory.setConnectTimeout(10000); // 10秒连接超时
|
||||
factory.setReadTimeout(30000); // 30秒读取超时
|
||||
factory.setConnectionRequestTimeout(5000); // 5秒获取连接超时
|
||||
|
||||
restTemplate = new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
}
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
// 应用关闭时清理资源
|
||||
public static void shutdown() {
|
||||
if (httpClient != null) {
|
||||
try {
|
||||
httpClient.close();
|
||||
logger.info("HttpClient已关闭");
|
||||
} catch (IOException e) {
|
||||
logger.warn("关闭HttpClient失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fileAttribute fileAttribute
|
||||
@@ -61,7 +118,8 @@ public class DownloadUtils {
|
||||
}
|
||||
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
|
||||
String realPath = getRelFilePath(fileName, fileAttribute);
|
||||
|
||||
// 获取文件后缀用于校验
|
||||
final String fileSuffix = fileAttribute.getSuffix();
|
||||
// 判断是否非法地址
|
||||
if (KkFileUtils.isIllegalFileName(realPath)) {
|
||||
response.setCode(1);
|
||||
@@ -92,11 +150,8 @@ public class DownloadUtils {
|
||||
if (isHttpUrl(url)) {
|
||||
File realFile = new File(realPath);
|
||||
|
||||
// 创建配置好的HttpClient
|
||||
CloseableHttpClient httpClient = createConfiguredHttpClient();
|
||||
|
||||
factory.setHttpClient(httpClient);
|
||||
restTemplate.setRequestFactory(factory);
|
||||
// 使用单例的RestTemplate,复用连接池
|
||||
RestTemplate template = getRestTemplate();
|
||||
String finalUrlStr = urlStr;
|
||||
RequestCallback requestCallback = request -> {
|
||||
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
||||
@@ -111,10 +166,41 @@ public class DownloadUtils {
|
||||
}
|
||||
};
|
||||
try {
|
||||
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
||||
FileUtils.copyToFile(fileResponse.getBody(), realFile);
|
||||
final boolean[] hasError = {false};
|
||||
String finalUrlStr1 = urlStr;
|
||||
template.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
||||
try {
|
||||
// 获取响应头中的Content-Type
|
||||
String contentType = WebUtils.headersType(fileResponse);
|
||||
|
||||
// 如果是Office/设计文件,需要校验MIME类型
|
||||
if ( WebUtils.isMimeCheckRequired(fileSuffix)) {
|
||||
if (! WebUtils.isValidMimeType(contentType, fileSuffix)) {
|
||||
logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}",
|
||||
finalUrlStr1, contentType);
|
||||
hasError[0] = true;
|
||||
// 重要:关闭响应流,不读取后续数据
|
||||
fileResponse.close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
FileUtils.copyToFile(fileResponse.getBody(), realFile);
|
||||
} catch (Exception e) {
|
||||
logger.error("处理文件响应时出错", e);
|
||||
hasError[0] = true;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// 如果下载过程中出现错误
|
||||
if (hasError[0]) {
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("文件类型校验失败");
|
||||
return response;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果是SSL证书错误,给出建议
|
||||
if (e.getMessage() != null &&
|
||||
@@ -126,16 +212,10 @@ public class DownloadUtils {
|
||||
}
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("下载失败:" + e);
|
||||
response.setMsg("下载失败:" + e.getMessage());
|
||||
return response;
|
||||
} finally {
|
||||
// 确保HttpClient被关闭
|
||||
try {
|
||||
httpClient.close();
|
||||
} catch (IOException e) {
|
||||
logger.warn("关闭HttpClient失败", e);
|
||||
}
|
||||
}
|
||||
// 不再需要finally块中关闭HttpClient,因为复用
|
||||
} else if (isFtpUrl(url)) {
|
||||
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
||||
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
||||
@@ -153,7 +233,7 @@ public class DownloadUtils {
|
||||
response.setMsg(fileName);
|
||||
return response;
|
||||
} catch (IOException | GalimatiasParseException e) {
|
||||
logger.error("文件下载失败,url:{}", urlStr);
|
||||
logger.error("文件下载失败,url:{}", urlStr, e);
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
if (e instanceof FileNotFoundException) {
|
||||
@@ -163,22 +243,53 @@ public class DownloadUtils {
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
logger.error("下载过程发生未知异常", e);
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("下载失败:" + e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建根据配置定制的HttpClient
|
||||
* 创建根据配置定制的HttpClient(连接池版本)
|
||||
*/
|
||||
private static CloseableHttpClient createConfiguredHttpClient() throws Exception {
|
||||
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom();
|
||||
// 使用新的Builder API创建连接池管理器
|
||||
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setMaxConnTotal(100) // 最大连接数
|
||||
.setMaxConnPerRoute(20) // 每个路由最大连接数
|
||||
.setDefaultConnectionConfig(ConnectionConfig.custom()
|
||||
.setConnectTimeout(Timeout.ofSeconds(10)) // 连接超时10秒
|
||||
.setSocketTimeout(Timeout.ofSeconds(30)) // Socket超时30秒
|
||||
.setTimeToLive(TimeValue.ofMinutes(10)) // 连接存活时间10分钟
|
||||
.build())
|
||||
.build();
|
||||
|
||||
// 创建请求配置
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setResponseTimeout(Timeout.ofSeconds(30)) // 响应超时30秒
|
||||
.setConnectionRequestTimeout(Timeout.ofSeconds(5)) // 获取连接超时5秒
|
||||
.build();
|
||||
|
||||
// 创建HttpClientBuilder
|
||||
HttpClientBuilder builder = HttpClients.custom()
|
||||
.setConnectionManager(connectionManager)
|
||||
.setDefaultRequestConfig(requestConfig);
|
||||
|
||||
// 配置Keep-Alive策略
|
||||
ConnectionKeepAliveStrategy keepAliveStrategy = (response, context) -> {
|
||||
// 默认保持30秒
|
||||
return TimeValue.ofSeconds(30);
|
||||
};
|
||||
builder.setKeepAliveStrategy(keepAliveStrategy);
|
||||
|
||||
// 配置SSL
|
||||
if (ConfigConstants.isIgnoreSSL()) {
|
||||
logger.debug("创建忽略SSL验证的HttpClient");
|
||||
// 如果SslUtils有创建builder的方法就更好了,这里假设我们直接使用SslUtils
|
||||
// 或者我们可以创建一个新的方法来返回配置了忽略SSL的builder
|
||||
return createHttpClientWithConfig();
|
||||
// 如果SslUtils支持HttpClient5,则使用它来创建client
|
||||
// 否则,我们需要在这里配置忽略SSL
|
||||
return createHttpClientWithConfig(builder);
|
||||
} else {
|
||||
logger.debug("创建标准HttpClient");
|
||||
}
|
||||
@@ -195,10 +306,17 @@ public class DownloadUtils {
|
||||
/**
|
||||
* 创建配置了忽略SSL的HttpClient
|
||||
*/
|
||||
private static CloseableHttpClient createHttpClientWithConfig() throws Exception {
|
||||
return SslUtils.createHttpClientIgnoreSsl();
|
||||
}
|
||||
private static CloseableHttpClient createHttpClientWithConfig(HttpClientBuilder builder) throws Exception {
|
||||
// 如果SslUtils支持直接配置builder,则:
|
||||
// SslUtils.configureIgnoreSsl(builder);
|
||||
// return builder.build();
|
||||
|
||||
// 否则,使用SslUtils创建client
|
||||
CloseableHttpClient sslIgnoredClient = SslUtils.createHttpClientIgnoreSsl();
|
||||
|
||||
logger.warn("SslUtils.createHttpClientIgnoreSsl()可能没有连接池配置,建议修改SslUtils以支持builder配置");
|
||||
return sslIgnoredClient;
|
||||
}
|
||||
|
||||
// 处理file协议的文件下载
|
||||
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
|
||||
@@ -280,5 +398,4 @@ public class DownloadUtils {
|
||||
}
|
||||
return realPath;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
@@ -461,6 +463,78 @@ public class WebUtils {
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Content-Type
|
||||
*/
|
||||
|
||||
public static String headersType(ClientHttpResponse fileResponse) {
|
||||
if (fileResponse == null) {
|
||||
return null;
|
||||
}
|
||||
HttpHeaders headers = fileResponse.getHeaders();
|
||||
if (headers == null) {
|
||||
return null;
|
||||
}
|
||||
String contentTypeStr = null;
|
||||
try {
|
||||
// 直接获取Content-Type头字符串
|
||||
contentTypeStr = headers.getFirst(HttpHeaders.CONTENT_TYPE);
|
||||
if (contentTypeStr == null || contentTypeStr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
// 解析为MediaType对象
|
||||
MediaType mediaType = MediaType.parseMediaType(contentTypeStr);
|
||||
// 返回主类型和子类型,忽略参数
|
||||
return mediaType.getType() + "/" + mediaType.getSubtype();
|
||||
} catch (Exception e) {
|
||||
// 如果解析失败,尝试简单的字符串处理
|
||||
if (contentTypeStr != null) {
|
||||
// 移除分号及后面的参数
|
||||
int semicolonIndex = contentTypeStr.indexOf(';');
|
||||
if (semicolonIndex > 0) {
|
||||
return contentTypeStr.substring(0, semicolonIndex).trim();
|
||||
}
|
||||
return contentTypeStr.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否需要校验MIME类型
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否需要校验
|
||||
*/
|
||||
public static boolean isMimeCheckRequired(String suffix) {
|
||||
if (suffix == null) {
|
||||
return false;
|
||||
}
|
||||
String lowerSuffix = suffix.toLowerCase();
|
||||
return Arrays.asList(
|
||||
"doc", "docx", "ppt", "pptx", "pdf", "dwg",
|
||||
"dxf", "dwf", "psd", "wps", "xlsx", "xls",
|
||||
"rar", "zip"
|
||||
).contains(lowerSuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验文件MIME类型是否有效
|
||||
* @param contentType 响应头中的Content-Type
|
||||
* @param suffix 文件后缀
|
||||
* @return 是否有效
|
||||
*/
|
||||
public static boolean isValidMimeType(String contentType, String suffix) {
|
||||
if (contentType == null || suffix == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 如果检测到是HTML、文本或JSON格式,则认为是错误响应
|
||||
String lowerContentType = contentType.toLowerCase();
|
||||
return !lowerContentType.contains("text/html")
|
||||
&& !lowerContentType.contains("text/plain")
|
||||
&& !lowerContentType.contains("application/json");
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持basic 下载方法
|
||||
*/
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
9. 新增 防重复转换 <br>
|
||||
10. 新增 异步等待 <br>
|
||||
11. 新增 上传限制不支持的文件禁止上传 <br>
|
||||
12. 新增 异步等待 <br>
|
||||
13. 新增 cadviewer转换方法<br>
|
||||
12. 新增 cadviewer转换方法<br>
|
||||
<h4>修复</h4>
|
||||
1. 压缩包路径问题 <br>
|
||||
2. 安全问题 <br>
|
||||
|
||||
Reference in New Issue
Block a user