Dv1
This commit is contained in:
89
ruoyi-kkfileview/pom.xml
Normal file
89
ruoyi-kkfileview/pom.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>ruoyi</artifactId>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<version>3.9.1</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<packaging>jar</packaging>
|
||||
<artifactId>ruoyi-kkfileview</artifactId>
|
||||
|
||||
<description>
|
||||
kkFileView 文件预览模块
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- SpringBoot Web容器 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 通用工具-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Office files -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons IO -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenOffice/LibreOffice integration (for document conversion) -->
|
||||
<!-- 注意:kkFileView 使用 OpenOffice 或 LibreOffice 进行文档转换 -->
|
||||
<!-- 如果需要完整的Office文件转换功能,请取消注释以下依赖并安装LibreOffice -->
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>org.jodconverter</groupId>
|
||||
<artifactId>jodconverter-core</artifactId>
|
||||
<version>4.4.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jodconverter</groupId>
|
||||
<artifactId>jodconverter-local</artifactId>
|
||||
<version>4.4.6</version>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
<!-- PDF处理 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>2.0.24</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 图片处理 -->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>0.4.17</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 文件类型检测 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>2.7.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Base64编码 -->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.15</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.ruoyi.kkfileview.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* kkFileView 配置类
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "kkfileview")
|
||||
public class KkFileViewConfig
|
||||
{
|
||||
/** 是否启用 */
|
||||
private boolean enabled = true;
|
||||
|
||||
/** 文件存储路径 */
|
||||
private String fileSaveDir;
|
||||
|
||||
/** 缓存清理时间(分钟) */
|
||||
private int cacheCleanTime = 60;
|
||||
|
||||
/** OpenOffice/LibreOffice 安装路径 */
|
||||
private String officeHome;
|
||||
|
||||
/** 支持的预览文件类型 */
|
||||
private String previewTypes = "doc,docx,xls,xlsx,ppt,pptx,pdf,txt,csv,md,html,htm,xml,json";
|
||||
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getFileSaveDir()
|
||||
{
|
||||
return fileSaveDir;
|
||||
}
|
||||
|
||||
public void setFileSaveDir(String fileSaveDir)
|
||||
{
|
||||
this.fileSaveDir = fileSaveDir;
|
||||
}
|
||||
|
||||
public int getCacheCleanTime()
|
||||
{
|
||||
return cacheCleanTime;
|
||||
}
|
||||
|
||||
public void setCacheCleanTime(int cacheCleanTime)
|
||||
{
|
||||
this.cacheCleanTime = cacheCleanTime;
|
||||
}
|
||||
|
||||
public String getOfficeHome()
|
||||
{
|
||||
return officeHome;
|
||||
}
|
||||
|
||||
public void setOfficeHome(String officeHome)
|
||||
{
|
||||
this.officeHome = officeHome;
|
||||
}
|
||||
|
||||
public String getPreviewTypes()
|
||||
{
|
||||
return previewTypes;
|
||||
}
|
||||
|
||||
public void setPreviewTypes(String previewTypes)
|
||||
{
|
||||
this.previewTypes = previewTypes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ruoyi.kkfileview.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.kkfileview.config.KkFileViewConfig;
|
||||
import com.ruoyi.kkfileview.service.FilePreviewService;
|
||||
|
||||
/**
|
||||
* kkFileView 在线预览控制器
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/kkfileview")
|
||||
@Anonymous // 允许匿名访问(因为预览接口会通过token参数进行认证)
|
||||
public class OnlinePreviewController
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(OnlinePreviewController.class);
|
||||
|
||||
@Autowired
|
||||
private KkFileViewConfig config;
|
||||
|
||||
@Autowired(required = false)
|
||||
private FilePreviewService filePreviewService;
|
||||
|
||||
/**
|
||||
* 在线预览接口
|
||||
*
|
||||
* @param url 文件URL(Base64编码)
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@RequestMapping("/onlinePreview")
|
||||
public void onlinePreview(@RequestParam(value = "url", required = false) String url,
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response)
|
||||
{
|
||||
// 允许在iframe中加载(kkFileView预览需要)
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
|
||||
if (!config.isEnabled())
|
||||
{
|
||||
try
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.getWriter().write("kkFileView 服务未启用");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.error("写入响应失败", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (url == null || url.isEmpty())
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.getWriter().write("文件URL参数不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解码Base64 URL
|
||||
String decodedUrl = new String(Base64.getDecoder().decode(url), StandardCharsets.UTF_8);
|
||||
log.info("预览文件URL: {}", decodedUrl);
|
||||
|
||||
// 如果有预览服务,调用预览服务
|
||||
if (filePreviewService != null)
|
||||
{
|
||||
filePreviewService.previewFile(decodedUrl, request, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 临时方案:重定向到文件URL(让浏览器处理)
|
||||
// 注意:这只是一个占位实现,实际需要集成 kkFileView 的完整功能
|
||||
// 或者使用其他预览方案(如前端直接预览)
|
||||
response.sendRedirect(decodedUrl);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("文件预览失败", e);
|
||||
try
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.getWriter().write("文件预览失败:" + e.getMessage());
|
||||
}
|
||||
catch (IOException ioException)
|
||||
{
|
||||
log.error("写入错误响应失败", ioException);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务状态
|
||||
*/
|
||||
@RequestMapping("/status")
|
||||
@ResponseBody
|
||||
public AjaxResult status()
|
||||
{
|
||||
return AjaxResult.success()
|
||||
.put("enabled", config.isEnabled())
|
||||
.put("status", config.isEnabled() ? "运行中" : "未启用")
|
||||
.put("fileSaveDir", config.getFileSaveDir())
|
||||
.put("previewTypes", config.getPreviewTypes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.kkfileview.service;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* 文件预览服务接口
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
public interface FilePreviewService
|
||||
{
|
||||
/**
|
||||
* 预览文件
|
||||
*
|
||||
* @param fileUrl 文件URL
|
||||
* @param request HTTP请求
|
||||
* @param response HTTP响应
|
||||
* @throws Exception 预览异常
|
||||
*/
|
||||
void previewFile(String fileUrl, HttpServletRequest request, HttpServletResponse response) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.kkfileview.service.convert;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 文件转换服务接口
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
public interface FileConvertService
|
||||
{
|
||||
/**
|
||||
* 将文件转换为PDF
|
||||
*
|
||||
* @param sourceFile 源文件
|
||||
* @param targetFile 目标PDF文件
|
||||
* @return 是否转换成功
|
||||
*/
|
||||
boolean convertToPdf(File sourceFile, File targetFile);
|
||||
|
||||
/**
|
||||
* 将文件转换为图片
|
||||
*
|
||||
* @param sourceFile 源文件
|
||||
* @param targetDir 目标目录
|
||||
* @return 转换后的图片文件数组
|
||||
*/
|
||||
File[] convertToImages(File sourceFile, File targetDir);
|
||||
|
||||
/**
|
||||
* 是否支持该文件类型
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 是否支持
|
||||
*/
|
||||
boolean supports(String fileType);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.ruoyi.kkfileview.service.convert.impl;
|
||||
|
||||
import java.io.File;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.ruoyi.kkfileview.config.KkFileViewConfig;
|
||||
import com.ruoyi.kkfileview.service.convert.FileConvertService;
|
||||
|
||||
/**
|
||||
* Office文件转换服务实现
|
||||
*
|
||||
* 注意:完整实现需要集成 jodconverter 和 LibreOffice
|
||||
* 当前为占位实现,需要根据实际环境配置
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
@Service
|
||||
public class OfficeConvertService implements FileConvertService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(OfficeConvertService.class);
|
||||
|
||||
@Autowired
|
||||
private KkFileViewConfig config;
|
||||
|
||||
@Override
|
||||
public boolean convertToPdf(File sourceFile, File targetFile)
|
||||
{
|
||||
// TODO: 使用 jodconverter 和 LibreOffice 进行转换
|
||||
// 示例代码:
|
||||
// LocalConverter converter = LocalConverter.builder()
|
||||
// .officeHome(new File(config.getOfficeHome()))
|
||||
// .build();
|
||||
// converter.convert(sourceFile).to(targetFile).execute();
|
||||
|
||||
log.warn("Office文件转PDF功能未实现,需要配置 LibreOffice 和 jodconverter");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File[] convertToImages(File sourceFile, File targetDir)
|
||||
{
|
||||
// TODO: 先转换为PDF,再将PDF转换为图片
|
||||
log.warn("Office文件转图片功能未实现");
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(String fileType)
|
||||
{
|
||||
if (fileType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("doc") || lowerType.equals("docx") ||
|
||||
lowerType.equals("xls") || lowerType.equals("xlsx") ||
|
||||
lowerType.equals("ppt") || lowerType.equals("pptx");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package com.ruoyi.kkfileview.service.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.kkfileview.config.KkFileViewConfig;
|
||||
import com.ruoyi.kkfileview.service.FilePreviewService;
|
||||
import com.ruoyi.kkfileview.service.convert.FileConvertService;
|
||||
import com.ruoyi.kkfileview.utils.DownloadUtils;
|
||||
import com.ruoyi.kkfileview.utils.FileTypeUtils;
|
||||
|
||||
/**
|
||||
* 文件预览服务实现类
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
@Service
|
||||
public class FilePreviewServiceImpl implements FilePreviewService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(FilePreviewServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private KkFileViewConfig config;
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<FileConvertService> convertServices;
|
||||
|
||||
@Override
|
||||
public void previewFile(String fileUrl, HttpServletRequest request, HttpServletResponse response) throws Exception
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从请求中提取token(如果URL中包含token参数)
|
||||
String token = extractTokenFromUrl(fileUrl);
|
||||
if (token == null)
|
||||
{
|
||||
// 尝试从请求头中获取
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer "))
|
||||
{
|
||||
token = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. 下载文件到临时目录
|
||||
File tempFile = downloadFileToTemp(fileUrl, token);
|
||||
if (tempFile == null || !tempFile.exists())
|
||||
{
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.setContentType("text/html;charset=UTF-8");
|
||||
response.getWriter().write("文件下载失败或文件不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取文件类型
|
||||
String fileType = FileTypeUtils.getFileType(tempFile.getName());
|
||||
log.info("预览文件类型: {}, 文件: {}", fileType, tempFile.getName());
|
||||
|
||||
// 3. 根据文件类型处理
|
||||
if (FileTypeUtils.isImageFile(fileType))
|
||||
{
|
||||
// 图片文件直接返回
|
||||
serveFile(tempFile, response, FileTypeUtils.detectMimeType(tempFile));
|
||||
}
|
||||
else if (FileTypeUtils.isPdfFile(fileType))
|
||||
{
|
||||
// PDF文件直接返回
|
||||
serveFile(tempFile, response, "application/pdf");
|
||||
}
|
||||
else if (FileTypeUtils.isTextFile(fileType))
|
||||
{
|
||||
// 文本文件直接返回
|
||||
serveFile(tempFile, response, "text/plain;charset=UTF-8");
|
||||
}
|
||||
else if (FileTypeUtils.isOfficeFile(fileType))
|
||||
{
|
||||
// Office文件需要转换
|
||||
handleOfficeFile(tempFile, fileType, response);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 其他文件类型,尝试直接返回
|
||||
serveFile(tempFile, response, FileTypeUtils.detectMimeType(tempFile));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("文件预览失败: {}", fileUrl, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取token参数
|
||||
*/
|
||||
private String extractTokenFromUrl(String fileUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (fileUrl != null && fileUrl.contains("token="))
|
||||
{
|
||||
int tokenIndex = fileUrl.indexOf("token=");
|
||||
String tokenPart = fileUrl.substring(tokenIndex + 6);
|
||||
int endIndex = tokenPart.indexOf("&");
|
||||
if (endIndex > 0)
|
||||
{
|
||||
return java.net.URLDecoder.decode(tokenPart.substring(0, endIndex), StandardCharsets.UTF_8.name());
|
||||
}
|
||||
else
|
||||
{
|
||||
return java.net.URLDecoder.decode(tokenPart, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("从URL提取token失败: {}", fileUrl, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件到临时目录
|
||||
*/
|
||||
private File downloadFileToTemp(String fileUrl) throws IOException
|
||||
{
|
||||
return downloadFileToTemp(fileUrl, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件到临时目录(支持传递认证token)
|
||||
*/
|
||||
private File downloadFileToTemp(String fileUrl, String token) throws IOException
|
||||
{
|
||||
try
|
||||
{
|
||||
// 创建临时目录
|
||||
String tempDir = config.getFileSaveDir();
|
||||
if (StringUtils.isEmpty(tempDir))
|
||||
{
|
||||
tempDir = RuoYiConfig.getProfile() + "/kkfileview/temp";
|
||||
}
|
||||
|
||||
File tempDirFile = new File(tempDir);
|
||||
if (!tempDirFile.exists())
|
||||
{
|
||||
tempDirFile.mkdirs();
|
||||
}
|
||||
|
||||
// 生成临时文件名
|
||||
String fileName = DownloadUtils.getFileNameFromUrl(fileUrl);
|
||||
if (StringUtils.isEmpty(fileName) || fileName.equals("file"))
|
||||
{
|
||||
fileName = "temp_" + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
File tempFile = new File(tempDirFile, fileName);
|
||||
|
||||
// 下载文件(传递token用于认证)
|
||||
if (DownloadUtils.downloadFile(fileUrl, tempFile, token))
|
||||
{
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("下载文件到临时目录失败: {}", fileUrl, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Office文件
|
||||
*/
|
||||
private void handleOfficeFile(File sourceFile, String fileType, HttpServletResponse response) throws Exception
|
||||
{
|
||||
// 查找支持该文件类型的转换服务
|
||||
FileConvertService convertService = null;
|
||||
if (!CollectionUtils.isEmpty(convertServices))
|
||||
{
|
||||
for (FileConvertService service : convertServices)
|
||||
{
|
||||
if (service.supports(fileType))
|
||||
{
|
||||
convertService = service;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (convertService != null)
|
||||
{
|
||||
// 转换为PDF
|
||||
File pdfFile = new File(sourceFile.getParent(), sourceFile.getName() + ".pdf");
|
||||
if (convertService.convertToPdf(sourceFile, pdfFile) && pdfFile.exists())
|
||||
{
|
||||
serveFile(pdfFile, response, "application/pdf");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果转换失败,尝试直接返回(浏览器可能支持部分预览)
|
||||
log.warn("Office文件转换失败,尝试直接返回: {}", sourceFile.getName());
|
||||
serveFile(sourceFile, response, FileTypeUtils.detectMimeType(sourceFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文件内容
|
||||
*/
|
||||
private void serveFile(File file, HttpServletResponse response, String contentType) throws IOException
|
||||
{
|
||||
response.setContentType(contentType);
|
||||
response.setHeader("Content-Disposition", "inline; filename=\"" +
|
||||
java.net.URLEncoder.encode(file.getName(), StandardCharsets.UTF_8.name()) + "\"");
|
||||
response.setHeader("Cache-Control", "public, max-age=3600");
|
||||
// 允许在iframe中加载(kkFileView预览需要)
|
||||
response.setHeader("X-Frame-Options", "SAMEORIGIN");
|
||||
response.setContentLengthLong(file.length());
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
OutputStream os = response.getOutputStream())
|
||||
{
|
||||
IOUtils.copy(fis, os);
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.ruoyi.kkfileview.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* 文件下载工具类
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
public class DownloadUtils
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(DownloadUtils.class);
|
||||
|
||||
private static final int CONNECT_TIMEOUT = 30000; // 30秒
|
||||
private static final int READ_TIMEOUT = 60000; // 60秒
|
||||
|
||||
/**
|
||||
* 从URL下载文件到本地
|
||||
*
|
||||
* @param fileUrl 文件URL
|
||||
* @param targetFile 目标文件
|
||||
* @return 是否下载成功
|
||||
*/
|
||||
public static boolean downloadFile(String fileUrl, File targetFile)
|
||||
{
|
||||
return downloadFile(fileUrl, targetFile, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL下载文件到本地(支持传递认证token)
|
||||
*
|
||||
* @param fileUrl 文件URL
|
||||
* @param targetFile 目标文件
|
||||
* @param token 认证token(可选)
|
||||
* @return 是否下载成功
|
||||
*/
|
||||
public static boolean downloadFile(String fileUrl, File targetFile, String token)
|
||||
{
|
||||
InputStream inputStream = null;
|
||||
FileOutputStream outputStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
// 确保目标文件的父目录存在
|
||||
File parentDir = targetFile.getParentFile();
|
||||
if (parentDir != null && !parentDir.exists())
|
||||
{
|
||||
parentDir.mkdirs();
|
||||
}
|
||||
|
||||
URL url = new URL(fileUrl);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
connection.setReadTimeout(READ_TIMEOUT);
|
||||
|
||||
// 如果是HTTP连接,设置请求头
|
||||
if (connection instanceof HttpURLConnection)
|
||||
{
|
||||
HttpURLConnection httpConnection = (HttpURLConnection) connection;
|
||||
httpConnection.setRequestProperty("User-Agent", "Mozilla/5.0");
|
||||
httpConnection.setInstanceFollowRedirects(true);
|
||||
// 如果提供了token,添加到请求头
|
||||
if (token != null && !token.isEmpty())
|
||||
{
|
||||
httpConnection.setRequestProperty("Authorization", "Bearer " + token);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应码
|
||||
if (connection instanceof HttpURLConnection)
|
||||
{
|
||||
HttpURLConnection httpConnection = (HttpURLConnection) connection;
|
||||
int responseCode = httpConnection.getResponseCode();
|
||||
if (responseCode >= 400)
|
||||
{
|
||||
log.error("文件下载失败,HTTP响应码: {}, URL: {}", responseCode, fileUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inputStream = connection.getInputStream();
|
||||
outputStream = new FileOutputStream(targetFile);
|
||||
|
||||
IOUtils.copy(inputStream, outputStream);
|
||||
|
||||
log.info("文件下载成功: {} -> {}", fileUrl, targetFile.getAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("文件下载失败: {}", fileUrl, e);
|
||||
// 如果下载失败,删除可能创建的不完整文件
|
||||
if (targetFile.exists())
|
||||
{
|
||||
targetFile.delete();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
IOUtils.closeQuietly(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL获取文件输入流
|
||||
*
|
||||
* @param fileUrl 文件URL
|
||||
* @return 输入流
|
||||
*/
|
||||
public static InputStream getInputStream(String fileUrl) throws IOException
|
||||
{
|
||||
URL url = new URL(fileUrl);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT);
|
||||
connection.setReadTimeout(READ_TIMEOUT);
|
||||
|
||||
if (connection instanceof HttpURLConnection)
|
||||
{
|
||||
HttpURLConnection httpConnection = (HttpURLConnection) connection;
|
||||
httpConnection.setRequestProperty("User-Agent", "Mozilla/5.0");
|
||||
httpConnection.setInstanceFollowRedirects(true);
|
||||
}
|
||||
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件名(从URL中提取)
|
||||
*
|
||||
* @param fileUrl 文件URL
|
||||
* @return 文件名
|
||||
*/
|
||||
public static String getFileNameFromUrl(String fileUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
String path = new URL(fileUrl).getPath();
|
||||
if (StringUtils.isNotEmpty(path))
|
||||
{
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
// URL解码
|
||||
return java.net.URLDecoder.decode(fileName, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("从URL提取文件名失败: {}", fileUrl, e);
|
||||
}
|
||||
return "file";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.ruoyi.kkfileview.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.tika.Tika;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* 文件类型工具类
|
||||
*
|
||||
* @author jackeyhuang
|
||||
* @version 1.0.0
|
||||
* @date 2026-01-11
|
||||
*/
|
||||
public class FileTypeUtils
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(FileTypeUtils.class);
|
||||
|
||||
private static final Tika tika = new Tika();
|
||||
|
||||
/**
|
||||
* 根据文件扩展名获取文件类型
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 文件类型(小写)
|
||||
*/
|
||||
public static String getFileType(String fileName)
|
||||
{
|
||||
if (StringUtils.isEmpty(fileName))
|
||||
{
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(fileName).toLowerCase();
|
||||
return StringUtils.isEmpty(extension) ? "unknown" : extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文件MIME类型
|
||||
*
|
||||
* @param file 文件
|
||||
* @return MIME类型
|
||||
*/
|
||||
public static String detectMimeType(File file)
|
||||
{
|
||||
try
|
||||
{
|
||||
return tika.detect(file);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
log.warn("检测文件MIME类型失败: {}", file.getAbsolutePath(), e);
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测文件MIME类型(通过文件名)
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return MIME类型
|
||||
*/
|
||||
public static String detectMimeType(String fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return tika.detect(fileName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.warn("检测文件MIME类型失败: {}", fileName, e);
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Office文件
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 是否为Office文件
|
||||
*/
|
||||
public static boolean isOfficeFile(String fileType)
|
||||
{
|
||||
if (StringUtils.isEmpty(fileType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("doc") || lowerType.equals("docx") ||
|
||||
lowerType.equals("xls") || lowerType.equals("xlsx") ||
|
||||
lowerType.equals("ppt") || lowerType.equals("pptx");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为图片文件
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 是否为图片文件
|
||||
*/
|
||||
public static boolean isImageFile(String fileType)
|
||||
{
|
||||
if (StringUtils.isEmpty(fileType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("jpg") || lowerType.equals("jpeg") ||
|
||||
lowerType.equals("png") || lowerType.equals("gif") ||
|
||||
lowerType.equals("bmp") || lowerType.equals("webp");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为PDF文件
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 是否为PDF文件
|
||||
*/
|
||||
public static boolean isPdfFile(String fileType)
|
||||
{
|
||||
return "pdf".equalsIgnoreCase(fileType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文本文件
|
||||
*
|
||||
* @param fileType 文件类型
|
||||
* @return 是否为文本文件
|
||||
*/
|
||||
public static boolean isTextFile(String fileType)
|
||||
{
|
||||
if (StringUtils.isEmpty(fileType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
String lowerType = fileType.toLowerCase();
|
||||
return lowerType.equals("txt") || lowerType.equals("md") ||
|
||||
lowerType.equals("csv") || lowerType.equals("log") ||
|
||||
lowerType.equals("json") || lowerType.equals("xml") ||
|
||||
lowerType.equals("html") || lowerType.equals("htm");
|
||||
}
|
||||
}
|
||||
12
ruoyi-kkfileview/src/main/resources/application.yml
Normal file
12
ruoyi-kkfileview/src/main/resources/application.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
# kkFileView 模块配置
|
||||
kkfileview:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
# 文件存储路径
|
||||
file-save-dir: ${ruoyi.profile}/kkfileview
|
||||
# 缓存清理时间(分钟)
|
||||
cache-clean-time: 60
|
||||
# OpenOffice/LibreOffice 安装路径(可选,用于文档转换)
|
||||
office-home:
|
||||
# 支持的预览文件类型
|
||||
preview-types: doc,docx,xls,xlsx,ppt,pptx,pdf,txt,csv,md,html,htm,xml,json
|
||||
Reference in New Issue
Block a user