新增 页码定位 美化前端 其他功能调整等

This commit is contained in:
高雄
2026-01-19 17:46:32 +08:00
parent 904af89190
commit 7dc0469b30
48 changed files with 10922 additions and 1241 deletions

View File

@@ -17,6 +17,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
public class ConfigConstants {
public static final String BEAN_NAME = "configConstants";
static {
// PDFBox兼容低版本JDK
System.setProperty("sun.java2d.cmm", "sun.java2d.cmm.kcms.KcmsServiceProvider");
@@ -90,7 +91,7 @@ public class ConfigConstants {
public static final String DEFAULT_PICTURES_PREVIEW = "true";
public static final String DEFAULT_GET_CORS_FILE = "true";
public static final String DEFAULT_ADD_TASK = "true";
public static final String DEFAULT_AES_KEY = "1234567890123456";
public static final String DEFAULT_AES_KEY = "false";
// 12. UserAgent配置常量
public static final String DEFAULT_USER_AGENT = "false";
@@ -134,6 +135,17 @@ public class ConfigConstants {
// 20. 重定向启用配置常量
public static final String DEFAULT_ENABLE_REDIRECT = "true";
// 22. 异步定时
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
// 23. 其他配置常量
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
public static final String DEFAULT_IS_JAVASCRIPT = "false";
public static final String DEFAULT_XLSX_ALLOW_EDIT = "false";
public static final String DEFAULT_XLSX_SHOW_TOOLBAR = "false";
public static final String DEFAULT_IS_SHOW_KEY= "false";
public static final String DEFAULT_SCRIPT_JS ="false" ;
// ==================================================
// 配置变量定义区(按功能分类,均为静态变量)
// ==================================================
@@ -254,6 +266,18 @@ public class ConfigConstants {
// 21. 重定向启用配置
private static Boolean enableRedirect;
// 22. 异步定时
private static int refreshSchedule;
// 23. 其他配置变量
private static boolean isShowaesKey;
private static boolean isJavaScript;
private static boolean xlsxAllowEdit;
private static boolean xlsxShowtoolbar;
private static boolean isShowKey;
private static boolean scriptJs;
// ==================================================
// 获取方法(按功能分类)
@@ -579,6 +603,36 @@ public class ConfigConstants {
return enableRedirect;
}
// 22. 异步定时刷新时间
public static int getTime() {
return 0;
}
// 23. 其他配置获取方法
public static boolean getisShowaesKey() {
return isShowaesKey;
}
public static boolean getisJavaScript() {
return isJavaScript;
}
public static boolean getxlsxAllowEdit() {
return xlsxAllowEdit;
}
public static boolean getxlsxShowtoolbar() {
return xlsxShowtoolbar;
}
public static boolean getisShowKey() {
return isShowKey;
}
public static boolean getscriptJs() {
return scriptJs;
}
// ==================================================
// Setter方法按功能分类
// ==================================================
@@ -1036,7 +1090,7 @@ public class ConfigConstants {
}
// 12. 权限配置Setter方法
@Value("${kk.Key:}")
@Value("${kk.key:false}")
public void setKey(String key) {
setKeyValue(key);
}
@@ -1072,7 +1126,7 @@ public class ConfigConstants {
ConfigConstants.addTask = addTask;
}
@Value("${ase.key:1234567890123456}")
@Value("${aes.key:1234567890123456}")
public void setaesKey(String aesKey) {
setaesKeyValue(aesKey);
}
@@ -1305,4 +1359,69 @@ public class ConfigConstants {
public static void setEnableRedirectValue(Boolean enableRedirect) {
ConfigConstants.enableRedirect = enableRedirect;
}
// 22 异步定时刷新时间
@Value("${kk.refreshSchedule:5}")
public void setRefreshSchedule(int refreshSchedule) {
setRefreshScheduleValue(refreshSchedule);
}
public static void setRefreshScheduleValue(int refreshSchedule) {
ConfigConstants.refreshSchedule = refreshSchedule;
}
// 23. 其他配置Setter方法
@Value("${kk.isshowaeskey:false}")
public void setIsShowaesKey(String isShowaesKey) {
setIsShowaesKeyValue(Boolean.parseBoolean(isShowaesKey));
}
public static void setIsShowaesKeyValue(boolean isShowaesKey) {
ConfigConstants.isShowaesKey = isShowaesKey;
}
@Value("${kk.isjavascript:false}")
public void setIsJavaScript(String isJavaScript) {
setIsJavaScriptValue(Boolean.parseBoolean(isJavaScript));
}
public static void setIsJavaScriptValue(boolean isJavaScript) {
ConfigConstants.isJavaScript = isJavaScript;
}
@Value("${kk.xlsxallowedit:false}")
public void setXlsxAllowEdit(String xlsxAllowEdit) {
setXlsxAllowEditValue(Boolean.parseBoolean(xlsxAllowEdit));
}
public static void setXlsxAllowEditValue(boolean xlsxAllowEdit) {
ConfigConstants.xlsxAllowEdit = xlsxAllowEdit;
}
@Value("${kk.xlsxshowtoolbar:false}")
public void setXlsxShowtoolbar(String xlsxShowtoolbar) {
setXlsxShowtoolbarValue(Boolean.parseBoolean(xlsxShowtoolbar));
}
public static void setXlsxShowtoolbarValue(boolean xlsxShowtoolbar) {
ConfigConstants.xlsxShowtoolbar = xlsxShowtoolbar;
}
@Value("${kk.isshowkey:false}")
public void setisShowKey(String isShowKey) {
setisShowKeyValue(Boolean.parseBoolean(isShowKey));
}
public static void setisShowKeyValue(boolean isShowKey) {
ConfigConstants.isShowKey = isShowKey;
}
@Value("${kk.scriptjs:false}")
public void setscriptJs(String scriptJs) {
setscriptJsValue(Boolean.parseBoolean(scriptJs));
}
public static void setscriptJsValue(boolean scriptJs) {
ConfigConstants.scriptJs = scriptJs;
}
}

View File

@@ -251,11 +251,11 @@ public class ConfigRefreshComponent {
String homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
// 12. 权限配置
String key = properties.getProperty("kk.Key", ConfigConstants.DEFAULT_KEY);
String key = properties.getProperty("kk.key=", ConfigConstants.DEFAULT_KEY);
boolean picturesPreview = Boolean.parseBoolean(properties.getProperty("kk.Picturespreview", ConfigConstants.DEFAULT_PICTURES_PREVIEW));
boolean getCorsFile = Boolean.parseBoolean(properties.getProperty("kk.Getcorsfile", ConfigConstants.DEFAULT_GET_CORS_FILE));
boolean addTask = Boolean.parseBoolean(properties.getProperty("kk.addTask", ConfigConstants.DEFAULT_ADD_TASK));
String aesKey = properties.getProperty("ase.key", ConfigConstants.DEFAULT_AES_KEY);
String aesKey = properties.getProperty("aes.key", ConfigConstants.DEFAULT_AES_KEY);
// 13. UserAgent配置
String userAgent = properties.getProperty("useragent", ConfigConstants.DEFAULT_USER_AGENT);
@@ -299,6 +299,17 @@ public class ConfigRefreshComponent {
// 21. 重定向启用配置
boolean enableRedirect = Boolean.parseBoolean(properties.getProperty("kk.enable.redirect", ConfigConstants.DEFAULT_ENABLE_REDIRECT));
// 22. 重定向启用配置
int refreshSchedule = Integer.parseInt(properties.getProperty("kk.refreshSchedule ", ConfigConstants.DEFAULT_ENABLE_REFRECSHSCHEDULE).trim());
// 23. 其他配置
boolean isShowaesKey = Boolean.parseBoolean(properties.getProperty("kk.isshowaeskey", ConfigConstants.DEFAULT_SHOW_AES_KEY));
boolean isJavaScript = Boolean.parseBoolean(properties.getProperty("kk.isjavascript", ConfigConstants.DEFAULT_IS_JAVASCRIPT));
boolean xlsxAllowEdit = Boolean.parseBoolean(properties.getProperty("kk.xlsxallowedit", ConfigConstants.DEFAULT_XLSX_ALLOW_EDIT));
boolean xlsxShowtoolbar = Boolean.parseBoolean(properties.getProperty("kk.xlsxshowtoolbar", ConfigConstants.DEFAULT_XLSX_SHOW_TOOLBAR));
boolean isShowKey = Boolean.parseBoolean(properties.getProperty("kk.isshowkey", ConfigConstants.DEFAULT_IS_SHOW_KEY));
boolean scriptJs = Boolean.parseBoolean(properties.getProperty("kk.scriptjs", ConfigConstants.DEFAULT_SCRIPT_JS));
// 设置配置值
// 1. 缓存配置
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
@@ -385,16 +396,6 @@ public class ConfigRefreshComponent {
ConfigConstants.setMediaXLFileTimeoutValue(mediaXLFileTimeout);
ConfigConstants.setMediaXXLFileTimeoutValue(mediaXXLFileTimeout);
ConfigConstants.setMediaXXXLFileTimeoutValue(mediaXXXLFileTimeout);
// 19. CAD水印配置
ConfigConstants.setCadwatermarkValue(cadwatermark);
// 20. SSL忽略配置
ConfigConstants.setIgnoreSSLValue(ignoreSSL);
// 21. 重定向启用配置
ConfigConstants.setEnableRedirectValue(enableRedirect);
// 16. PDF DPI配置
ConfigConstants.setPdfDpiEnabledValue(pdfDpiEnabled);
ConfigConstants.setPdfSmallDpiValue(pdfSmallDpi);
@@ -411,6 +412,26 @@ public class ConfigRefreshComponent {
// 18. PDF线程配置
ConfigConstants.setPdfMaxThreadsValue(pdfMaxThreads);
// 19. CAD水印配置
ConfigConstants.setCadwatermarkValue(cadwatermark);
// 20. SSL忽略配置
ConfigConstants.setIgnoreSSLValue(ignoreSSL);
// 21. 重定向启用配置
ConfigConstants.setEnableRedirectValue(enableRedirect);
// 22. 重定向启用配置
ConfigConstants.setRefreshScheduleValue(refreshSchedule);
// 23. 其他配置
ConfigConstants.setIsShowaesKeyValue(isShowaesKey);
ConfigConstants.setIsJavaScriptValue(isJavaScript);
ConfigConstants.setXlsxAllowEditValue(xlsxAllowEdit);
ConfigConstants.setXlsxShowtoolbarValue(xlsxShowtoolbar);
ConfigConstants.setisShowKeyValue(isShowKey);
ConfigConstants.setscriptJsValue(scriptJs);
}
/**

View File

@@ -56,44 +56,44 @@ public class CadToPdfService {
* @param fileAttribute 文件属性
* @return 转换结果的CompletableFuture
*/
public CompletableFuture<Boolean> cadToPdfAsync(String inputFilePath, String outputFilePath,
public CompletableFuture<Boolean> cadToPdfAsync(String inputFilePath, String outputFilePath,String cacheName,
String cadPreviewType, FileAttribute fileAttribute) {
String fileName = new File(inputFilePath).getName();
// 立即创建初始状态,防止重复执行
FileConvertStatusManager.startConvert(fileName);
FileConvertStatusManager.startConvert(cacheName);
// 创建可取消的任务
CompletableFuture<Boolean> taskFuture = new CompletableFuture<>();
taskCompletionStatus.put(fileName, new AtomicBoolean(false));
taskCompletionStatus.put(cacheName, new AtomicBoolean(false));
// 提交任务到线程池
Future<?> future = virtualThreadExecutor.submit(() -> {
try {
// 添加初始状态更新
FileConvertStatusManager.updateProgress(fileName, "正在启动转换任务", 5);
boolean result = convertCadWithConcurrencyControl(inputFilePath, outputFilePath,
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 5);
boolean result = convertCadWithConcurrencyControl(inputFilePath, outputFilePath,cacheName,
cadPreviewType, fileAttribute);
if (result) {
taskFuture.complete(true);
taskCompletionStatus.get(fileName).set(true);
taskCompletionStatus.get(cacheName).set(true);
} else {
taskFuture.complete(false);
}
} catch (Exception e) {
logger.error("CAD转换任务执行失败: {}", fileName, e);
FileConvertStatusManager.markError(fileName, "转换过程异常: " + e.getMessage());
logger.error("CAD转换任务执行失败: {}", cacheName, e);
FileConvertStatusManager.markError(cacheName, "转换过程异常: " + e.getMessage());
taskFuture.completeExceptionally(e);
} finally {
// 移除任务记录
runningTasks.remove(fileName);
taskCompletionStatus.remove(fileName);
runningTasks.remove(cacheName);
taskCompletionStatus.remove(cacheName);
}
});
// 记录正在运行的任务
runningTasks.put(fileName, future);
runningTasks.put(cacheName, future);
// 设置超时取消
scheduleTimeoutCheck(fileName, taskFuture, future, outputFilePath);
scheduleTimeoutCheck(cacheName, taskFuture, future, outputFilePath);
return taskFuture;
}
@@ -160,34 +160,34 @@ public class CadToPdfService {
/**
* 带并发控制的CAD转换
*/
private boolean convertCadWithConcurrencyControl(String inputFilePath, String outputFilePath,
private boolean convertCadWithConcurrencyControl(String inputFilePath, String outputFilePath,String cacheName,
String cadPreviewType, FileAttribute fileAttribute)
throws Exception {
String fileName = new File(inputFilePath).getName();
long acquireStartTime = System.currentTimeMillis();
// 获取并发许可
if (!concurrentLimit.tryAcquire(30, TimeUnit.SECONDS)) {
long acquireTime = System.currentTimeMillis() - acquireStartTime;
logger.warn("获取并发许可超时,文件: {}, 等待时间: {}ms", fileName, acquireTime);
FileConvertStatusManager.updateProgress(fileName, "系统繁忙,等待资源中...", 15);
logger.warn("获取并发许可超时,文件: {}, 等待时间: {}ms", cacheName, acquireTime);
FileConvertStatusManager.updateProgress(cacheName, "系统繁忙,等待资源中...", 15);
throw new TimeoutException("系统繁忙,请稍后重试");
}
long acquireTime = System.currentTimeMillis() - acquireStartTime;
logger.debug("获取并发许可成功: {}, 等待时间: {}ms", fileName, acquireTime);
logger.debug("获取并发许可成功: {}, 等待时间: {}ms", cacheName, acquireTime);
// 更新状态
FileConvertStatusManager.updateProgress(fileName, "已获取转换资源,开始转换", 20);
FileConvertStatusManager.updateProgress(cacheName, "已获取转换资源,开始转换", 20);
long conversionStartTime = System.currentTimeMillis();
try {
boolean result = performCadConversion(inputFilePath, outputFilePath, cadPreviewType, fileAttribute);
boolean result = performCadConversion(inputFilePath, outputFilePath,cacheName, cadPreviewType, fileAttribute);
long conversionTime = System.currentTimeMillis() - conversionStartTime;
logger.debug("CAD转换核心完成: {}, 转换耗时: {}ms, 总耗时(含等待): {}ms",
fileName, conversionTime, conversionTime + acquireTime);
cacheName, conversionTime, conversionTime + acquireTime);
return result;
@@ -199,49 +199,48 @@ public class CadToPdfService {
/**
* 执行实际的CAD转换逻辑
*/
private boolean performCadConversion(String inputFilePath, String outputFilePath,
private boolean performCadConversion(String inputFilePath, String outputFilePath,String cacheName,
String cadPreviewType, FileAttribute fileAttribute) {
final InterruptionTokenSource source = new InterruptionTokenSource();
String fileName = new File(inputFilePath).getName();
long totalStartTime = System.currentTimeMillis();
try {
// 1. 验证输入参数
long validationStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在验证文件参数", 25);
FileConvertStatusManager.updateProgress(cacheName, "正在验证文件参数", 25);
if (!validateInputParameters(inputFilePath, outputFilePath, cadPreviewType)) {
long validationTime = System.currentTimeMillis() - validationStartTime;
logger.error("CAD转换参数验证失败: {}, 验证耗时: {}ms", fileName, validationTime);
FileConvertStatusManager.markError(fileName, "文件参数验证失败");
logger.error("CAD转换参数验证失败: {}, 验证耗时: {}ms", cacheName, validationTime);
FileConvertStatusManager.markError(cacheName, "文件参数验证失败");
return false;
}
long validationTime = System.currentTimeMillis() - validationStartTime;
// 2. 创建输出目录
long directoryStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在准备输出目录", 30);
FileConvertStatusManager.updateProgress(cacheName, "正在准备输出目录", 30);
createOutputDirectoryIfNeeded(outputFilePath, fileAttribute.isCompressFile());
long directoryTime = System.currentTimeMillis() - directoryStartTime;
// 3. 加载并转换CAD文件
long loadStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在加载CAD文件", 40);
FileConvertStatusManager.updateProgress(cacheName, "正在加载CAD文件", 40);
LoadOptions loadOptions = createLoadOptions();
try (Image cadImage = Image.load(inputFilePath, loadOptions)) {
long loadTime = System.currentTimeMillis() - loadStartTime;
logger.debug("CAD文件加载完成: {}, 加载耗时: {}ms", fileName, loadTime);
logger.debug("CAD文件加载完成: {}, 加载耗时: {}ms", cacheName, loadTime);
FileConvertStatusManager.updateProgress(fileName, "CAD文件加载完成开始渲染", 50);
FileConvertStatusManager.updateProgress(cacheName, "CAD文件加载完成开始渲染", 50);
// 4. 创建光栅化选项
long rasterizationStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在设置渲染参数", 60);
FileConvertStatusManager.updateProgress(cacheName, "正在设置渲染参数", 60);
CadRasterizationOptions rasterizationOptions = createRasterizationOptions(cadImage);
long rasterizationTime = System.currentTimeMillis() - rasterizationStartTime;
// 5. 根据预览类型创建选项
long optionsStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在配置输出格式", 70);
FileConvertStatusManager.updateProgress(cacheName, "正在配置输出格式", 70);
var options = switch (cadPreviewType.toLowerCase()) {
case "svg" -> createSvgOptions(rasterizationOptions, source);
case "pdf" -> createPdfOptions(rasterizationOptions, source);
@@ -251,15 +250,15 @@ public class CadToPdfService {
long optionsTime = System.currentTimeMillis() - optionsStartTime;
// 6. 保存转换结果
long saveStartTime = System.currentTimeMillis();
FileConvertStatusManager.updateProgress(fileName, "正在生成输出文件", 80);
FileConvertStatusManager.updateProgress(cacheName, "正在生成输出文件", 80);
saveConvertedFile(outputFilePath, cadImage, options);
long saveTime = System.currentTimeMillis() - saveStartTime;
FileConvertStatusManager.updateProgress(fileName, "文件转换完成", 90);
FileConvertStatusManager.updateProgress(cacheName, "文件转换完成", 90);
// 计算总时间
long totalTime = System.currentTimeMillis() - totalStartTime;
// 记录详细的性能信息
logger.debug("CAD转换详细耗时 - 文件: {}, 验证={}ms, 目录={}ms, 加载={}ms, 光栅化={}ms, 选项={}ms, 保存={}ms, 总耗时={}ms",
fileName, validationTime, directoryTime, loadTime,
cacheName, validationTime, directoryTime, loadTime,
rasterizationTime, optionsTime, saveTime, totalTime);
logger.info("CAD转换完成: 总耗时: {}ms", totalTime);
@@ -272,9 +271,9 @@ public class CadToPdfService {
}
// 转换成功,标记为完成
FileConvertStatusManager.updateProgress(fileName, "转换成功", 100);
FileConvertStatusManager.updateProgress(cacheName, "转换成功", 100);
// 短暂延迟后清理状态给前端一个显示100%的机会
FileConvertStatusManager.convertSuccess(fileName);
FileConvertStatusManager.convertSuccess(cacheName);
return true;
@@ -282,12 +281,12 @@ public class CadToPdfService {
} catch (Exception e) {
long totalTime = System.currentTimeMillis() - totalStartTime;
logger.error("CAD转换执行失败: {}, 耗时: {}ms", fileName, totalTime, e);
logger.error("CAD转换执行失败: {}, 耗时: {}ms", cacheName, totalTime, e);
// 检查是否已经标记为超时
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(fileName);
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) {
FileConvertStatusManager.markError(fileName, "转换失败: " + e.getMessage());
FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage());
}
// 删除可能已创建的不完整文件
@@ -302,7 +301,7 @@ public class CadToPdfService {
long cleanupTime = System.currentTimeMillis() - cleanupStartTime;
long totalTime = System.currentTimeMillis() - totalStartTime;
logger.debug("CAD转换资源清理完成: {}, 清理耗时: {}ms, 总耗时: {}ms",
fileName, cleanupTime, totalTime);
cacheName, cleanupTime, totalTime);
}
}
}

View File

@@ -254,10 +254,16 @@ public class FileHandlerService {
try {
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
} catch (UnsupportedEncodingException e) {
logger.error("Failed to decode file name: {}", originFileName, e);
e.printStackTrace();
}
}else {
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义
url = Objects.requireNonNull(WebUtils.encodeUrlFileName(url))
.replaceAll("\\+", "%20")
.replaceAll("%3A", ":")
.replaceAll("%2F", "/")
.replaceAll("%3F", "?")
.replaceAll("%26", "&")
.replaceAll("%3D", "=");
}
originFileName = KkFileUtils.htmlEscape(originFileName); //文件名处理
boolean isHtmlView = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx") || suffix.equalsIgnoreCase("csv") || suffix.equalsIgnoreCase("xlsm") || suffix.equalsIgnoreCase("xlt") || suffix.equalsIgnoreCase("xltm") || suffix.equalsIgnoreCase("et") || suffix.equalsIgnoreCase("ett") || suffix.equalsIgnoreCase("xlam");

View File

@@ -2,6 +2,7 @@ package cn.keking.service;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.utils.FileConvertStatusManager;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
@@ -12,7 +13,6 @@ import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -89,15 +89,17 @@ public class Mediatomp4Service {
* 异步转换方法带任务ID和超时控制
*/
public static CompletableFuture<Boolean> convertToMp4Async(
String filePath, String outFilePath, FileAttribute fileAttribute) {
String filePath, String outFilePath,String cacheName, FileAttribute fileAttribute) {
String taskId = generateTaskId(filePath);
// 立即创建初始状态,防止重复执行
FileConvertStatusManager.startConvert(cacheName);
CompletableFuture<Boolean> resultFuture = new CompletableFuture<>();
// 创建转换线程
Thread conversionThread = new Thread(() -> {
try {
boolean result = convertToMp4WithCancellation(filePath, outFilePath,
boolean result = convertToMp4WithCancellation(filePath, outFilePath,cacheName,
fileAttribute, taskId, resultFuture);
resultFuture.complete(result);
} catch (Exception e) {
@@ -120,10 +122,11 @@ public class Mediatomp4Service {
// 设置超时监控
File inputFile = new File(filePath);
long fileSizeMB = inputFile.length() / (1024 * 1024);
scheduleTimeoutMonitor(taskId, calculateTimeout(fileSizeMB));
scheduleTimeoutMonitor(taskId, calculateTimeout(fileSizeMB),cacheName);
return resultFuture;
} catch (Exception e) {
FileConvertStatusManager.markError(cacheName, "转换过程异常: " + e.getMessage());
resultFuture.completeExceptionally(e);
cleanupFailedFile(outFilePath);
return resultFuture;
@@ -134,25 +137,18 @@ public class Mediatomp4Service {
* 带取消支持的同步转换方法(核心改进)
*/
private static boolean convertToMp4WithCancellation(
String filePath, String outFilePath, FileAttribute fileAttribute,
String filePath, String outFilePath,String cacheName, FileAttribute fileAttribute,
String taskId, CompletableFuture<Boolean> resultFuture) throws Exception {
FFmpegFrameGrabber frameGrabber = null;
FFmpegFrameRecorder recorder = null;
ConversionContext context = null;
try {
File sourceFile = new File(filePath);
if (!sourceFile.exists()) {
throw new FileNotFoundException("源文件不存在: " + filePath);
}
File desFile = new File(outFilePath);
if (desFile.exists() && !fileAttribute.forceUpdatedCache()) {
logger.info("目标文件已存在,跳过转换: {}", outFilePath);
return true;
}
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 10);
// 初始化抓取器
frameGrabber = new FFmpegFrameGrabber(sourceFile);
frameGrabber.setOption("stimeout", "10000000"); // 10秒超时
@@ -168,7 +164,7 @@ public class Mediatomp4Service {
configureRecorder(recorder, frameGrabber);
recorder.start();
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 40);
// 创建任务上下文
context = new ConversionContext(frameGrabber, recorder);
@@ -180,7 +176,7 @@ public class Mediatomp4Service {
logger.info("开始转换任务 {}: {} -> {}", taskId, filePath, outFilePath);
// 核心:使用非阻塞方式读取帧
return processFramesWithTimeout(frameGrabber, recorder, context, taskId);
return processFramesWithTimeout(frameGrabber, recorder, context, taskId,cacheName);
} catch (Exception e) {
// 检查是否是取消操作
@@ -208,7 +204,7 @@ public class Mediatomp4Service {
*/
private static boolean processFramesWithTimeout(
FFmpegFrameGrabber grabber, FFmpegFrameRecorder recorder,
ConversionContext context, String taskId) throws Exception {
ConversionContext context, String taskId, String cacheName) throws Exception {
long frameCount = 0;
long startTime = System.currentTimeMillis();
@@ -227,10 +223,9 @@ public class Mediatomp4Service {
logger.warn("任务 {} 帧读取超时,可能文件损坏", taskId);
throw new TimeoutException("帧读取超时");
}
// 尝试抓取帧
frame = grabber.grabFrame();
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 60);
if (frame == null) {
consecutiveNullFrames++;
@@ -260,7 +255,7 @@ public class Mediatomp4Service {
}
continue;
}
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 80);
// 成功获取到帧,重置计数器
consecutiveNullFrames = 0;
lastFrameTime = System.currentTimeMillis();
@@ -295,12 +290,12 @@ public class Mediatomp4Service {
// 完成录制
recorder.stop();
recorder.close();
FileConvertStatusManager.updateProgress(cacheName, "正在启动转换任务", 100);
long totalTime = System.currentTimeMillis() - startTime;
double fps = totalTime > 0 ? (frameCount * 1000.0) / totalTime : 0;
logger.info("任务 {} 转换完成: {} 帧, 耗时: {}ms, 平均速度: {} fps",
taskId, frameCount, totalTime, String.format("%.2f", fps));
FileConvertStatusManager.convertSuccess(cacheName);
return true;
} catch (Exception e) {
@@ -404,11 +399,12 @@ public class Mediatomp4Service {
/**
* 配置超时监控
*/
private static void scheduleTimeoutMonitor(String taskId, long timeoutSeconds) {
private static void scheduleTimeoutMonitor(String taskId, long timeoutSeconds,String cacheName) {
ScheduledFuture<?> timeoutFuture = monitorExecutor.schedule(() -> {
ConversionTask task = activeTasks.get(taskId);
if (task != null && !task.context.completed) {
logger.warn("任务 {} 超时 ({}秒),开始强制终止", taskId, timeoutSeconds);
FileConvertStatusManager.markTimeout(cacheName);
cancelConversion(taskId);
task.future.completeExceptionally(
new TimeoutException("转换超时: " + timeoutSeconds + "")
@@ -471,16 +467,6 @@ public class Mediatomp4Service {
throw new FileNotFoundException("源文件不存在: " + filePath);
}
File desFile = new File(outFilePath);
if (desFile.exists()) {
if (fileAttribute.forceUpdatedCache()) {
if (!desFile.delete()) {
throw new IOException("无法删除已存在的文件: " + outFilePath);
}
} else {
throw new IllegalStateException("目标文件已存在,跳过转换");
}
}
}
/**

View File

@@ -79,29 +79,20 @@ public class OfficeToPdfService {
// 格式化显示耗时(支持不同时间单位)
String durationFormatted;
if (duration.toMinutes() > 0) {
durationFormatted = String.format("%d分%d秒",
duration.toMinutes(),
duration.toSecondsPart());
durationFormatted = String.format("%d分%d秒", duration.toMinutes(), duration.toSecondsPart());
} else if (duration.toSeconds() > 0) {
durationFormatted = String.format("%d.%03d秒",
duration.toSeconds(),
duration.toMillisPart());
durationFormatted = String.format("%d.%03d秒",duration.toSeconds(), duration.toMillisPart());
} else {
durationFormatted = String.format("%d毫秒", duration.toMillis());
}
logger.info("文件转换成功:{} -> {},耗时:{}",
inputFile.getName(),
outputFile.getName(),
durationFormatted);
inputFile.getName(),outputFile.getName(), durationFormatted);
} catch (OfficeException e) {
Instant endTime = Instant.now();
Duration duration = Duration.between(startTime, endTime);
logger.error("文件转换失败:{},已耗时:{}毫秒,错误信息:{}",
inputFile.getName(),
duration.toMillis(),
e.getMessage());
logger.error("文件转换失败:{},已耗时:{}毫秒,错误信息:{}", inputFile.getName(), duration.toMillis(), e.getMessage());
throw e;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package cn.keking.service;
import cn.keking.config.ConfigConstants;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
import jakarta.annotation.PostConstruct;
@@ -60,14 +61,13 @@ public class TifToPdfService {
/**
* TIF转JPG - 虚拟线程版本
*/
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,String fileName,
String cacheName,
boolean forceUpdatedCache) throws Exception {
String fileName = new File(strInputFile).getName();
Instant startTime = Instant.now();
try {
List<String> result = performTifToJpgConversionVirtual(
strInputFile, strOutputFile, forceUpdatedCache
strInputFile, strOutputFile, forceUpdatedCache,fileName,cacheName
);
Duration elapsedTime = Duration.between(startTime, Instant.now());
@@ -90,7 +90,8 @@ public class TifToPdfService {
* 虚拟线程执行TIF转JPG转换
*/
private List<String> performTifToJpgConversionVirtual(String strInputFile, String strOutputFile,
boolean forceUpdatedCache) throws Exception {
boolean forceUpdatedCache,String fileName,
String cacheName) throws Exception {
Instant totalStart = Instant.now();
String baseUrl = BaseUrlFilter.getBaseUrl();
@@ -105,7 +106,7 @@ public class TifToPdfService {
if (!outputDir.exists() && !outputDir.mkdirs()) {
throw new IOException("创建目录失败: " + outputDirPath);
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30);
// 加载所有图片
List<BufferedImage> images;
try {
@@ -121,12 +122,12 @@ public class TifToPdfService {
logger.warn("TIF文件没有可转换的页面: {}", strInputFile);
return Collections.emptyList();
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 50);
List<String> result = convertPagesVirtualThreads(images, outputDirPath, baseUrl, forceUpdatedCache);
Duration totalTime = Duration.between(totalStart, Instant.now());
logger.info("TIF转换PNG完成总页数: {}, 总耗时: {}ms", pageCount, totalTime.toMillis());
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 100);
return result;
}
@@ -211,9 +212,9 @@ public class TifToPdfService {
/**
* TIF转PDF - 虚拟线程版本
*/
public void convertTif2Pdf(String strJpgFile, String strPdfFile,
public void convertTif2Pdf(String strJpgFile, String strPdfFile,String fileName,
String cacheName,
boolean forceUpdatedCache) throws Exception {
String fileName = new File(strJpgFile).getName();
Instant startTime = Instant.now();
try {
@@ -224,8 +225,8 @@ public class TifToPdfService {
logger.info("PDF文件已存在跳过转换: {}", strPdfFile);
return;
}
boolean result = performTifToPdfConversionVirtual(strJpgFile, strPdfFile);
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30);
boolean result = performTifToPdfConversionVirtual(strJpgFile, strPdfFile,fileName,cacheName);
Duration elapsedTime = Duration.between(startTime, Instant.now());
logger.info("TIF转PDF{} - 文件: {}, 耗时: {}ms",
@@ -244,7 +245,7 @@ public class TifToPdfService {
/**
* 虚拟线程执行TIF转PDF转换保持顺序
*/
private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile) throws Exception {
private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile,String fileName,String cacheName) throws Exception {
Instant totalStart = Instant.now();
File tiffFile = new File(strJpgFile);
@@ -257,14 +258,14 @@ public class TifToPdfService {
logger.warn("TIFF文件没有可转换的页面: {}", strJpgFile);
return false;
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30);
int pageCount = images.size();
AtomicInteger processedCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
// 创建页面处理结果的列表
List<CompletableFuture<ProcessedPageResult>> futures = new ArrayList<>(pageCount);
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 50);
// 为每个页面创建处理任务
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
final int currentPageIndex = pageIndex;
@@ -286,7 +287,7 @@ public class TifToPdfService {
futures.add(future);
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 70);
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
@@ -332,7 +333,7 @@ public class TifToPdfService {
errorCount.incrementAndGet();
}
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 100);
// 保存PDF
document.save(strPdfFile);

View File

@@ -55,7 +55,6 @@ public class CadFilePreviewImpl implements FilePreview {
// 预览Type参数传了就取参数的没传取系统默认
String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
String baseUrl = BaseUrlFilter.getBaseUrl();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileName = fileAttribute.getName();
String cadPreviewType = ConfigConstants.getCadPreviewType();
@@ -63,20 +62,9 @@ public class CadFilePreviewImpl implements FilePreview {
String outFilePath = fileAttribute.getOutFilePath();
// 查询转换状态
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(fileName);
if (status != null) {
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
// 正在转换中,返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", status.getRealTimeMessage());
return WAITING_FILE_PREVIEW_PAGE;
} else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) {
// 超时状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
} else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) {
// 失败状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换");
}
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
@@ -117,6 +105,7 @@ public class CadFilePreviewImpl implements FilePreview {
CompletableFuture<Boolean> conversionFuture = cadtopdfservice.cadToPdfAsync(
filePath,
outFilePath,
cacheName,
ConfigConstants.getCadPreviewType(),
fileAttribute
);
@@ -166,11 +155,8 @@ public class CadFilePreviewImpl implements FilePreview {
if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType,
cacheName, outFilePath, fileHandlerService,
OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath);
}
model.addAttribute("pdfUrl", cacheName);
return PDF_FILE_PREVIEW_PAGE;
}

View File

@@ -21,6 +21,11 @@ public class CodeFilePreviewImpl implements FilePreview {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
filePreviewHandle.filePreviewHandle(url, model, fileAttribute);
String suffix = fileAttribute.getSuffix();
if(suffix.equalsIgnoreCase("htm") || suffix.equalsIgnoreCase("html") || suffix.equalsIgnoreCase("shtml") ){
model.addAttribute("pdfUrl", url);
return TXT_FILE_PREVIEW_PAGE; //直接输出html
}
return CODE_FILE_PREVIEW_PAGE;
}
}

View File

@@ -8,23 +8,23 @@ import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.Mediatomp4Service;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author : kl
* @authorboke : kailing.pub
* @create : 2018-03-25 上午11:58
* @description:
* @description: 异步视频文件预览实现
**/
@Service
public class MediaFilePreviewImpl implements FilePreview {
@@ -32,10 +32,20 @@ public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private final Mediatomp4Service mediatomp4Service;
private final OfficeFilePreviewImpl officefilepreviewimpl;
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public MediaFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
Mediatomp4Service mediatomp4Service,
OfficeFilePreviewImpl officefilepreviewimpl) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.mediatomp4Service = mediatomp4Service;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@Override
@@ -46,69 +56,61 @@ public class MediaFilePreviewImpl implements FilePreview {
String outFilePath = fileAttribute.getOutFilePath();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
FileType type = fileAttribute.getType();
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
// 检查是否是需要转换的视频格式
boolean mediaTypes = false;
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES;
for (String temp : mediaTypesConvert) {
if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true;
break;
}
}
// 查询转换状态
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
// 非HTTP协议或需要转换的文件
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) {
// 检查缓存
File outputFile = new File(outFilePath);
if (outputFile.exists() && !forceUpdatedCache && ConfigConstants.isCacheEnabled()) {
String relativePath = fileHandlerService.getRelativePath(outFilePath);
if (fileHandlerService.listConvertedFiles().containsKey(cacheName)) {
if (fileHandlerService.listConvertedMedias().containsKey(cacheName)) {
model.addAttribute("mediaUrl", relativePath);
logger.info("使用已缓存的视频文件: {}", cacheName);
return MEDIA_FILE_PREVIEW_PAGE;
}
}
// 下载文件
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
try {
if (mediaTypes) {
// 检查文件大小限制
if (isFileSizeExceeded(filePath)) {
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
}
// 使用改进的转换方法
String convertedPath = convertVideoWithImprovedTimeout(filePath, outFilePath, fileAttribute);
if (convertedPath != null) {
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(convertedPath));
// 缓存转换结果
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(convertedPath));
}
return MEDIA_FILE_PREVIEW_PAGE;
} else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换失败,请联系管理员");
}
} else {
// 不需要转换的文件
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
return MEDIA_FILE_PREVIEW_PAGE;
if (mediaTypes) {
// 检查文件大小限制
if (isFileSizeExceeded(filePath)) {
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
}
} catch (Exception e) {
logger.error("处理媒体文件失败: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频处理异常: " + getErrorMessage(e));
try {
// 启动异步转换,并添加回调处理
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", "视频文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to start video conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
}
} else {
// 不需要转换的文件,直接返回
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
return MEDIA_FILE_PREVIEW_PAGE;
}
}
// HTTP协议的媒体文件直接播放
if (type.equals(FileType.MEDIA)) {
model.addAttribute("mediaUrl", url);
@@ -118,6 +120,45 @@ public class MediaFilePreviewImpl implements FilePreview {
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
}
/**
* 启动异步转换,并在转换完成后处理后续操作
*/
private void startAsyncConversion(String filePath, String outFilePath,
String cacheName, FileAttribute fileAttribute) {
// 启动异步转换
CompletableFuture<Boolean> conversionFuture = mediatomp4Service.convertToMp4Async(
filePath,
outFilePath,
cacheName,
fileAttribute
);
// 添加转换完成后的回调
conversionFuture.whenCompleteAsync((success, throwable) -> {
if (success != null && success) {
try {
// 1. 是否保留源文件(只在转换成功后才删除)
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
// 2. 加入视频缓存(只在转换成功后才添加)
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedMedias(cacheName,
fileHandlerService.getRelativePath(outFilePath));
}
} catch (Exception e) {
logger.error("视频转换后续处理失败: {}", filePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("视频转换失败,保留源文件: {}", filePath);
if (throwable != null) {
logger.error("视频转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 检查文件大小是否超过限制
*/
@@ -139,99 +180,14 @@ public class MediaFilePreviewImpl implements FilePreview {
return false;
}
/**
* 改进的转换方法
*/
private String convertVideoWithImprovedTimeout(String filePath, String outFilePath,
FileAttribute fileAttribute) {
try {
// 检查文件是否存在
File outputFile = new File(outFilePath);
if (outputFile.exists() && !fileAttribute.forceUpdatedCache()) {
logger.info("输出文件已存在且非强制更新模式,跳过转换!");
return outFilePath;
}
// 使用改进的异步转换方法
CompletableFuture<Boolean> future =
Mediatomp4Service.convertToMp4Async(filePath, outFilePath, fileAttribute);
// 计算超时时间
File inputFile = new File(filePath);
long fileSizeMB = inputFile.length() / (1024 * 1024);
int timeoutSeconds = calculateTimeout(fileSizeMB);
try {
boolean result = future.get(timeoutSeconds, TimeUnit.SECONDS);
if (result) {
// 验证输出文件
File convertedFile = new File(outFilePath);
if (!convertedFile.exists() || convertedFile.length() == 0) {
throw new IOException("转换完成但输出文件无效");
}
return outFilePath;
} else {
throw new Exception("转换返回失败状态");
}
} catch (TimeoutException e) {
// 超时后尝试获取任务ID并取消
logger.error("视频转换超时: {}, 文件大小: {}MB, 超时: {}秒",
filePath, fileSizeMB, timeoutSeconds);
throw new RuntimeException("视频转换超时,文件可能过大");
}
} catch (Exception e) {
logger.error("视频转换异常: {}", filePath, e);
throw new RuntimeException("视频转换失败: " + getErrorMessage(e), e);
}
}
/**
* 计算超时时间 - 从配置文件读取
*/
public int calculateTimeout(long fileSizeMB) {
// 如果超时功能被禁用,返回一个非常大的值
if (!ConfigConstants.isMediaTimeoutEnabled()) {
return Integer.MAX_VALUE;
}
// 根据文件大小从配置文件读取超时时间
if (fileSizeMB < 10) return ConfigConstants.getMediaSmallFileTimeout(); // 小文件
if (fileSizeMB < 50) return ConfigConstants.getMediaMediumFileTimeout(); // 中等文件
if (fileSizeMB < 200) return ConfigConstants.getMediaLargeFileTimeout(); // 较大文件
if (fileSizeMB < 500) return ConfigConstants.getMediaXLFileTimeout(); // 大文件
if (fileSizeMB < 1024) return ConfigConstants.getMediaXXLFileTimeout(); // 超大文件
return ConfigConstants.getMediaXXXLFileTimeout(); // 极大文件
}
/**
* 检查是否需要转换
*/
private boolean checkNeedConvert(boolean mediaTypes) {
// 1.检查开关是否开启
// 检查转换开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes;
}
return false;
}
/**
* 获取友好的错误信息
*/
private String getErrorMessage(Exception e) {
if (e instanceof CancellationException) {
return "转换被取消";
} else if (e instanceof TimeoutException) {
return "转换超时";
} else if (e.getMessage() != null) {
// 截取主要错误信息
String msg = e.getMessage();
if (msg.length() > 100) {
msg = msg.substring(0, 100) + "...";
}
return msg;
}
return "未知错误";
}
}

View File

@@ -8,19 +8,23 @@ import cn.keking.service.FilePreview;
import cn.keking.service.OfficeToPdfService;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.OfficeUtils;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.jodconverter.core.office.OfficeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by kl on 2018/1/17.
@@ -29,9 +33,12 @@ import java.util.List;
@Service
public class OfficeFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(OfficeFilePreviewImpl.class);
public static final String OFFICE_PREVIEW_TYPE_IMAGE = "image";
public static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
private static final String OFFICE_PASSWORD_MSG = "password";
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
private final FileHandlerService fileHandlerService;
private final OfficeToPdfService officeToPdfService;
@@ -54,12 +61,16 @@ public class OfficeFilePreviewImpl implements FilePreview {
String suffix = fileAttribute.getSuffix(); //获取文件后缀
String fileName = fileAttribute.getName(); //获取文件原始名称
String filePassword = fileAttribute.getFilePassword(); //获取密码
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html
String cacheName = fileAttribute.getCacheName(); //转换后的文件名
String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径
// 查询转换状态
checkAndHandleConvertStatus(model, fileName, cacheName,fileAttribute);
if (!officePreviewType.equalsIgnoreCase("html")) {
if (ConfigConstants.getOfficeTypeWeb() .equalsIgnoreCase("web")) {
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
if (suffix.equalsIgnoreCase("xlsx")) {
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
return XLSX_FILE_PREVIEW_PAGE;
@@ -70,14 +81,190 @@ public class OfficeFilePreviewImpl implements FilePreview {
}
}
}
if (forceUpdatedCache|| !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
// 下载远程文件到本地,如果文件在本地已存在不会重复下载
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
// 图片预览模式(异步转换)
if (!isHtmlView && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
boolean jiami = false;
if (!ObjectUtils.isEmpty(filePassword)) {
jiami = pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath);
}
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
if (jiami) {
return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath);
}
// 下载文件
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
// 检查是否加密文件
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath);
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
// 加密文件需要密码
model.addAttribute("needFilePassword", true);
model.addAttribute("fileName", fileName);
model.addAttribute("cacheName", cacheName);
return EXEL_FILE_PREVIEW_PAGE;
}
try {
// 启动异步转换
startAsyncOfficeConversion(filePath, outFilePath, cacheName, fileAttribute, officePreviewType);
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", "文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to start Office conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换异常,请联系管理员");
}
} else {
// 如果已有缓存,直接渲染预览
return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath);
}
}
// 处理普通Office转PDF预览
return handleRegularOfficePreview(model, fileAttribute, fileName, forceUpdatedCache, cacheName, outFilePath,
isHtmlView, userToken, filePassword);
}
/**
* 启动异步Office转换
*/
private void startAsyncOfficeConversion(String filePath, String outFilePath, String cacheName,
FileAttribute fileAttribute,
String officePreviewType) {
// 启动异步转换
CompletableFuture<List<String>> conversionFuture = CompletableFuture.supplyAsync(() -> {
try {
// 更新状态
FileConvertStatusManager.startConvert(cacheName);
FileConvertStatusManager.updateProgress(cacheName, "正在启动Office转换", 20);
// 转换Office到PDF
FileConvertStatusManager.updateProgress(cacheName, "正在转换Office到jpg", 60);
officeToPdfService.openOfficeToPDF(filePath, outFilePath, fileAttribute);
if (fileAttribute.isHtmlView()) {
// 对转换后的文件进行操作(改变编码方式)
FileConvertStatusManager.updateProgress(cacheName, "处理HTML编码", 95);
fileHandlerService.doActionConvertedFile(outFilePath);
}
// 是否需要转换为图片
List<String> imageUrls = null;
if (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) {
FileConvertStatusManager.updateProgress(cacheName, "正在转换PDF为图片", 90);
imageUrls = pdftojpgservice.pdf2jpg(outFilePath, outFilePath, cacheName, fileAttribute);
}
// 缓存处理
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath);
boolean userToken = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || userToken || !isPwdProtectedOffice)) {
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
FileConvertStatusManager.updateProgress(cacheName, "转换完成", 100);
FileConvertStatusManager.convertSuccess(cacheName);
return imageUrls;
} catch (OfficeException e) {
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath);
String filePassword = fileAttribute.getFilePassword();
if (isPwdProtectedOffice && !OfficeUtils.isCompatible(filePath, filePassword)) {
FileConvertStatusManager.markError(cacheName, "文件密码错误,请重新输入");
} else {
logger.error("Office转换执行失败: {}", cacheName, e);
FileConvertStatusManager.markError(cacheName, "Office转换失败: " + e.getMessage());
}
return null;
} catch (Exception e) {
logger.error("Office转换执行失败: {}", cacheName, e);
// 检查是否已经标记为超时
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) {
FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage());
}
return null;
}
});
// 添加转换完成后的回调
conversionFuture.thenAcceptAsync(imageUrls -> {
try {
// 这里假设imageUrls不为null且不为空表示转换成功
if (imageUrls != null && !imageUrls.isEmpty()) {
// 是否保留源文件(只在转换成功后才删除)
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
}
} catch (Exception e) {
logger.error("Office转换后续处理失败: {}", filePath, e);
}
}, callbackExecutor);
}
/**
* 获取预览类型(图片预览)
*/
String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType,
String cacheName, String outFilePath) {
String suffix = fileAttribute.getSuffix();
boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx");
List<String> imageUrls;
try {
if (pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath)) {
imageUrls = pdftojpgservice.getEncryptedPdfCache(outFilePath);
} else {
imageUrls = fileHandlerService.loadPdf2jpgCache(outFilePath);
}
if (imageUrls == null || imageUrls.isEmpty()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "Office转换缓存异常请联系管理员");
}
model.addAttribute("imgUrls", imageUrls);
model.addAttribute("currentUrl", imageUrls.getFirst());
if (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) {
// PPT 图片模式使用专用预览页面
return (isPPT ? PPT_FILE_PREVIEW_PAGE : OFFICE_PICTURE_FILE_PREVIEW_PAGE);
} else {
return PICTURE_FILE_PREVIEW_PAGE;
}
} catch (Exception e) {
logger.error("渲染Office预览页面失败: {}", cacheName, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "渲染预览页面异常,请联系管理员");
}
}
/**
* 处理普通Office预览转PDF
*/
private String handleRegularOfficePreview(Model model, FileAttribute fileAttribute,
String fileName, boolean forceUpdatedCache, String cacheName,
String outFilePath, boolean isHtmlView, boolean userToken,
String filePassword) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
// 下载远程文件到本地,如果文件在本地已存在不会重复下载
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
// 加密文件需要密码
model.addAttribute("needFilePassword", true);
@@ -109,43 +296,33 @@ public class OfficeFilePreviewImpl implements FilePreview {
}
}
}
}
}
if (!isHtmlView && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath, fileHandlerService, OFFICE_PREVIEW_TYPE_IMAGE, otherFilePreview);
}
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); //输出转义文件名 方便url识别
return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE;
}
String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, String pdfName, String outFilePath, FileHandlerService fileHandlerService, String officePreviewTypeImage, OtherFilePreviewImpl otherFilePreview) {
String suffix = fileAttribute.getSuffix();
boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx");
List<String> imageUrls = null;
try {
imageUrls = pdftojpgservice.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute);
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(OFFICE_PASSWORD_MSG)) {
model.addAttribute("needFilePassword", true);
return EXEL_FILE_PREVIEW_PAGE;
}
}
/**
* 异步方法
*/
public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute){
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
int refreshSchedule = ConfigConstants.getTime();
if (status != null) {
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
// 正在转换中,返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
model.addAttribute("message", status.getRealTimeMessage());
return WAITING_FILE_PREVIEW_PAGE;
} else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) {
// 超时状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
} else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) {
// 失败状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换失败,无法继续转换");
}
}
if (imageUrls == null || imageUrls.size() < 1) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "office转图片异常请联系管理员");
}
model.addAttribute("imgUrls", imageUrls);
model.addAttribute("currentUrl", imageUrls.get(0));
if (officePreviewTypeImage.equals(officePreviewType)) {
// PPT 图片模式使用专用预览页面
return (isPPT ? PPT_FILE_PREVIEW_PAGE : OFFICE_PICTURE_FILE_PREVIEW_PAGE);
} else {
return PICTURE_FILE_PREVIEW_PAGE;
}
return null;
}
}
}

View File

@@ -7,14 +7,23 @@ import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Created by kl on 2018/1/17.
@@ -23,80 +32,263 @@ import java.util.List;
@Service
public class PdfFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(PdfFilePreviewImpl.class);
private static final String PDF_PASSWORD_MSG = "password";
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private final PdfToJpgService pdftojpgservice;
private static final String PDF_PASSWORD_MSG = "password";
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) {
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public PdfFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
PdfToJpgService pdftojpgservice) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String pdfName = fileAttribute.getName(); //获取原始文件名
String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径
String originFilePath = fileAttribute.getOriginFilePath(); //原始文件路径
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) {
//当文件不存在时,就去下载
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
String originFilePath; //原始文件路径
String cacheName = fileAttribute.getCacheName();
String filePassword = fileAttribute.getFilePassword(); // 获取密码
int refreshSchedule = ConfigConstants.getTime();
// 查询转换状态
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
if (status != null) {
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
// 正在转换中,返回等待页面
model.addAttribute("fileName", pdfName);
model.addAttribute("time", refreshSchedule);
model.addAttribute("message", status.getRealTimeMessage());
return WAITING_FILE_PREVIEW_PAGE;
} else if (status.getStatus() == FileConvertStatusManager.Status.TIMEOUT) {
// 超时状态,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
}
}
boolean jiami=false;
if(!ObjectUtils.isEmpty(filePassword)){
jiami=pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath);
}
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) {
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
if(jiami){
return renderPreview(model, cacheName, outFilePath,
officePreviewType, pdfName, fileAttribute);
}
// 当文件不存在时,就去下载
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
originFilePath = response.getContent();
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(originFilePath));
// 检查文件是否需要密码,但不启动转换
if (filePassword == null || filePassword.trim().isEmpty()) {
// 没有提供密码,先检查文件是否需要密码
if (checkIfPdfNeedsPassword(originFilePath, cacheName, pdfName)) {
model.addAttribute("needFilePassword", true);
model.addAttribute("fileName", pdfName);
model.addAttribute("cacheName", cacheName);
return EXEL_FILE_PREVIEW_PAGE;
}
}
try {
// 启动异步转换
startAsyncPdfConversion(originFilePath, outFilePath, cacheName, pdfName, fileAttribute);
// 返回等待页面
model.addAttribute("fileName", pdfName);
model.addAttribute("message", "文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to start PDF conversion: {}", originFilePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "PDF转换异常请联系管理员");
}
} else {
// 如果已有缓存,直接渲染预览
return renderPreview(model, cacheName, outFilePath,
officePreviewType, pdfName, fileAttribute);
}
List<String> imageUrls;
try {
imageUrls = pdftojpgservice.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute);
} else {
// 处理普通PDF预览非图片转换
return handleRegularPdfPreview(url, model, fileAttribute, pdfName, forceUpdatedCache, outFilePath);
}
}
/**
* 检查PDF文件是否需要密码不进行实际转换
*/
private boolean checkIfPdfNeedsPassword(String originFilePath, String cacheName, String pdfName) {
try {
// 尝试用空密码加载PDF检查是否需要密码
File pdfFile = new File(originFilePath);
if (!pdfFile.exists()) {
return false;
}
// 使用try-with-resources确保资源释放
try (org.apache.pdfbox.pdmodel.PDDocument tempDoc = org.apache.pdfbox.Loader.loadPDF(pdfFile, "")) {
// 如果能加载成功,说明不需要密码
int pageCount = tempDoc.getNumberOfPages();
logger.info("PDF文件不需要密码总页数: {},文件: {}", pageCount, originFilePath);
return false;
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
model.addAttribute("needFilePassword", true);
return EXEL_FILE_PREVIEW_PAGE;
FileConvertStatusManager.convertSuccess(cacheName);
logger.info("PDF文件需要密码: {}", originFilePath);
return true;
}
}
}
return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常请联系管理员");
logger.warn("PDF文件检查异常: {}", e.getMessage());
return false;
}
if (imageUrls == null || imageUrls.size() < 1) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常请联系管理员");
} catch (Exception e) {
logger.error("检查PDF密码状态失败: {}", originFilePath, e);
return false;
}
}
/**
* 启动异步PDF转换
*/
private void startAsyncPdfConversion(String originFilePath, String outFilePath,
String cacheName, String pdfName,
FileAttribute fileAttribute) {
// 启动异步转换
CompletableFuture<List<String>> conversionFuture = CompletableFuture.supplyAsync(() -> {
try {
// 更新状态
FileConvertStatusManager.startConvert(cacheName);
FileConvertStatusManager.updateProgress(cacheName, "正在启动PDF转换", 10);
List<String> imageUrls = pdftojpgservice.pdf2jpg(originFilePath, outFilePath,
pdfName, fileAttribute);
if (imageUrls != null && !imageUrls.isEmpty()) {
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || usePasswordCache)) {
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath));
}
FileConvertStatusManager.updateProgress(cacheName, "转换完成", 100);
// 短暂延迟后清理状态
FileConvertStatusManager.convertSuccess(cacheName);
return imageUrls;
} else {
FileConvertStatusManager.markError(cacheName, "PDF转换失败未生成图片");
return null;
}
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
// 标记为需要密码的状态
return null;
}
}
}
logger.error("PDF转换执行失败: {}", cacheName, e);
// 检查是否已经标记为超时
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) {
FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage());
}
return null;
}
});
// 添加转换完成后的回调
conversionFuture.whenCompleteAsync((imageUrls, throwable) -> {
if (imageUrls != null && !imageUrls.isEmpty()) {
try {
// 是否保留PDF源文件只在转换成功后才删除
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(originFilePath);
}
} catch (Exception e) {
logger.error("PDF转换后续处理失败: {}", originFilePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("PDF转换失败保留源文件: {}", originFilePath);
if (throwable != null) {
logger.error("转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 渲染预览页面
*/
private String renderPreview(Model model, String cacheName,
String outFilePath, String officePreviewType,
String pdfName, FileAttribute fileAttribute) {
try {
List<String> imageUrls;
if(pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath)){
imageUrls = pdftojpgservice.getEncryptedPdfCache(outFilePath);
}else {
imageUrls = fileHandlerService.loadPdf2jpgCache(outFilePath);
}
if (imageUrls == null || imageUrls.isEmpty()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "PDF转换缓存异常请联系管理员");
}
model.addAttribute("imgUrls", imageUrls);
model.addAttribute("currentUrl", imageUrls.get(0));
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) {
return OFFICE_PICTURE_FILE_PREVIEW_PAGE;
} else {
return PICTURE_FILE_PREVIEW_PAGE;
}
} else {
// 不是http开头浏览器不能直接访问需下载到本地
if (url != null && !url.toLowerCase().startsWith("http")) {
if (!fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
model.addAttribute("pdfUrl", fileHandlerService.getRelativePath(response.getContent()));
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath));
}
} else {
model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName));
} catch (Exception e) {
logger.error("渲染PDF预览页面失败: {}", cacheName, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "渲染预览页面异常,请联系管理员");
}
}
/**
* 处理普通PDF预览非图片转换
*/
private String handleRegularPdfPreview(String url, Model model, FileAttribute fileAttribute,
String pdfName, boolean forceUpdatedCache,
String outFilePath) {
// 不是http开头浏览器不能直接访问需下载到本地
if (url != null && !url.toLowerCase().startsWith("http")) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
model.addAttribute("pdfUrl", fileHandlerService.getRelativePath(response.getContent()));
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath));
}
} else {
model.addAttribute("pdfUrl", url);
model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName));
}
} else {
model.addAttribute("pdfUrl", url);
}
return PDF_FILE_PREVIEW_PAGE;
}
}
}

