5.0版本 发布 优化下载方法 修复office 重复转换方法错误

This commit is contained in:
高雄
2026-01-22 15:41:54 +08:00
parent 4e5e9b7cba
commit 1e04822532
4 changed files with 248 additions and 37 deletions

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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 下载方法
*/

View File

@@ -60,8 +60,7 @@
9. 新增 防重复转换 <br>
10. 新增 异步等待 <br>
11. 新增 上传限制不支持的文件禁止上传 <br>
12. 新增 异步等待 <br>
13. 新增 cadviewer转换方法<br>
12. 新增 cadviewer转换方法<br>
<h4>修复</h4>
1. 压缩包路径问题 <br>
2. 安全问题 <br>