View File

@@ -77,7 +77,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
return null;
}
if (!file.exists() || file.length() == 0) {
return "";
return "KK提醒您文件不存在或者已经被删除了!";
} else {
String charset = EncodingDetects.getJavaEncode(filePath);
if ("ASCII".equals(charset)) {

View File

@@ -7,12 +7,18 @@ import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.TifToPdfService;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* tiff 图片文件处理
@@ -22,21 +28,37 @@ import java.util.List;
@Service
public class TiffFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(TiffFilePreviewImpl.class);
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private final TifToPdfService tiftoservice;
public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview,TifToPdfService tiftoservice) {
private final OfficeFilePreviewImpl officefilepreviewimpl;
public TiffFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, TifToPdfService tiftoservice, OfficeFilePreviewImpl officefilepreviewimpl) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.tiftoservice = tiftoservice;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String fileName = fileAttribute.getName();
String tifPreviewType = ConfigConstants.getTifPreviewType();
String cacheName = fileAttribute.getCacheName();
String cacheName = fileAttribute.getCacheName();
String outFilePath = fileAttribute.getOutFilePath();
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
// 查询转换状态
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
if ("jpg".equalsIgnoreCase(tifPreviewType) || "pdf".equalsIgnoreCase(tifPreviewType)) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
@@ -44,66 +66,111 @@ public class TiffFilePreviewImpl implements FilePreview {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
try {
// 启动异步转换
startAsyncTiffConversion(filePath, outFilePath, cacheName, fileName, fileAttribute, tifPreviewType, forceUpdatedCache);
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("message", "文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to start TIF conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换异常请联系系统管理员!");
}
} else {
// 如果已有缓存,直接渲染预览
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
try {
tiftoservice.convertTif2Pdf(filePath, outFilePath,forceUpdatedCache);
} catch (Exception e) {
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
model.addAttribute("imgUrls", url);
model.addAttribute("currentUrl", url);
return PICTURE_FILE_PREVIEW_PAGE;
}else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转pdf异常请联系系统管理员!" );
}
}
//是否保留TIFF源文件
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName));
return PDF_FILE_PREVIEW_PAGE;
}else {
// 将tif转换为jpg返回转换后的文件路径、文件名的list
List<String> listPic2Jpg;
try {
listPic2Jpg = tiftoservice.convertTif2Jpg(filePath, outFilePath,forceUpdatedCache);
} catch (Exception e) {
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
model.addAttribute("imgUrls", url);
model.addAttribute("currentUrl", url);
return PICTURE_FILE_PREVIEW_PAGE;
}else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转JPG异常请联系系统管理员!" );
}
} else if ("jpg".equalsIgnoreCase(tifPreviewType)) {
List<String> imgCache = fileHandlerService.getImgCache(cacheName);
if (imgCache == null || imgCache.isEmpty()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换缓存异常请联系系统管理员!");
}
//是否保留源文件,转换失败保留源文件,转换成功删除源文件
if(!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.putImgCache(cacheName, listPic2Jpg);
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
model.addAttribute("imgUrls", listPic2Jpg);
model.addAttribute("currentUrl", listPic2Jpg.get(0));
model.addAttribute("imgUrls", imgCache);
model.addAttribute("currentUrl", imgCache.getFirst());
return PICTURE_FILE_PREVIEW_PAGE;
}
}
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName));
return PDF_FILE_PREVIEW_PAGE;
}
else if ("jpg".equalsIgnoreCase(tifPreviewType)) {
model.addAttribute("imgUrls", fileHandlerService.getImgCache(cacheName));
model.addAttribute("currentUrl", fileHandlerService.getImgCache(cacheName).get(0));
return PICTURE_FILE_PREVIEW_PAGE;
}
}
// 处理普通TIF预览不进行转换
return handleRegularTiffPreview(url, model, fileAttribute, fileName, forceUpdatedCache, outFilePath);
}
/**
* 启动异步TIF转换
*/
private void startAsyncTiffConversion(String filePath, String outFilePath, String cacheName,
String fileName, FileAttribute fileAttribute,
String tifPreviewType, boolean forceUpdatedCache) {
// 启动异步转换
CompletableFuture<Void> conversionFuture = CompletableFuture.supplyAsync(() -> {
try {
// 更新状态
FileConvertStatusManager.startConvert(cacheName);
FileConvertStatusManager.updateProgress(cacheName, "正在启动TIF转换", 10);
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
tiftoservice.convertTif2Pdf(filePath, outFilePath,fileName,cacheName, forceUpdatedCache);
// 转换成功,更新缓存
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
} else {
List<String> listPic2Jpg = tiftoservice.convertTif2Jpg(filePath, outFilePath,fileName,cacheName, forceUpdatedCache);
// 转换成功,更新缓存
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.putImgCache(cacheName, listPic2Jpg);
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
}
FileConvertStatusManager.convertSuccess(cacheName);
return null;
} catch (Exception e) {
// 检查是否为Bad endianness tag异常
if (e.getMessage() != null && e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)")) {
// 特殊处理:对于这种异常,我们不标记为转换失败,而是记录日志
logger.warn("TIF文件格式异常Bad endianness tag将尝试直接预览: {}", filePath);
FileConvertStatusManager.convertSuccess(cacheName);
return null;
} else {
logger.error("TIF转换执行失败: {}", cacheName, e);
// 检查是否已经标记为超时
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) {
FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage());
}
throw new RuntimeException(e);
}
}
});
// 添加转换完成后的回调
conversionFuture.thenRunAsync(() -> {
try {
// 是否保留源文件(只在转换成功后才删除)
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
} catch (Exception e) {
logger.error("TIF转换后续处理失败: {}", filePath, e);
}
}, callbackExecutor).exceptionally(throwable -> {
// 转换失败,记录日志但不删除源文件
logger.error("TIF转换失败保留源文件供排查: {}", filePath, throwable);
return null;
});
}
/**
* 处理普通TIF预览不进行转换
*/
private String handleRegularTiffPreview(String url, Model model, FileAttribute fileAttribute,
String fileName, boolean forceUpdatedCache, String outFilePath) {
// 不是http开头浏览器不能直接访问需下载到本地
if (url != null && !url.toLowerCase().startsWith("http")) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
@@ -117,12 +184,11 @@ public class TiffFilePreviewImpl implements FilePreview {
fileHandlerService.addConvertedFile(fileName, fileHandlerService.getRelativePath(outFilePath));
}
} else {
model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName));
model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName));
}
return TIFF_FILE_PREVIEW_PAGE;
}
model.addAttribute("currentUrl", url);
return TIFF_FILE_PREVIEW_PAGE;
}
}
}

View File

@@ -55,7 +55,7 @@ public class DownloadUtils {
String urlStr = null;
try {
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
urlStr = fileAttribute.getUrl();
} catch (Exception e) {
logger.error("处理URL异常:", e);
}

View File

@@ -10,6 +10,7 @@ import org.springframework.web.util.HtmlUtils;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class KkFileUtils {
@@ -236,5 +237,15 @@ public class KkFileUtils {
File file = new File(filePath);
return file.exists();
}
/**
* 判断是否是数字
*/
public static boolean isNumeric(String str){
Pattern pattern = Pattern.compile("[0-9]*");
if (ObjectUtils.isEmpty(str)){
return false;
}
Matcher isNum = pattern.matcher(str);
return isNum.matches();
}
}

View File

@@ -4,7 +4,7 @@ import java.util.BitSet;
public class UrlEncoderUtils {
private static BitSet dontNeedEncoding;
private static final BitSet dontNeedEncoding;
static {
dontNeedEncoding = new BitSet(256);
@@ -19,7 +19,7 @@ public class UrlEncoderUtils {
dontNeedEncoding.set(i);
}
dontNeedEncoding.set('+');
/**
/*
* 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢<br>
* 其实问题是一样的比如遇到123%2B456,它到底是原文即使如此还是123+456 urlEncode后的呢 <br>
* 在这里我认为只要符合urlEncode规范的就当作已经urlEncode过了<br>
@@ -36,13 +36,10 @@ public class UrlEncoderUtils {
* 判断str是否urlEncoder.encode过<br>
* 经常遇到这样的情况拿到一个URL,但是搞不清楚到底要不要encode.<Br>
* 不做encode吧担心出错做encode吧又怕重复了<Br>
*
* @param str
* @return
*/
public static boolean hasUrlEncoded(String str) {
/**
/*
* 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' <br>
* 0-9a-zA-Z保留 <br>
* '-''_''.''*'保留 <br>
@@ -51,7 +48,7 @@ public class UrlEncoderUtils {
boolean needEncode = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (dontNeedEncoding.get((int) c)) {
if (dontNeedEncoding.get(c)) {
continue;
}
if (c == '%' && (i + 2) < str.length()) {
@@ -72,9 +69,6 @@ public class UrlEncoderUtils {
/**
* 判断c是否是16进制的字符
*
* @param c
* @return
*/
private static boolean isDigit16Char(char c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');

View File

@@ -229,11 +229,7 @@ public class WebUtils {
if (fileNameEndIndex < fileNameStartIndex) {
return url;
}
try {
encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), "UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), StandardCharsets.UTF_8);
return url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex);
}
@@ -471,6 +467,8 @@ public class WebUtils {
*/
public static void applyBasicAuthHeaders(HttpHeaders headers, FileAttribute fileAttribute) {
String url = fileAttribute.getUrl();
System.out.println(" T555.");
System.out.println(url);
// 从配置文件读取User-Agent如果没有配置则使用默认值
String customUserAgent=ConfigConstants.getUserAgent();
String userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";

View File

@@ -1,6 +1,7 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.CaptchaUtil;
import cn.keking.utils.DateUtils;
@@ -27,13 +28,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
import static cn.keking.utils.CaptchaUtil.CAPTCHA_GENERATE_TIME;
@@ -54,23 +52,72 @@ public class FileController {
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
@PostMapping("/fileUpload")
public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file) {
ReturnResponse<Object> checkResult = this.fileUploadCheck(file);
public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file,
@RequestParam(value = "path", defaultValue = "") String path) {
ReturnResponse<Object> checkResult = this.fileUploadCheck(file, path);
if (checkResult.isFailure()) {
return checkResult;
}
File outFile = new File(fileDir + demoPath);
if (!outFile.exists() && !outFile.mkdirs()) {
logger.error("创建文件夹【{}】失败,请检查目录权限!", fileDir + demoPath);
String uploadPath = fileDir + demoPath;
if (!ObjectUtils.isEmpty(path)) {
uploadPath += path + File.separator;
}
File outFile = new File(uploadPath);
if (!outFile.exists() && !outFile.mkdirs()) {
logger.error("创建文件夹【{}】失败,请检查目录权限!", uploadPath);
return ReturnResponse.failure("创建文件夹失败,请检查目录权限!");
}
String fileName = checkResult.getContent().toString();
logger.info("上传文件:{}{}{}", fileDir, demoPath, fileName);
try (InputStream in = file.getInputStream(); OutputStream out = Files.newOutputStream(Paths.get(fileDir + demoPath + fileName))) {
logger.info("上传文件:{}{}", uploadPath, fileName);
try (InputStream in = file.getInputStream();
OutputStream out = Files.newOutputStream(Paths.get(uploadPath + fileName))) {
StreamUtils.copy(in, out);
return ReturnResponse.success(null);
} catch (IOException e) {
logger.error("文件上传失败", e);
return ReturnResponse.failure();
return ReturnResponse.failure("文件上传失败");
}
}
@PostMapping("/createFolder")
public ReturnResponse<Object> createFolder(@RequestParam(value = "path", defaultValue = "") String path,
@RequestParam("folderName") String folderName) {
if (ConfigConstants.getFileUploadDisable()) {
return ReturnResponse.failure("文件上传接口已禁用");
}
try {
// 验证文件夹名称
if (ObjectUtils.isEmpty(folderName)) {
return ReturnResponse.failure("文件夹名称不能为空");
}
if (KkFileUtils.isIllegalFileName(folderName)) {
return ReturnResponse.failure("非法文件夹名称");
}
String basePath = fileDir + demoPath;
if (!ObjectUtils.isEmpty(path)) {
basePath += path + File.separator;
}
File newFolder = new File(basePath + folderName);
if (newFolder.exists()) {
return ReturnResponse.failure("文件夹已存在");
}
if (newFolder.mkdirs()) {
logger.info("创建文件夹:{}", newFolder.getAbsolutePath());
return ReturnResponse.success();
} else {
logger.error("创建文件夹失败:{}", newFolder.getAbsolutePath());
return ReturnResponse.failure("创建文件夹失败,请检查目录权限");
}
} catch (Exception e) {
logger.error("创建文件夹异常", e);
return ReturnResponse.failure("创建文件夹失败:" + e.getMessage());
}
}
@@ -81,15 +128,55 @@ public class FileController {
return checkResult;
}
fileName = checkResult.getContent().toString();
File file = new File(fileDir + demoPath + fileName);
logger.info("删除文件:{}", file.getAbsolutePath());
if (file.exists() && !file.delete()) {
String msg = String.format("删除文件【%s】失败请检查目录权限", file.getPath());
logger.error(msg);
return ReturnResponse.failure(msg);
// 构建完整路径
String fullPath = fileDir + demoPath + fileName;
File file = new File(fullPath);
logger.info("删除文件/文件夹:{}", file.getAbsolutePath());
if (file.exists()) {
if (file.isDirectory()) {
// 删除文件夹及其内容
if (deleteDirectory(file)) {
WebUtils.removeSessionAttr(request, CAPTCHA_CODE);
return ReturnResponse.success();
} else {
String msg = String.format("删除文件夹【%s】失败请检查目录权限", file.getPath());
logger.error(msg);
return ReturnResponse.failure(msg);
}
} else {
// 删除文件
if (file.delete()) {
WebUtils.removeSessionAttr(request, CAPTCHA_CODE);
return ReturnResponse.success();
} else {
String msg = String.format("删除文件【%s】失败请检查目录权限", file.getPath());
logger.error(msg);
return ReturnResponse.failure(msg);
}
}
} else {
return ReturnResponse.failure("文件或文件夹不存在");
}
WebUtils.removeSessionAttr(request, CAPTCHA_CODE); //删除缓存验证码
return ReturnResponse.success();
}
/**
* 递归删除目录
*/
private boolean deleteDirectory(File dir) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
boolean success = deleteDirectory(child);
if (!success) {
return false;
}
}
}
}
return dir.delete();
}
/**
@@ -124,32 +211,156 @@ public class FileController {
outputStream.close();
}
@GetMapping("/listFiles")
public List<Map<String, String>> getFiles() {
List<Map<String, String>> list = new ArrayList<>();
File file = new File(fileDir + demoPath);
if (file.exists()) {
File[] files = Objects.requireNonNull(file.listFiles());
Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
Arrays.stream(files).forEach(file1 -> {
Map<String, String> fileName = new HashMap<>();
fileName.put("fileName", demoDir + "/" + file1.getName());
list.add(fileName);
});
@PostMapping("/listFiles")
public Map<String, Object> getFiles(@RequestParam(value = "path", defaultValue = "") String path,
@RequestParam(value = "searchText", defaultValue = "") String searchText,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) String sort,
@RequestParam(required = false) String order) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> fileList = new ArrayList<>();
try {
// 构建完整路径
String basePath = fileDir + demoPath;
if (!ObjectUtils.isEmpty(path)) {
basePath += path + File.separator;
}
File currentDir = new File(basePath);
if (currentDir.exists() && currentDir.isDirectory()) {
File[] files = currentDir.listFiles();
if (files != null) {
// 转换为List
List<File> fileObjects = new ArrayList<>(Arrays.asList(files));
// 如果搜索文本不为空,进行过滤
if (!ObjectUtils.isEmpty(searchText)) {
String searchLower = searchText.toLowerCase();
fileObjects.removeIf(f -> !f.getName().toLowerCase().contains(searchLower));
}
// 排序
Comparator<File> comparator = getFileComparator(sort, order);
if (comparator != null) {
fileObjects.sort(comparator);
}
int total = fileObjects.size();
int start = page * size;
int end = Math.min(start + size, total);
if (start < total) {
for (int i = start; i < end; i++) {
File f = fileObjects.get(i);
Map<String, Object> fileInfo = new HashMap<>();
fileInfo.put("name", f.getName());
fileInfo.put("isDirectory", f.isDirectory());
fileInfo.put("lastModified", f.lastModified());
fileInfo.put("size", f.length());
// 构建路径信息
String relativePath = demoDir + "/" + (ObjectUtils.isEmpty(path) ? "" : path + "/") + f.getName();
fileInfo.put("relativePath", relativePath);
// 如果是目录,保存完整的相对路径用于导航
if (f.isDirectory()) {
String fullPath = ObjectUtils.isEmpty(path) ? f.getName() : path + "/" + f.getName();
fileInfo.put("fullPath", fullPath);
}
// 获取文件属性
try {
Path filePath = f.toPath();
BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
fileInfo.put("creationTime", attrs.creationTime().toMillis());
} catch (IOException e) {
logger.warn("获取文件属性失败: {}", f.getName(), e);
}
fileList.add(fileInfo);
}
}
result.put("total", total);
result.put("data", fileList);
}
} else {
result.put("total", 0);
result.put("data", Collections.emptyList());
}
} catch (Exception e) {
logger.error("获取文件列表失败", e);
result.put("total", 0);
result.put("data", Collections.emptyList());
}
return list;
return result;
}
/**
* 获取文件比较器
*/
private Comparator<File> getFileComparator(String sort, String order) {
if (ObjectUtils.isEmpty(sort)) {
// 默认按文件夹优先,然后按名称排序
return (f1, f2) -> {
if (f1.isDirectory() && !f2.isDirectory()) {
return -1;
} else if (!f1.isDirectory() && f2.isDirectory()) {
return 1;
} else {
return f1.getName().compareToIgnoreCase(f2.getName());
}
};
}
boolean isDesc = "desc".equalsIgnoreCase(order);
return switch (sort) {
case "name" -> (f1, f2) -> {
int compare = f1.getName().compareToIgnoreCase(f2.getName());
return isDesc ? -compare : compare;
};
case "lastModified" -> (f1, f2) -> {
long compare = Long.compare(f1.lastModified(), f2.lastModified());
return isDesc ? (int) -compare : (int) compare;
};
case "size" -> (f1, f2) -> {
if (f1.isDirectory() && f2.isDirectory()) {
return 0;
} else if (f1.isDirectory()) {
return isDesc ? 1 : -1;
} else if (f2.isDirectory()) {
return isDesc ? -1 : 1;
} else {
long compare = Long.compare(f1.length(), f2.length());
return isDesc ? (int) -compare : (int) compare;
}
};
case "isDirectory" -> (f1, f2) -> {
if (f1.isDirectory() && !f2.isDirectory()) {
return isDesc ? 1 : -1;
} else if (!f1.isDirectory() && f2.isDirectory()) {
return isDesc ? -1 : 1;
} else {
return f1.getName().compareToIgnoreCase(f2.getName());
}
};
default -> null;
};
}
/**
* 上传文件前校验
*
* @param file 文件
* @return 校验结果
*/
private ReturnResponse<Object> fileUploadCheck(MultipartFile file) {
private ReturnResponse<Object> fileUploadCheck(MultipartFile file, String path) {
if (ConfigConstants.getFileUploadDisable()) {
return ReturnResponse.failure("文件传接口已禁用");
return ReturnResponse.failure("文件传接口已禁用");
}
String fileName = WebUtils.getFileNameFromMultipartFile(file);
if (fileName.lastIndexOf(".") == -1) {
return ReturnResponse.failure("不允许上传的类型");
@@ -160,47 +371,56 @@ public class FileController {
if (KkFileUtils.isIllegalFileName(fileName)) {
return ReturnResponse.failure("不允许上传的文件名: " + fileName);
}
FileType type = FileType.typeFromFileName(fileName);
if (Objects.equals(type, FileType.OTHER)) {
return ReturnResponse.failure("该文件格式还不支持预览,请联系管理员,添加该格式: " + fileName);
}
// 判断是否存在同名文件
if (existsFile(fileName)) {
if (existsFile(fileName, path)) {
return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传");
}
return ReturnResponse.success(fileName);
}
/**
* 删除文件前校验
*
* @param fileName 文件名
* @return 校验结果
*/
private ReturnResponse<Object> deleteFileCheck(HttpServletRequest request, String fileName, String password) {
if (ObjectUtils.isEmpty(fileName)) {
return ReturnResponse.failure("文件名为空,删除失败!");
}
try {
fileName = WebUtils.decodeUrl(fileName,"base64");
fileName = WebUtils.decodeUrl(fileName, "base64");
} catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName);
return ReturnResponse.failure(errorMsg + "删除失败!");
}
assert fileName != null;
if (ObjectUtils.isEmpty(fileName)) {
return ReturnResponse.failure("文件名为空,删除失败!");
}
if (fileName.contains("/")) {
fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
}
if (KkFileUtils.isIllegalFileName(fileName)) {
return ReturnResponse.failure("非法文件名,删除失败!");
}
if (ObjectUtils.isEmpty(password)) {
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
}
String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, CAPTCHA_CODE) : ConfigConstants.getPassword();
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
ConfigConstants.getPassword();
if (!password.equalsIgnoreCase(expectedPassword)) {
logger.error("删除文件【{}】失败,密码错误!", fileName);
return ReturnResponse.failure("删除文件失败,密码错误!");
}
return ReturnResponse.success(fileName);
}
@@ -220,8 +440,12 @@ public class FileController {
return RarUtils.getTree(fileUrl);
}
private boolean existsFile(String fileName) {
File file = new File(fileDir + demoPath + fileName);
private boolean existsFile(String fileName, String path) {
String fullPath = fileDir + demoPath;
if (!ObjectUtils.isEmpty(path)) {
fullPath += path + File.separator;
}
File file = new File(fullPath + fileName);
return file.exists();
}
}
}

View File

@@ -77,6 +77,9 @@ public class OnlinePreviewController {
public String onlinePreview(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
@RequestParam(defaultValue = "false") String highlightall,
@RequestParam(defaultValue = "0") String page,
@RequestParam(defaultValue = "false") String kkagent,
Model model,
HttpServletRequest req) {
// 验证访问权限
@@ -90,7 +93,12 @@ public class OnlinePreviewController {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return otherFilePreview.notSupportedFile(model, errorMsg);
}
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req); //这里不在进行URL 处理了
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req);
highlightall= KkFileUtils.htmlEscape(highlightall);
model.addAttribute("highlightall", highlightall);
model.addAttribute("page", page);
model.addAttribute("kkagent", kkagent);
model.addAttribute("file", fileAttribute);
FilePreview filePreview = previewFactory.get(fileAttribute);
logger.info("预览文件url{}previewType{}", fileUrl, fileAttribute.getType());
@@ -151,8 +159,8 @@ public class OnlinePreviewController {
public void getCorsFile(@RequestParam String urlPath,
@RequestParam(required = false) String key,
HttpServletResponse response,
@RequestParam(required = false) String encryption,
FileAttribute fileAttribute) throws Exception {
HttpServletRequest req,
@RequestParam(required = false) String encryption) throws Exception {
// 1. 验证接口是否开启
if (!ConfigConstants.getGetCorsFile()) {
@@ -177,6 +185,7 @@ public class OnlinePreviewController {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return;
}
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath);
if (!isFtpUrl(url)) {
@@ -188,7 +197,6 @@ public class OnlinePreviewController {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
WebUtils.applyBasicAuthHeaders(request.getHeaders(), fileAttribute);

View File

@@ -47,6 +47,14 @@ public class AttributeSetFilter implements Filter {
request.setAttribute("homePagination", ConfigConstants.getHomePagination());
request.setAttribute("homePageSize", ConfigConstants.getHomePageSize());
request.setAttribute("homeSearch", ConfigConstants.getHomeSearch());
request.setAttribute("isshowaeskey", ConfigConstants.getisShowaesKey());
request.setAttribute("isjavascript", ConfigConstants.getisJavaScript());
request.setAttribute("xlsxallowEdit", ConfigConstants.getxlsxAllowEdit());
request.setAttribute("xlsxshowtoolbar", ConfigConstants.getxlsxShowtoolbar());
request.setAttribute("aeskey", ConfigConstants.getaesKey());
request.setAttribute("isshowkey", ConfigConstants.getisShowKey());
request.setAttribute("kkkey", ConfigConstants.getKey());
request.setAttribute("scriptjs", ConfigConstants.getscriptJs());
}
/**