Merge PR342 into master with trust-host conflict resolution

This commit is contained in:
kl
2026-04-07 15:59:20 +08:00
1378 changed files with 551360 additions and 263501 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>kkFileView-parent</artifactId>
<groupId>cn.keking</groupId>
<version>4.4.0</version>
<version>5.0</version>
</parent>
<artifactId>kkFileView</artifactId>
@@ -24,28 +24,26 @@
</dependencyManagement>
<repositories>
<repository>
<id>aspose-maven-repository</id>
<url>https://repository.aspose.com/repo</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<!-- Aspose 仓库且只启用 releases -->
<repository>
<id>aspose-maven-repository</id>
<url>https://repository.aspose.com/repo</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>${jodconverter.version}</version>
</dependency>
<!-- web start -->
<!-- ========== Spring Boot 框架依赖 ========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
@@ -54,9 +52,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- web end -->
<!-- poi start -->
<!-- ========== 文档格式转换 ========== -->
<dependency>
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>${jodconverter.version}</version>
</dependency>
<!-- ========== Office文档处理 (POI相关) ========== -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
@@ -93,85 +97,13 @@
<artifactId>fr.opensagres.xdocreport.document</artifactId>
<version>${xdocreport.version}</version>
</dependency>
<!-- poi start -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<groupId>com.aspose</groupId>
<artifactId>aspose-cad</artifactId>
<version>${aspose-cad.version}</version>
</dependency>
<!-- rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding</artifactId>
<version>${sevenzipjbinding.version}</version>
</dependency>
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding-all-platforms</artifactId>
<version>${sevenzipjbinding.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- 编码检测-JUniversalCharDet-->
<dependency>
<groupId>com.googlecode.juniversalchardet</groupId>
<artifactId>juniversalchardet</artifactId>
<version>${juniversalchardet.version}</version>
</dependency>
<!-- 解压(rar)-->
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>${junrar.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jchardet</groupId>
<artifactId>jchardet</artifactId>
<version>${jchardet.version}</version>
</dependency>
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${commons-cli.version}</version>
</dependency>
<!-- FTP -->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>${xstream.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
<artifactId>concurrentlinkedhashmap-lru</artifactId>
<version>${concurrentlinkedhashmap.version}</version>
</dependency>
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>${rocksdb.version}</version>
</dependency>
<!-- ========== PDF处理 ========== -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
@@ -188,6 +120,25 @@
<artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version>
</dependency>
<!-- ========== 压缩文件处理 ========== -->
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding</artifactId>
<version>${sevenzipjbinding.version}</version>
</dependency>
<dependency>
<groupId>net.sf.sevenzipjbinding</groupId>
<artifactId>sevenzipjbinding-all-platforms</artifactId>
<version>${sevenzipjbinding.version}</version>
</dependency>
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>${junrar.version}</version>
</dependency>
<!-- ========== 图像处理 ========== -->
<dependency>
<groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-jpeg2000</artifactId>
@@ -203,79 +154,12 @@
<artifactId>jbig2-imageio</artifactId>
<version>${jbig2-imageio.version}</version>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-cad</artifactId>
<version>${aspose-cad.version}</version>
</dependency>
<!-- 密钥算法 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bcprov-jdk15on.version}</version>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>${commons-imaging.version}</version>
</dependency>
<!-- url 规范化 -->
<dependency>
<groupId>io.mola.galimatias</groupId>
<artifactId>galimatias</artifactId>
<version>${galimatias.version}</version>
</dependency>
<!-- 以下是bytedeco 基于opencv ffmpeg封装的javacv用于视频处理 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${bytedeco.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${bytedeco.version}</version>
</dependency>
<!-- 此版本中主要兼容linux和windows系统如需兼容其他系统平台请引入对应依赖即可 -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>${opencv.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>${opencv.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>${itextpdf.version}</version>
</dependency>
<!-- JAI 系统依赖 -->
<dependency>
<groupId>javax.media</groupId>
<artifactId>jai_core</artifactId>
@@ -291,25 +175,134 @@
<systemPath>${pom.basedir}/lib/jai_codec-1.1.3.jar</systemPath>
</dependency>
<!-- test dependency - start -->
<!-- ========== 视频处理 (JavaCV) ========== -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${bytedeco.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>${bytedeco.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>${opencv.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>${opencv.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>${openblas.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
<classifier>linux-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>${ffmpeg.version}</version>
<classifier>windows-x86_64</classifier>
</dependency>
<!-- ========== 网络通信 ========== -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
<!-- ========== 缓存和存储 ========== -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.rocksdb</groupId>
<artifactId>rocksdbjni</artifactId>
<version>${rocksdb.version}</version>
</dependency>
<dependency>
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
<artifactId>concurrentlinkedhashmap-lru</artifactId>
<version>${concurrentlinkedhashmap.version}</version>
</dependency>
<!-- ========== 编码检测和字符处理 ========== -->
<dependency>
<groupId>com.googlecode.juniversalchardet</groupId>
<artifactId>juniversalchardet</artifactId>
<version>${juniversalchardet.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.jchardet</groupId>
<artifactId>jchardet</artifactId>
<version>${jchardet.version}</version>
</dependency>
<!-- ========== Apache Commons 工具库 ========== -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>${commons-cli.version}</version>
</dependency>
<!-- ========== 其他工具库 ========== -->
<dependency>
<groupId>antlr</groupId>
<artifactId>antlr</artifactId>
<version>${antlr.version}</version>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>${xstream.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bcprov-jdk15on.version}</version>
</dependency>
<dependency>
<groupId>io.mola.galimatias</groupId>
<artifactId>galimatias</artifactId>
<version>${galimatias.version}</version>
</dependency>
<!-- ========== 测试依赖 ========== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${httpclient.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- test dependency - end -->
</dependencies>
<build>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,478 @@
###############################################################################
# 服务器基础配置需要重启生效
###############################################################################
# 服务器端口号默认8012
# 可以通过环境变量 KK_SERVER_PORT 覆盖
server.port = ${KK_SERVER_PORT:8012}
# 应用上下文路径默认为根路径 /
# 可以通过环境变量 KK_CONTEXT_PATH 覆盖
server.servlet.context-path = ${KK_CONTEXT_PATH:/}
# 字符编码设置统一使用UTF-8
server.servlet.encoding.charset = utf-8
# 启用响应压缩减少网络传输
server.compression.enabled = true
server.compression.min-response-size = 2048
server.compression.mime-types = application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain,font/woff,application/font-woff,font/eot,image/svg+xml,image/x-icon
# 文件上传大小限制默认500MB
# 注意需要同时设置spring.servlet.multipart.max-file-size和max-request-size
spring.servlet.multipart.max-file-size = 500MB
spring.servlet.multipart.max-request-size = 500MB
# FreeMarker模板引擎配置
spring.freemarker.template-loader-path = classpath:/web/
spring.freemarker.cache = false
spring.freemarker.charset = UTF-8
spring.freemarker.check-template-location = true
spring.freemarker.content-type = text/html
spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.request-context-attribute = request
spring.freemarker.suffix = .ftl
# Spring Boot Actuator监控端点配置
management.endpoints.web.exposure.include = health,info,metrics
management.endpoint.health.show-details = always
management.health.defaults.enabled = true
###############################################################################
# Office文档处理配置部分支持动态配置
###############################################################################
# Office组件安装路径默认为自动查找
# Windows示例注意双反斜杠C:\\Program Files (x86)\\OpenOffice 4
# Linux示例/opt/libreoffice
# MacOS示例/Applications/LibreOffice.app/Contents
office.home = ${KK_OFFICE_HOME:default}
# Office组件服务端口支持多个端口实现负载均衡
office.plugin.server.ports = 2001,2002
# Office组件任务超时时间默认5分钟
office.plugin.task.timeout = 5m
# 每个进程最大任务数防止内存溢出
office.plugin.task.maxtasksperprocess = 200
# 任务执行超时时间默认5分钟
office.plugin.task.taskexecutiontimeout = 5m
# Office文档分页范围支持动态配置
# 默认false开启后可以指定转换的页面范围
office.pagerange = ${KK_OFFICE_PAGERANGE:false}
# Office文档水印功能支持动态配置
# 默认false开启后会在Office文档上添加水印
office.watermark = ${KK_OFFICE_WATERMARK:false}
# Office图片质量1-100默认80
# 值越高图片质量越好但文件越大
office.quality = ${KK_OFFICE_QUALITY:80}
# Office图片最大分辨率默认150
# 控制生成图片的最大分辨率
office.maximageresolution = ${KK_OFFICE_MAXIMAGERESOLUTION:150}
# 导出Office书签支持动态配置
# 默认true转换PDF时保留书签
office.exportbookmarks = ${KK_OFFICE_EXPORTBOOKMARKS:true}
# 是否将Office文档中的批注作为PDF注释导出默认为true导出
# 保留批注便于文档审阅
office.exportnotes = ${KK_OFFICE_EXPORTNOTES:true}
# 加密文档生成的PDF是否添加密码默认为true添加
# 密码为原始加密文档的密码增强文档安全性
office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true}
# Excel文档(xlsx)的前端解析方式默认为webWeb端解析
# web: 使用前端SheetJS库解析减轻服务器压力
# image: 服务器转换为图片兼容性更好
office.type.web = ${KK_OFFICE_TYPE_WEB:web}
# Office文档预览类型
# 支持动态配置可选值image/pdf
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
# 是否关闭Office预览模式切换开关默认为false允许切换
# 设置为true时用户无法在图片和PDF模式间切换
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
###############################################################################
# CAD文件处理配置支持动态配置
###############################################################################
# CAD文件预览类型
# svg: 转换为SVG矢量格式缩放不失真
# pdf: 转换为PDF格式便于打印和标注
cad.preview.type = ${KK_CAD_PREVIEW_TYPE:svg}
# Cad转换模块设置(aspose-cad=1 ,cadviewer=3)
# aspose-cad 默认集成到系统,但是特别吃服务器性能 (支持转换格式为 svgpdf)
# cadviewer 下载地址 https://cadviewer.com/alldownloads/autoxchange/ 更具自己系统下载转换包(支持转换格式为 svg、svgz、pdf)
# 1=aspose-cad 转换格式为 pdf,svg,tif 支持类型最多
# 2=cadviewer 转换格式为 pdf,svg 支持的类型 dwg dxf dwf
cad.conversionmodule = 2
# Cad 后端转换包路径 linux 严格注意大小写
# cadviewer windows 修改名称为 cadviewer.exe linux修改名称为 cadviewer 需要安装字体
# cadviewer 字体下载 https://cadviewer.com/downloads/fonts/fonts.tar.gz 放在 cad.file.path 目录里面的fonts.
cad.cadconverterpath = D:/github/AutoXChange/
# CAD文件处理线程数
cad.thread = ${KK_CAD_THREAD:5}
# CAD文件处理超时时间
cad.timeout = ${KK_CAD_TIMEOUT:90}
###############################################################################
# PDF文件处理配置支持动态配置
###############################################################################
# 是否禁止PDF演示模式默认为true禁止
pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
# 是否禁止PDF文件菜单中的"打开文件"选项默认为true禁止
pdf.openFile.disable = ${KK_PDF_OPEN_FILE_DISABLE:true}
# 是否禁止PDF打印功能默认为true禁止
pdf.print.disable = ${KK_PDF_PRINT_DISABLE:true}
# 是否禁止PDF下载功能默认为true禁止
pdf.download.disable = ${KK_PDF_DOWNLOAD_DISABLE:true}
# 是否禁止PDF书签/大纲功能默认为true禁止
pdf.bookmark.disable = ${KK_PDF_BOOKMARK_DISABLE:true}
# 是否禁止PDF编辑功能注释表单等默认为false允许编辑
pdf.disable.editing = ${KK_PDF_DISABLE_EDITING:false}
# PDF处理最大线程数控制并发处理能力
pdf.max.threads = 10
# PDF处理超时配置
pdf.timeout.small = 90
pdf.timeout.medium = 180
pdf.timeout.large = 300
pdf.timeout.xlarge = 600
# PDF智能DPI优化
# 是否启用PDF DPI智能调整默认为true启用
# 根据PDF页数自动调整DPI平衡清晰度和性能
pdf.dpi.enabled = true
# PDF转图片的基准DPI默认为144
# 当DPI优化禁用时使用此值
pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
# 智能DPI分级配置
# 小文件0-50页150 DPI高质量
pdf.dpi.small = 150
# 中等文件50-100页120 DPI平衡质量与性能
pdf.dpi.medium = 120
# 大文件100-200页96 DPI优化性能
pdf.dpi.large = 96
# 超大文件200-500页72 DPI快速转换
pdf.dpi.xlarge = 72
# 巨量文件>500页72 DPI最小资源消耗
pdf.dpi.xxlarge = 72
###############################################################################
# TIF文件处理配置支持动态配置
###############################################################################
# TIF文件预览类型
# tif: 使用前端Tiff.js插件直接浏览需要浏览器支持
# jpg: 服务器转换为JPG格式后显示
# pdf: 服务器转换为PDF格式显示
tif.preview.type = ${KK_TIF_PREVIEW_TYPE:tif}
# TIF文件处理线程数
tif.thread = 5
# TIF文件处理超时时间
tif.timeout = 90
###############################################################################
# 媒体文件处理配置支持动态配置
###############################################################################
# 媒体文件类型音频视频
media = ${KK_MEDIA:mp3,wav,mp4,flv,mpd,m3u8,ts,mpeg,m4a}
# 需要转换的媒体文件类型
convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm,mpeg}
# 媒体文件超时控制
media.timeout.enabled = true
media.small.file.timeout = 30
media.medium.file.timeout = 60
media.large.file.timeout = 180
media.xl.file.timeout = 300
media.xxl.file.timeout = 600
media.xxxl.file.timeout = 1200
# 媒体文件转换最大大小MB
media.convert.max.size = 300
# 是否禁用视频格式转换功能默认为false禁用
# 重要视频转换非常消耗CPU和内存资源
media.convert.disable = ${KK_MEDIA_CONVERT_DISABLE:true}
###############################################################################
# 文件存储与缓存配置支持动态配置
###############################################################################
# 预览生成资源的存储路径默认为应用根路径下的file目录
# Windows示例D:\\kkFileview\\注意双反斜杠
# Linux示例/opt/kkfileview/file/
# 重要确保应用有该目录的读写权限
file.dir = ${KK_FILE_DIR:default}
# 允许预览的本地文件夹路径默认为default禁止所有本地文件预览
# 安全警告配置此路径可能允许访问系统文件请谨慎配置
# Windows示例注意前面加反斜杠\D:\\kkFileview\\1\\1.txt
# Linux示例注意前面加正斜杠/opt/1.txt
# 使用file协议访问file://d:/1/1.txtWindows或 file:/opt/1.txtLinux
local.preview.dir = \D:\\
# 是否启用缓存支持动态配置
# 默认true开启缓存提高性能
cache.enabled = ${KK_CACHE_ENABLED:true}
# 缓存实现类型默认为jdk使用JDK内置对象实现
# 可选值
# jdk: JDK内置ConcurrentHashMap单机部署推荐
# redis: Redis分布式缓存集群部署推荐
# default: 内嵌RocksDB支持持久化
cache.type = ${KK_CACHE_TYPE:jdk}
# Redis部署模式默认为single单机模式
# 可选值
# single: 单机模式默认
# cluster: 集群模式
# sentinel: 哨兵模式高可用
# master-slave: 主从模式
spring.redisson.mode = single
# Redis连接地址支持多种格式
# 单机模式redis://127.0.0.1:6379
# 集群模式redis://node1:6379,redis://node2:6379,redis://node3:6379
# 哨兵模式redis://sentinel1:26379,redis://sentinel2:26379
spring.redisson.address = ${KK_SPRING_REDISSON_ADDRESS:redis://127.0.0.1:6379}
# Redis连接密码无密码时留空
spring.redisson.password = ${KK_SPRING_REDISSON_PASSWORD:}
# Redis数据库索引默认为00-15
# 不同业务可使用不同数据库隔离
spring.redisson.database = ${KK_SPRING_REDISSON_DATABASE:0}
# 缓存清理配置
# 是否启用缓存自动清理默认为true启用
# 定期清理过期缓存避免磁盘空间无限增长
cache.clean.enabled = ${KK_CACHE_CLEAN_ENABLED:true}
# 缓存自动清理时间使用Quartz cron表达式默认为每天凌晨3点执行
# 表达式格式 可选
# 0 0 3 * * ? 表示每天3:00:00执行清理
cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
###############################################################################
# 安全与访问控制配置支持动态配置
###############################################################################
# 提供预览服务的地址默认从请求url读如果使用nginx等反向代理需要手动设置
# base.url = https://file.keking.cn
base.url = ${KK_BASE_URL:default}
# 信任站点白名单配置多个用','隔开
# 安全提示为防止SSRF攻击强烈建议配置信任主机白名单
# 如果不配置系统将默认拒绝所有外部文件预览请求
# 配置示例
# trust.host = kkview.cn,yourdomain.com,cdn.example.com
# 如果需要允许所有域名不推荐仅用于测试环境请设置为
# trust.host = *
# 当前配置默认本机测试 正式启用请修改
trust.host = *
# 不信任站点黑名单配置多个用逗号隔开
# 黑名单优先级高于白名单设置后将禁止预览来自这些站点的文件
# 建议配置禁止访问内网地址和本地地址防止内部信息泄露
# 配置示例
# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.*,172.17.*,172.18.*,172.19.*,172.20.*,172.21.*,172.22.*,172.23.*,172.24.*,172.25.*,172.26.*,172.27.*,172.28.*,172.29.*,172.30.*,172.31.*
not.trust.host = ${KK_NOT_TRUST_HOST:default}
# 禁止访问的文件类型安全限制
# 支持动态配置格式exe,dll,dat
prohibit = ${KK_PROHIBIT:exe,dll,dat}
# 是否忽略SSL证书验证默认为true忽略
# 用于开发环境或自签名证书场景
# 生产环境建议设置为false启用完整的证书验证
kk.ignore.ssl = true
# 是否启用URL重定向功能默认为true启用
# 用于处理文件下载外部资源引用等场景
kk.enable.redirect = true
###############################################################################
# 水印配置支持动态配置
###############################################################################
# 水印文本内容
# 可以通过环境变量 WATERMARK_TXT 覆盖
watermark.txt = ${WATERMARK_TXT:}
# 水印X轴间距
# 可以通过环境变量 WATERMARK_X_SPACE 覆盖
watermark.x.space = ${WATERMARK_X_SPACE:10}
# 水印Y轴间距
# 可以通过环境变量 WATERMARK_Y_SPACE 覆盖
watermark.y.space = ${WATERMARK_Y_SPACE:10}
# 水印字体
# 可以通过环境变量 WATERMARK_FONT 覆盖
watermark.font = ${WATERMARK_FONT:微软雅黑}
# 水印字体大小
# 可以通过环境变量 WATERMARK_FONTSIZE 覆盖
watermark.fontsize = ${WATERMARK_FONTSIZE:18px}
# 水印颜色
# 可以通过环境变量 WATERMARK_COLOR 覆盖
watermark.color = ${WATERMARK_COLOR:black}
# 水印透明度0.0-1.0
# 可以通过环境变量 WATERMARK_ALPHA 覆盖
watermark.alpha = ${WATERMARK_ALPHA:0.2}
# 水印宽度
# 可以通过环境变量 WATERMARK_WIDTH 覆盖
watermark.width = ${WATERMARK_WIDTH:180}
# 水印高度
# 可以通过环境变量 WATERMARK_HEIGHT 覆盖
watermark.height = ${WATERMARK_HEIGHT:80}
# 水印旋转角度
# 可以通过环境变量 WATERMARK_ANGLE 覆盖
watermark.angle = ${WATERMARK_ANGLE:10}
###############################################################################
# FTP文件访问配置支持动态配置
###############################################################################
# FTP模块设置
# 预览源为FTP时可在ftp url后面加参数?ftp.username=ftpuser&ftp.password=123456&ftp.control.encoding=GBK,指定,不指定默认用配置的 (为了安全我们强烈建议在配置中设置相关信息)
# ftp.control.encodin (根据FTP服务器操作系统选择Linux一般为UTF-8Windows一般为GBK)
# 使用方法,支持,分割第一个是域名或者IP地址后面是用户名在后面是密码,在后面是编码(用户名密码和编码用:分割, 域名用,分割),切记url不需要任何协议头
# ftp.username = 地址:端口:用户名:密码:编码,192.168.0.2:21:name:123456:UTF-8,www.xxx.com:21:admin:pass:UTF-8 多客户端,分割
# ftp.username =10.99.1.2:21:666:88888:GBK
ftp.username = false
###############################################################################
# 十一首页与文件管理配置支持动态配置
###############################################################################
# 是否禁用首页文件上传功能默认为true禁用
# 设置为true可关闭上传功能仅用于预览
file.upload.disable = false
# 网站备案信息显示在首页底部默认为空
beian = ${KK_BEIAN:default}
# 首页初始化加载的页码默认为1第一页
home.pagenumber = ${DEFAULT_HOME_PAGENUMBER:1}
# 首页每页显示的文件数量默认为20
home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
# 文件删除验证配置
# 是否启用验证码验证删除文件默认为false不启用
# 启用后删除文件需要输入验证码防止误删
delete.captcha = ${KK_DELETE_CAPTCHA:false}
# 删除文件密码默认为123456
delete.password = ${KK_DELETE_PASSWORD:123456}
# 是否删除转换后的源文件默认为true删除
# 启用可节约磁盘空间但会丢失原始文件
delete.source.file = ${KK_DELETE_SOURCE_FILE:true}
###############################################################################
# 十二权限与认证配置支持动态配置
###############################################################################
# 是否启用图片预览权限默认为true启用
# 设置为false可禁用所有图片预览功能
kk.Picturespreview = true
# 是否启用跨域文件获取权限默认为true启用
kk.Getcorsfile = true
# 是否启用添加异步任务权限默认为true启用
# 大文件转换通常使用异步任务处理
kk.addTask = true
# API密钥功能默认为false禁用
# 启用后需要提供密钥才能调用API
kk.key = false
# AES加密密钥必须为16位字符
# 启用AES加密时接入方需使用相同的密钥
# 用于敏感数据传输加密
aes.key = 1234567890123456
# Basic认证配置格式域名:用户名:密码多个用逗号分隔
# 用于保护特定域名的访问
# 示例192.168.0.1:admin:pass123,example.com:user:pass456
basic.name = 10.99.1.2:aaa:bbb
# User-Agent验证字符串默认不启用
# 可用于简单的客户端验证
useragent = false
###############################################################################
# 十三高级功能与兼容性配置支持动态配置
###############################################################################
# 异步配置刷新定时时间
kk.refreshschedule = 2
# 首页是否显示AES密钥 默认为false禁用
kk.isshowaeskey = false
# 是否允许XLSX编辑
kk.xlsxallowedit = true
# 是否显示XLSX工具栏
kk.xlsxshowtoolbar = true
# 首页是否显示key密钥 默认为false禁用
kk.isshowkey = true
# 预览html文件 是否启用JavaScript 默认为true启用
kk.scriptjs = true
###############################################################################
# 十四文件类型分类配置支持动态配置
###############################################################################
# 纯文本文件类型直接显示
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}

File diff suppressed because it is too large Load Diff

View File

@@ -6,213 +6,282 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.*;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
/**
* @auther: chenjh
* @time: 2019/4/10 16:16
* @description 每隔1s读取并更新一次配置文件
*/
@Component
public class ConfigRefreshComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class);
private static final long DEBOUNCE_DELAY_SECONDS = 5;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final ExecutorService watchServiceExecutor = Executors.newSingleThreadExecutor();
private final Object lock = new Object();
private ScheduledFuture<?> scheduledReloadTask;
private WatchService watchService;
private volatile boolean running = true;
@PostConstruct
void refresh() {
Thread configRefreshThread = new Thread(new ConfigRefreshThread());
configRefreshThread.start();
void init() {
loadConfig();
watchServiceExecutor.submit(this::watchConfigFile);
}
static class ConfigRefreshThread implements Runnable {
@Override
public void run() {
@PreDestroy
void destroy() {
running = false;
watchServiceExecutor.shutdownNow();
scheduler.shutdownNow();
if (watchService != null) {
try {
watchService.close();
} catch (IOException e) {
LOGGER.warn("关闭 WatchService 时发生异常", e);
}
}
}
private void watchConfigFile() {
try {
String configFilePath = ConfigUtils.getCustomizedConfigPath();
Path configPath = Paths.get(configFilePath);
Path configDir = configPath.getParent();
if (configDir == null) {
LOGGER.error("配置文件路径无效: {}", configFilePath);
return;
}
watchService = FileSystems.getDefault().newWatchService();
configDir.register(watchService,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE);
LOGGER.info("开始监听配置文件: {}", configFilePath);
while (running && !Thread.currentThread().isInterrupted()) {
try {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path changedPath = (Path) event.context();
if (changedPath.equals(configPath.getFileName())) {
handleConfigChange(kind);
}
}
if (!key.reset()) {
LOGGER.warn("WatchKey 无法重置");
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
} catch (IOException e) {
LOGGER.error("初始化配置文件监听失败", e);
}
}
private void handleConfigChange(WatchEvent.Kind<?> kind) {
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
running = false;
return;
}
if (kind == StandardWatchEventKinds.ENTRY_MODIFY ||
kind == StandardWatchEventKinds.ENTRY_CREATE) {
synchronized (lock) {
if (scheduledReloadTask != null && !scheduledReloadTask.isDone()) {
scheduledReloadTask.cancel(false);
}
scheduledReloadTask = scheduler.schedule(() -> {
try {
loadConfig();
LOGGER.info("配置文件已重新加载");
} catch (Exception e) {
LOGGER.error("重新加载配置失败", e);
}
}, DEBOUNCE_DELAY_SECONDS, TimeUnit.SECONDS);
}
}
}
private void loadConfig() {
synchronized (lock) {
try {
Properties properties = new Properties();
String text;
String media;
boolean cacheEnabled;
String[] textArray;
String[] mediaArray;
String officePreviewType;
String officePreviewSwitchDisabled;
String ftpUsername;
String ftpPassword;
String ftpControlEncoding;
String configFilePath = ConfigUtils.getCustomizedConfigPath();
String baseUrl;
String trustHost;
String notTrustHost;
String pdfPresentationModeDisable;
String pdfOpenFileDisable;
String pdfPrintDisable;
String pdfDownloadDisable;
String pdfBookmarkDisable;
String pdfDisableEditing;
boolean fileUploadDisable;
String tifPreviewType;
String prohibit;
String[] prohibitArray;
String beian;
String size;
String password;
int pdf2JpgDpi;
String officeTypeWeb;
String cadPreviewType;
boolean deleteSourceFile;
boolean deleteCaptcha;
String officPageRange;
String officWatermark;
String officQuality;
String officMaxImageResolution;
boolean officExportBookmarks;
boolean officeExportNotes;
boolean officeDocumentOpenPasswords;
String cadTimeout;
int cadThread;
String homePageNumber;
String homePagination;
String homePageSize;
String homeSearch;
int pdfTimeout;
int pdfTimeout80;
int pdfTimeout200;
int pdfThread;
while (true) {
FileReader fileReader = new FileReader(configFilePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
Path configPath = Paths.get(configFilePath);
if (!Files.exists(configPath)) {
LOGGER.warn("配置文件不存在: {}", configFilePath);
return;
}
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(configFilePath))) {
properties.load(bufferedReader);
ConfigUtils.restorePropertiesFromEnvFormat(properties);
cacheEnabled = Boolean.parseBoolean(properties.getProperty("cache.enabled", ConfigConstants.DEFAULT_CACHE_ENABLED));
text = properties.getProperty("simText", ConfigConstants.DEFAULT_TXT_TYPE);
media = properties.getProperty("media", ConfigConstants.DEFAULT_MEDIA_TYPE);
officePreviewType = properties.getProperty("office.preview.type", ConfigConstants.DEFAULT_OFFICE_PREVIEW_TYPE);
officePreviewSwitchDisabled = properties.getProperty("office.preview.switch.disabled", ConfigConstants.DEFAULT_OFFICE_PREVIEW_SWITCH_DISABLED);
ftpUsername = properties.getProperty("ftp.username", ConfigConstants.DEFAULT_FTP_USERNAME);
ftpPassword = properties.getProperty("ftp.password", ConfigConstants.DEFAULT_FTP_PASSWORD);
ftpControlEncoding = properties.getProperty("ftp.control.encoding", ConfigConstants.DEFAULT_FTP_CONTROL_ENCODING);
textArray = text.split(",");
mediaArray = media.split(",");
baseUrl = properties.getProperty("base.url", ConfigConstants.DEFAULT_VALUE);
trustHost = properties.getProperty("trust.host", ConfigConstants.DEFAULT_VALUE);
notTrustHost = properties.getProperty("not.trust.host", ConfigConstants.DEFAULT_VALUE);
pdfPresentationModeDisable = properties.getProperty("pdf.presentationMode.disable", ConfigConstants.DEFAULT_PDF_PRESENTATION_MODE_DISABLE);
pdfOpenFileDisable = properties.getProperty("pdf.openFile.disable", ConfigConstants.DEFAULT_PDF_OPEN_FILE_DISABLE);
pdfPrintDisable = properties.getProperty("pdf.print.disable", ConfigConstants.DEFAULT_PDF_PRINT_DISABLE);
pdfDownloadDisable = properties.getProperty("pdf.download.disable", ConfigConstants.DEFAULT_PDF_DOWNLOAD_DISABLE);
pdfBookmarkDisable = properties.getProperty("pdf.bookmark.disable", ConfigConstants.DEFAULT_PDF_BOOKMARK_DISABLE);
pdfDisableEditing = properties.getProperty("pdf.disable.editing", ConfigConstants.DEFAULT_PDF_DISABLE_EDITING);
fileUploadDisable = Boolean.parseBoolean(properties.getProperty("file.upload.disable", ConfigConstants.DEFAULT_FILE_UPLOAD_DISABLE));
tifPreviewType = properties.getProperty("tif.preview.type", ConfigConstants.DEFAULT_TIF_PREVIEW_TYPE);
cadPreviewType = properties.getProperty("cad.preview.type", ConfigConstants.DEFAULT_CAD_PREVIEW_TYPE);
size = properties.getProperty("spring.servlet.multipart.max-file-size", ConfigConstants.DEFAULT_SIZE);
beian = properties.getProperty("beian", ConfigConstants.DEFAULT_BEIAN);
prohibit = properties.getProperty("prohibit", ConfigConstants.DEFAULT_PROHIBIT);
password = properties.getProperty("delete.password", ConfigConstants.DEFAULT_PASSWORD);
pdf2JpgDpi = Integer.parseInt(properties.getProperty("pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI));
officeTypeWeb = properties.getProperty("office.type.web", ConfigConstants.DEFAULT_OFFICE_TYPE_WEB);
deleteSourceFile = Boolean.parseBoolean(properties.getProperty("delete.source.file", ConfigConstants.DEFAULT_DELETE_SOURCE_FILE));
deleteCaptcha = Boolean.parseBoolean(properties.getProperty("delete.captcha", ConfigConstants.DEFAULT_DELETE_CAPTCHA));
officPageRange = properties.getProperty("office.pagerange", ConfigConstants.DEFAULT_OFFICE_PAQERANQE);
officWatermark = properties.getProperty("office.watermark", ConfigConstants.DEFAULT_OFFICE_WATERMARK);
officQuality = properties.getProperty("office.quality", ConfigConstants.DEFAULT_OFFICE_QUALITY);
officMaxImageResolution = properties.getProperty("office.maximageresolution", ConfigConstants.DEFAULT_OFFICE_MAXIMAQERESOLUTION);
officExportBookmarks = Boolean.parseBoolean(properties.getProperty("office.exportbookmarks", ConfigConstants.DEFAULT_OFFICE_EXPORTBOOKMARKS));
officeExportNotes = Boolean.parseBoolean(properties.getProperty("office.exportnotes", ConfigConstants.DEFAULT_OFFICE_EXPORTNOTES));
officeDocumentOpenPasswords = Boolean.parseBoolean(properties.getProperty("office.documentopenpasswords", ConfigConstants.DEFAULT_OFFICE_EOCUMENTOPENPASSWORDS));
cadTimeout = properties.getProperty("cad.timeout", ConfigConstants.DEFAULT_CAD_TIMEOUT);
homePageNumber = properties.getProperty("home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER);
homePagination = properties.getProperty("home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION);
homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
cadThread = Integer.parseInt(properties.getProperty("cad.thread", ConfigConstants.DEFAULT_CAD_THREAD));
pdfTimeout = Integer.parseInt(properties.getProperty("pdf.timeout", ConfigConstants.DEFAULT_PDF_TIMEOUT));
pdfTimeout80 = Integer.parseInt(properties.getProperty("pdf.timeout80", ConfigConstants.DEFAULT_PDF_TIMEOUT80));
pdfTimeout200 = Integer.parseInt(properties.getProperty("pdf.timeout200", ConfigConstants.DEFAULT_PDF_TIMEOUT200));
pdfThread = Integer.parseInt(properties.getProperty("pdf.thread", ConfigConstants.DEFAULT_PDF_THREAD));
prohibitArray = prohibit.split(",");
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
ConfigConstants.setSimTextValue(textArray);
ConfigConstants.setMediaValue(mediaArray);
ConfigConstants.setOfficePreviewTypeValue(officePreviewType);
ConfigConstants.setFtpUsernameValue(ftpUsername);
ConfigConstants.setFtpPasswordValue(ftpPassword);
ConfigConstants.setFtpControlEncodingValue(ftpControlEncoding);
ConfigConstants.setBaseUrlValue(baseUrl);
ConfigConstants.setTrustHostValue(trustHost);
ConfigConstants.setNotTrustHostValue(notTrustHost);
ConfigConstants.setOfficePreviewSwitchDisabledValue(officePreviewSwitchDisabled);
ConfigConstants.setPdfPresentationModeDisableValue(pdfPresentationModeDisable);
ConfigConstants.setPdfOpenFileDisableValue(pdfOpenFileDisable);
ConfigConstants.setPdfPrintDisableValue(pdfPrintDisable);
ConfigConstants.setPdfDownloadDisableValue(pdfDownloadDisable);
ConfigConstants.setPdfBookmarkDisableValue(pdfBookmarkDisable);
ConfigConstants.setPdfDisableEditingValue(pdfDisableEditing);
ConfigConstants.setFileUploadDisableValue(fileUploadDisable);
ConfigConstants.setTifPreviewTypeValue(tifPreviewType);
ConfigConstants.setCadPreviewTypeValue(cadPreviewType);
ConfigConstants.setBeianValue(beian);
ConfigConstants.setSizeValue(size);
ConfigConstants.setProhibitValue(prohibitArray);
ConfigConstants.setPasswordValue(password);
ConfigConstants.setPdf2JpgDpiValue(pdf2JpgDpi);
ConfigConstants.setOfficeTypeWebValue(officeTypeWeb);
ConfigConstants.setOfficePageRangeValue(officPageRange);
ConfigConstants.setOfficeWatermarkValue(officWatermark);
ConfigConstants.setOfficeQualityValue(officQuality);
ConfigConstants.setOfficeMaxImageResolutionValue(officMaxImageResolution);
ConfigConstants.setOfficeExportBookmarksValue(officExportBookmarks);
ConfigConstants.setOfficeExportNotesValue(officeExportNotes);
ConfigConstants.setOfficeDocumentOpenPasswordsValue(officeDocumentOpenPasswords);
ConfigConstants.setDeleteSourceFileValue(deleteSourceFile);
ConfigConstants.setDeleteCaptchaValue(deleteCaptcha);
ConfigConstants.setCadTimeoutValue(cadTimeout);
ConfigConstants.setCadThreadValue(cadThread);
ConfigConstants.setHomePageNumberValue(homePageNumber);
ConfigConstants.setHomePaginationValue(homePagination);
ConfigConstants.setHomePageSizeValue(homePageSize);
ConfigConstants.setHomeSearchValue(homeSearch);
ConfigConstants.setPdfTimeoutValue(pdfTimeout);
ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
ConfigConstants.setPdfThreadValue(pdfThread);
updateConfigConstants(properties);
setWatermarkConfig(properties);
bufferedReader.close();
fileReader.close();
TimeUnit.SECONDS.sleep(1);
LOGGER.info("配置文件重新加载完成");
}
} catch (IOException | InterruptedException e) {
} catch (IOException e) {
LOGGER.error("读取配置文件异常", e);
}
}
private void setWatermarkConfig(Properties properties) {
String watermarkTxt = properties.getProperty("watermark.txt", WatermarkConfigConstants.DEFAULT_WATERMARK_TXT);
String watermarkXSpace = properties.getProperty("watermark.x.space", WatermarkConfigConstants.DEFAULT_WATERMARK_X_SPACE);
String watermarkYSpace = properties.getProperty("watermark.y.space", WatermarkConfigConstants.DEFAULT_WATERMARK_Y_SPACE);
String watermarkFont = properties.getProperty("watermark.font", WatermarkConfigConstants.DEFAULT_WATERMARK_FONT);
String watermarkFontsize = properties.getProperty("watermark.fontsize", WatermarkConfigConstants.DEFAULT_WATERMARK_FONTSIZE);
String watermarkColor = properties.getProperty("watermark.color", WatermarkConfigConstants.DEFAULT_WATERMARK_COLOR);
String watermarkAlpha = properties.getProperty("watermark.alpha", WatermarkConfigConstants.DEFAULT_WATERMARK_ALPHA);
String watermarkWidth = properties.getProperty("watermark.width", WatermarkConfigConstants.DEFAULT_WATERMARK_WIDTH);
String watermarkHeight = properties.getProperty("watermark.height", WatermarkConfigConstants.DEFAULT_WATERMARK_HEIGHT);
String watermarkAngle = properties.getProperty("watermark.angle", WatermarkConfigConstants.DEFAULT_WATERMARK_ANGLE);
WatermarkConfigConstants.setWatermarkTxtValue(watermarkTxt);
WatermarkConfigConstants.setWatermarkXSpaceValue(watermarkXSpace);
WatermarkConfigConstants.setWatermarkYSpaceValue(watermarkYSpace);
WatermarkConfigConstants.setWatermarkFontValue(watermarkFont);
WatermarkConfigConstants.setWatermarkFontsizeValue(watermarkFontsize);
WatermarkConfigConstants.setWatermarkColorValue(watermarkColor);
WatermarkConfigConstants.setWatermarkAlphaValue(watermarkAlpha);
WatermarkConfigConstants.setWatermarkWidthValue(watermarkWidth);
WatermarkConfigConstants.setWatermarkHeightValue(watermarkHeight);
WatermarkConfigConstants.setWatermarkAngleValue(watermarkAngle);
}
}
}
private void updateConfigConstants(Properties properties) {
// 1. 缓存配置
boolean cacheEnabled = Boolean.parseBoolean(getProperty(properties, "cache.enabled", ConfigConstants.DEFAULT_CACHE_ENABLED));
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
// 2. 文件类型配置
ConfigConstants.setSimTextValue(getProperty(properties, "simText", ConfigConstants.DEFAULT_TXT_TYPE).split(","));
ConfigConstants.setMediaValue(getProperty(properties, "media", ConfigConstants.DEFAULT_MEDIA_TYPE).split(","));
ConfigConstants.setConvertMediaValue(getProperty(properties, "convertMedias", "avi,mov,wmv,mkv,3gp,rm").split(","));
ConfigConstants.setProhibitValue(getProperty(properties, "prohibit", ConfigConstants.DEFAULT_PROHIBIT).split(","));
ConfigConstants.setMediaConvertDisableValue(getProperty(properties, "media.convert.disable", "true"));
ConfigConstants.setTifPreviewTypeValue(getProperty(properties, "tif.preview.type", ConfigConstants.DEFAULT_TIF_PREVIEW_TYPE));
ConfigConstants.setCadPreviewTypeValue(getProperty(properties, "cad.preview.type", ConfigConstants.DEFAULT_CAD_PREVIEW_TYPE));
// 3. Office配置
ConfigConstants.setOfficePreviewTypeValue(getProperty(properties, "office.preview.type", ConfigConstants.DEFAULT_OFFICE_PREVIEW_TYPE));
ConfigConstants.setOfficePreviewSwitchDisabledValue(getProperty(properties, "office.preview.switch.disabled", ConfigConstants.DEFAULT_OFFICE_PREVIEW_SWITCH_DISABLED));
ConfigConstants.setOfficeTypeWebValue(getProperty(properties, "office.type.web", ConfigConstants.DEFAULT_OFFICE_TYPE_WEB));
ConfigConstants.setOfficePageRangeValue(getProperty(properties, "office.pagerange", ConfigConstants.DEFAULT_OFFICE_PAQERANQE));
ConfigConstants.setOfficeWatermarkValue(getProperty(properties, "office.watermark", ConfigConstants.DEFAULT_OFFICE_WATERMARK));
ConfigConstants.setOfficeQualityValue(getProperty(properties, "office.quality", ConfigConstants.DEFAULT_OFFICE_QUALITY));
ConfigConstants.setOfficeMaxImageResolutionValue(getProperty(properties, "office.maximageresolution", ConfigConstants.DEFAULT_OFFICE_MAXIMAQERESOLUTION));
ConfigConstants.setOfficeExportBookmarksValue(Boolean.parseBoolean(getProperty(properties, "office.exportbookmarks", ConfigConstants.DEFAULT_OFFICE_EXPORTBOOKMARKS)));
ConfigConstants.setOfficeExportNotesValue(Boolean.parseBoolean(getProperty(properties, "office.exportnotes", ConfigConstants.DEFAULT_OFFICE_EXPORTNOTES)));
ConfigConstants.setOfficeDocumentOpenPasswordsValue(Boolean.parseBoolean(getProperty(properties, "office.documentopenpasswords", ConfigConstants.DEFAULT_OFFICE_EOCUMENTOPENPASSWORDS)));
// 4. FTP配置
ConfigConstants.setFtpUsernameValue(getProperty(properties, "ftp.username", ConfigConstants.DEFAULT_FTP_USERNAME));
// 5. 路径配置
ConfigConstants.setBaseUrlValue(getProperty(properties, "base.url", ConfigConstants.DEFAULT_VALUE));
ConfigConstants.setFileDirValue(getProperty(properties, "file.dir", ConfigConstants.DEFAULT_VALUE));
ConfigConstants.setLocalPreviewDirValue(getProperty(properties, "local.preview.dir", ConfigConstants.DEFAULT_VALUE));
// 6. 安全配置
ConfigConstants.setTrustHostValue(getProperty(properties, "trust.host", ConfigConstants.DEFAULT_VALUE));
ConfigConstants.setNotTrustHostValue(getProperty(properties, "not.trust.host", ConfigConstants.DEFAULT_VALUE));
// 7. PDF配置
ConfigConstants.setPdfPresentationModeDisableValue(getProperty(properties, "pdf.presentationMode.disable", ConfigConstants.DEFAULT_PDF_PRESENTATION_MODE_DISABLE));
ConfigConstants.setPdfOpenFileDisableValue(getProperty(properties, "pdf.openFile.disable", ConfigConstants.DEFAULT_PDF_OPEN_FILE_DISABLE));
ConfigConstants.setPdfPrintDisableValue(getProperty(properties, "pdf.print.disable", ConfigConstants.DEFAULT_PDF_PRINT_DISABLE));
ConfigConstants.setPdfDownloadDisableValue(getProperty(properties, "pdf.download.disable", ConfigConstants.DEFAULT_PDF_DOWNLOAD_DISABLE));
ConfigConstants.setPdfBookmarkDisableValue(getProperty(properties, "pdf.bookmark.disable", ConfigConstants.DEFAULT_PDF_BOOKMARK_DISABLE));
ConfigConstants.setPdfDisableEditingValue(getProperty(properties, "pdf.disable.editing", ConfigConstants.DEFAULT_PDF_DISABLE_EDITING));
ConfigConstants.setPdf2JpgDpiValue(Integer.parseInt(getProperty(properties, "pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI)));
// 8. CAD配置
ConfigConstants.setCadTimeoutValue(getProperty(properties, "cad.timeout", ConfigConstants.DEFAULT_CAD_TIMEOUT));
ConfigConstants.setCadThreadValue(Integer.parseInt(getProperty(properties, "cad.thread", ConfigConstants.DEFAULT_CAD_THREAD)));
ConfigConstants.setCadConverterPathValue(getProperty(properties, "cad.cadconverterpath", ConfigConstants.DEFAULT_CAD_CONVERT));
ConfigConstants.setconversionModuleValue(Integer.parseInt(getProperty(properties, "cad.conversionmodule", ConfigConstants.DEFAULT_CAD_VERSION)));
// 9. TIF配置
ConfigConstants.setTifTimeoutValue(getProperty(properties, "tif.timeout", ConfigConstants.DEFAULT_TIF_TIMEOUT));
ConfigConstants.setTifThreadValue(Integer.parseInt(getProperty(properties, "tif.thread", ConfigConstants.DEFAULT_TIF_THREAD)));
// 10. 文件操作配置
ConfigConstants.setFileUploadDisableValue(Boolean.parseBoolean(getProperty(properties, "file.upload.disable", ConfigConstants.DEFAULT_FILE_UPLOAD_DISABLE)));
ConfigConstants.setSizeValue(getProperty(properties, "spring.servlet.multipart.max-file-size", ConfigConstants.DEFAULT_SIZE));
ConfigConstants.setPasswordValue(getProperty(properties, "delete.password", ConfigConstants.DEFAULT_PASSWORD));
ConfigConstants.setDeleteSourceFileValue(Boolean.parseBoolean(getProperty(properties, "delete.source.file", ConfigConstants.DEFAULT_DELETE_SOURCE_FILE)));
ConfigConstants.setDeleteCaptchaValue(Boolean.parseBoolean(getProperty(properties, "delete.captcha", ConfigConstants.DEFAULT_DELETE_CAPTCHA)));
// 11. 首页配置
ConfigConstants.setBeianValue(getProperty(properties, "beian", ConfigConstants.DEFAULT_BEIAN));
ConfigConstants.setHomePageNumberValue(getProperty(properties, "home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER));
ConfigConstants.setHomePaginationValue(getProperty(properties, "home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION));
ConfigConstants.setHomePageSizeValue(getProperty(properties, "home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE));
ConfigConstants.setHomeSearchValue(getProperty(properties, "home.search", ConfigConstants.DEFAULT_HOME_SEARCH));
// 12. 权限配置
ConfigConstants.setKeyValue(getProperty(properties, "kk.key", ConfigConstants.DEFAULT_KEY));
ConfigConstants.setPicturesPreviewValue(Boolean.parseBoolean(getProperty(properties, "kk.Picturespreview", ConfigConstants.DEFAULT_PICTURES_PREVIEW)));
ConfigConstants.setGetCorsFileValue(Boolean.parseBoolean(getProperty(properties, "kk.Getcorsfile", ConfigConstants.DEFAULT_GET_CORS_FILE)));
ConfigConstants.setAddTaskValue(Boolean.parseBoolean(getProperty(properties, "kk.addTask", ConfigConstants.DEFAULT_ADD_TASK)));
ConfigConstants.setaesKeyValue(getProperty(properties, "aes.key", ConfigConstants.DEFAULT_AES_KEY));
// 13. UserAgent配置
ConfigConstants.setUserAgentValue(getProperty(properties, "useragent", ConfigConstants.DEFAULT_USER_AGENT));
// 14. Basic认证配置
ConfigConstants.setBasicNameValue(getProperty(properties, "basic.name", ConfigConstants.DEFAULT_BASIC_NAME));
// 15. 视频转换配置
ConfigConstants.setMediaConvertMaxSizeValue(Integer.parseInt(getProperty(properties, "media.convert.max.size", ConfigConstants.DEFAULT_MEDIA_CONVERT_MAX_SIZE)));
ConfigConstants.setMediaTimeoutEnabledValue(Boolean.parseBoolean(getProperty(properties, "media.timeout.enabled", ConfigConstants.DEFAULT_MEDIA_TIMEOUT_ENABLED)));
ConfigConstants.setMediaSmallFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.small.file.timeout", ConfigConstants.DEFAULT_MEDIA_SMALL_FILE_TIMEOUT)));
ConfigConstants.setMediaMediumFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.medium.file.timeout", ConfigConstants.DEFAULT_MEDIA_MEDIUM_FILE_TIMEOUT)));
ConfigConstants.setMediaLargeFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.large.file.timeout", ConfigConstants.DEFAULT_MEDIA_LARGE_FILE_TIMEOUT)));
ConfigConstants.setMediaXLFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.xl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XL_FILE_TIMEOUT)));
ConfigConstants.setMediaXXLFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.xxl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XXL_FILE_TIMEOUT)));
ConfigConstants.setMediaXXXLFileTimeoutValue(Integer.parseInt(getProperty(properties, "media.xxxl.file.timeout", ConfigConstants.DEFAULT_MEDIA_XXXL_FILE_TIMEOUT)));
// 16. PDF DPI配置
ConfigConstants.setPdfDpiEnabledValue(Boolean.parseBoolean(getProperty(properties, "pdf.dpi.enabled", ConfigConstants.DEFAULT_PDF_DPI_ENABLED)));
ConfigConstants.setPdfSmallDpiValue(Integer.parseInt(getProperty(properties, "pdf.dpi.small", ConfigConstants.DEFAULT_PDF_SMALL_DTI)));
ConfigConstants.setPdfMediumDpiValue(Integer.parseInt(getProperty(properties, "pdf.dpi.medium", ConfigConstants.DEFAULT_PDF_MEDIUM_DPI)));
ConfigConstants.setPdfLargeDpiValue(Integer.parseInt(getProperty(properties, "pdf.dpi.large", ConfigConstants.DEFAULT_PDF_LARGE_DPI)));
ConfigConstants.setPdfXLargeDpiValue(Integer.parseInt(getProperty(properties, "pdf.dpi.xlarge", ConfigConstants.DEFAULT_PDF_XLARGE_DPI)));
ConfigConstants.setPdfXXLargeDpiValue(Integer.parseInt(getProperty(properties, "pdf.dpi.xxlarge", ConfigConstants.DEFAULT_PDF_XXLARGE_DPI)));
// 17. PDF超时配置
ConfigConstants.setPdfTimeoutSmallValue(Integer.parseInt(getProperty(properties, "pdf.timeout.small", ConfigConstants.DEFAULT_PDF_TIMEOUT_SMALL)));
ConfigConstants.setPdfTimeoutMediumValue(Integer.parseInt(getProperty(properties, "pdf.timeout.medium", ConfigConstants.DEFAULT_PDF_TIMEOUT_MEDIUM)));
ConfigConstants.setPdfTimeoutLargeValue(Integer.parseInt(getProperty(properties, "pdf.timeout.large", ConfigConstants.DEFAULT_PDF_TIMEOUT_LARGE)));
ConfigConstants.setPdfTimeoutXLargeValue(Integer.parseInt(getProperty(properties, "pdf.timeout.xlarge", ConfigConstants.DEFAULT_PDF_TIMEOUT_XLARGE)));
// 18. PDF线程配置
ConfigConstants.setPdfMaxThreadsValue(Integer.parseInt(getProperty(properties, "pdf.max.threads", ConfigConstants.DEFAULT_PDF_MAX_THREADS)));
// 19. CAD水印配置
ConfigConstants.setCadwatermarkValue(Boolean.parseBoolean(getProperty(properties, "cad.watermark", ConfigConstants.DEFAULT_CAD_WATERMARK)));
// 20. SSL忽略配置
ConfigConstants.setIgnoreSSLValue(Boolean.parseBoolean(getProperty(properties, "kk.ignore.ssl", ConfigConstants.DEFAULT_IGNORE_SSL)));
// 21. 重定向启用配置
ConfigConstants.setEnableRedirectValue(Boolean.parseBoolean(getProperty(properties, "kk.enable.redirect", ConfigConstants.DEFAULT_ENABLE_REDIRECT)));
// 22. 异步定时刷新
ConfigConstants.setRefreshScheduleValue(Integer.parseInt(getProperty(properties, "kk.refreshschedule", ConfigConstants.DEFAULT_ENABLE_REFRECSHSCHEDULE)));
// 23. 其他配置
ConfigConstants.setIsShowaesKeyValue(Boolean.parseBoolean(getProperty(properties, "kk.isshowaeskey", ConfigConstants.DEFAULT_SHOW_AES_KEY)));
ConfigConstants.setIsJavaScriptValue(Boolean.parseBoolean(getProperty(properties, "kk.isjavascript", ConfigConstants.DEFAULT_IS_JAVASCRIPT)));
ConfigConstants.setXlsxAllowEditValue(Boolean.parseBoolean(getProperty(properties, "kk.xlsxallowedit", ConfigConstants.DEFAULT_XLSX_ALLOW_EDIT)));
ConfigConstants.setXlsxShowtoolbarValue(Boolean.parseBoolean(getProperty(properties, "kk.xlsxshowtoolbar", ConfigConstants.DEFAULT_XLSX_SHOW_TOOLBAR)));
ConfigConstants.setisShowKeyValue(Boolean.parseBoolean(getProperty(properties, "kk.isshowkey", ConfigConstants.DEFAULT_IS_SHOW_KEY)));
ConfigConstants.setscriptJsValue(Boolean.parseBoolean(getProperty(properties, "kk.scriptjs", ConfigConstants.DEFAULT_SCRIPT_JS)));
}
private String getProperty(Properties properties, String key, String defaultValue) {
return properties.getProperty(key, defaultValue).trim();
}
private void setWatermarkConfig(Properties properties) {
WatermarkConfigConstants.setWatermarkTxtValue(getProperty(properties, "watermark.txt", WatermarkConfigConstants.DEFAULT_WATERMARK_TXT));
WatermarkConfigConstants.setWatermarkXSpaceValue(getProperty(properties, "watermark.x.space", WatermarkConfigConstants.DEFAULT_WATERMARK_X_SPACE));
WatermarkConfigConstants.setWatermarkYSpaceValue(getProperty(properties, "watermark.y.space", WatermarkConfigConstants.DEFAULT_WATERMARK_Y_SPACE));
WatermarkConfigConstants.setWatermarkFontValue(getProperty(properties, "watermark.font", WatermarkConfigConstants.DEFAULT_WATERMARK_FONT));
WatermarkConfigConstants.setWatermarkFontsizeValue(getProperty(properties, "watermark.fontsize", WatermarkConfigConstants.DEFAULT_WATERMARK_FONTSIZE));
WatermarkConfigConstants.setWatermarkColorValue(getProperty(properties, "watermark.color", WatermarkConfigConstants.DEFAULT_WATERMARK_COLOR));
WatermarkConfigConstants.setWatermarkAlphaValue(getProperty(properties, "watermark.alpha", WatermarkConfigConstants.DEFAULT_WATERMARK_ALPHA));
WatermarkConfigConstants.setWatermarkWidthValue(getProperty(properties, "watermark.width", WatermarkConfigConstants.DEFAULT_WATERMARK_WIDTH));
WatermarkConfigConstants.setWatermarkHeightValue(getProperty(properties, "watermark.height", WatermarkConfigConstants.DEFAULT_WATERMARK_HEIGHT));
WatermarkConfigConstants.setWatermarkAngleValue(getProperty(properties, "watermark.angle", WatermarkConfigConstants.DEFAULT_WATERMARK_ANGLE));
}
}

View File

@@ -2,6 +2,8 @@ package cn.keking.config;
import io.netty.channel.nio.NioEventLoopGroup;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec;
import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@@ -11,42 +13,123 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
/**
* Redisson 客户端配置
* Created by kl on 2017/09/26.
* redisson 客户端配置
*/
@ConditionalOnExpression("'${cache.type:default}'.equals('redis')")
@ConfigurationProperties(prefix = "spring.redisson")
@Configuration
public class RedissonConfig {
private String address;
private int connectionMinimumIdleSize = 10;
private int idleConnectionTimeout=10000;
private int pingTimeout=1000;
private int connectTimeout=10000;
private int timeout=3000;
private int retryAttempts=3;
private int retryInterval=1500;
private int reconnectionTimeout=3000;
private int failedAttempts=3;
private String password = null;
private int subscriptionsPerConnection=5;
private String clientName=null;
private int subscriptionConnectionMinimumIdleSize = 1;
private int subscriptionConnectionPoolSize = 50;
private int connectionPoolSize = 64;
private int database = 0;
private boolean dnsMonitoring = false;
private int dnsMonitoringInterval = 5000;
// ========================== 连接配置 ==========================
private static String address;
private static String password;
private static String clientName;
private static int database = 0;
private static String mode = "single";
private static String masterName = "kkfile";
private int thread; //当前处理核数量 * 2
// ========================== 超时配置 ==========================
private static int idleConnectionTimeout = 10000;
private static int connectTimeout = 10000;
private static int timeout = 3000;
private String codec="org.redisson.codec.JsonJacksonCodec";
// ========================== 重试配置 ==========================
private static int retryAttempts = 3;
private static int retryInterval = 1500;
// ========================== 连接池配置 ==========================
private static int connectionMinimumIdleSize = 10;
private static int connectionPoolSize = 64;
private static int subscriptionsPerConnection = 5;
private static int subscriptionConnectionMinimumIdleSize = 1;
private static int subscriptionConnectionPoolSize = 50;
// ========================== 其他配置 ==========================
private static int dnsMonitoringInterval = 5000;
private static int thread; // 当前处理核数量 * 2
private static String codec = "org.redisson.codec.JsonJacksonCodec";
@Bean
Config config() throws Exception {
public static RedissonClient config() throws Exception {
Config config = new Config();
config.useSingleServer().setAddress(address)
// 密码处理
if (StringUtils.isBlank(password)) {
password = null;
}
// 根据模式创建对应的 Redisson 配置
switch (mode) {
case "cluster":
configureClusterMode(config);
break;
case "master-slave":
configureMasterSlaveMode(config);
break;
case "sentinel":
configureSentinelMode(config);
break;
default:
configureSingleMode(config);
break;
}
return Redisson.create(config);
}
// ========================== 配置方法 ==========================
/**
* 配置集群模式
*/
private static void configureClusterMode(Config config) {
String[] clusterAddresses = address.split(",");
config.useClusterServers()
.setScanInterval(2000)
.addNodeAddress(clusterAddresses)
.setPassword(password)
.setRetryAttempts(retryAttempts)
.setTimeout(timeout)
.setMasterConnectionPoolSize(100)
.setSlaveConnectionPoolSize(100);
}
/**
* 配置主从模式
*/
private static void configureMasterSlaveMode(Config config) {
String[] masterSlaveAddresses = address.split(",");
validateMasterSlaveAddresses(masterSlaveAddresses);
String[] slaveAddresses = new String[masterSlaveAddresses.length - 1];
System.arraycopy(masterSlaveAddresses, 1, slaveAddresses, 0, slaveAddresses.length);
config.useMasterSlaveServers()
.setDatabase(database)
.setPassword(password)
.setMasterAddress(masterSlaveAddresses[0])
.addSlaveAddress(slaveAddresses);
}
/**
* 配置哨兵模式
*/
private static void configureSentinelMode(Config config) {
String[] sentinelAddresses = address.split(",");
config.useSentinelServers()
.setDatabase(database)
.setPassword(password)
.setMasterName(masterName)
.addSentinelAddress(sentinelAddresses);
}
/**
* 配置单机模式
*/
private static void configureSingleMode(Config config) throws Exception {
config.useSingleServer()
.setAddress(address)
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize)
.setDatabase(database)
@@ -61,91 +144,35 @@ public class RedissonConfig {
.setConnectTimeout(connectTimeout)
.setIdleConnectionTimeout(idleConnectionTimeout)
.setPassword(StringUtils.trimToNull(password));
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
config.setCodec(codec);
// 设置编码器
Class<?> codecClass = ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader());
Codec codecInstance = (Codec) codecClass.getDeclaredConstructor().newInstance();
config.setCodec(codecInstance);
// 设置线程和事件循环组
config.setThreads(thread);
config.setEventLoopGroup(new NioEventLoopGroup());
return config;
}
public int getThread() {
return thread;
/**
* 验证主从模式地址
*/
private static void validateMasterSlaveAddresses(String[] addresses) {
if (addresses.length == 1) {
throw new IllegalArgumentException(
"redis.redisson.address MUST have multiple redis addresses for master-slave mode.");
}
}
public void setThread(int thread) {
this.thread = thread;
}
// ========================== Getter和Setter方法 ==========================
// 连接配置
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public int getIdleConnectionTimeout() {
return idleConnectionTimeout;
}
public void setIdleConnectionTimeout(int idleConnectionTimeout) {
this.idleConnectionTimeout = idleConnectionTimeout;
}
public int getPingTimeout() {
return pingTimeout;
}
public void setPingTimeout(int pingTimeout) {
this.pingTimeout = pingTimeout;
}
public int getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getRetryAttempts() {
return retryAttempts;
}
public void setRetryAttempts(int retryAttempts) {
this.retryAttempts = retryAttempts;
}
public int getRetryInterval() {
return retryInterval;
}
public void setRetryInterval(int retryInterval) {
this.retryInterval = retryInterval;
}
public int getReconnectionTimeout() {
return reconnectionTimeout;
}
public void setReconnectionTimeout(int reconnectionTimeout) {
this.reconnectionTimeout = reconnectionTimeout;
}
public int getFailedAttempts() {
return failedAttempts;
}
public void setFailedAttempts(int failedAttempts) {
this.failedAttempts = failedAttempts;
RedissonConfig.address = address;
}
public String getPassword() {
@@ -153,15 +180,7 @@ public class RedissonConfig {
}
public void setPassword(String password) {
this.password = password;
}
public int getSubscriptionsPerConnection() {
return subscriptionsPerConnection;
}
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) {
this.subscriptionsPerConnection = subscriptionsPerConnection;
RedissonConfig.password = password;
}
public String getClientName() {
@@ -169,39 +188,7 @@ public class RedissonConfig {
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public int getSubscriptionConnectionMinimumIdleSize() {
return subscriptionConnectionMinimumIdleSize;
}
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) {
this.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize;
}
public int getSubscriptionConnectionPoolSize() {
return subscriptionConnectionPoolSize;
}
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) {
this.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize;
}
public int getConnectionMinimumIdleSize() {
return connectionMinimumIdleSize;
}
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
this.connectionMinimumIdleSize = connectionMinimumIdleSize;
}
public int getConnectionPoolSize() {
return connectionPoolSize;
}
public void setConnectionPoolSize(int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
RedissonConfig.clientName = clientName;
}
public int getDatabase() {
@@ -209,30 +196,130 @@ public class RedissonConfig {
}
public void setDatabase(int database) {
this.database = database;
RedissonConfig.database = database;
}
public boolean isDnsMonitoring() {
return dnsMonitoring;
public static String getMode() {
return mode;
}
public void setDnsMonitoring(boolean dnsMonitoring) {
this.dnsMonitoring = dnsMonitoring;
public void setMode(String mode) {
RedissonConfig.mode = mode;
}
public static String getMasterNamee() {
return masterName;
}
public void setMasterNamee(String masterName) {
RedissonConfig.masterName = masterName;
}
// 超时配置
public int getIdleConnectionTimeout() {
return idleConnectionTimeout;
}
public void setIdleConnectionTimeout(int idleConnectionTimeout) {
RedissonConfig.idleConnectionTimeout = idleConnectionTimeout;
}
public int getConnectTimeout() {
return connectTimeout;
}
public void setConnectTimeout(int connectTimeout) {
RedissonConfig.connectTimeout = connectTimeout;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
RedissonConfig.timeout = timeout;
}
// 重试配置
public int getRetryAttempts() {
return retryAttempts;
}
public void setRetryAttempts(int retryAttempts) {
RedissonConfig.retryAttempts = retryAttempts;
}
public int getRetryInterval() {
return retryInterval;
}
public void setRetryInterval(int retryInterval) {
RedissonConfig.retryInterval = retryInterval;
}
// 连接池配置
public int getConnectionMinimumIdleSize() {
return connectionMinimumIdleSize;
}
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
RedissonConfig.connectionMinimumIdleSize = connectionMinimumIdleSize;
}
public int getConnectionPoolSize() {
return connectionPoolSize;
}
public void setConnectionPoolSize(int connectionPoolSize) {
RedissonConfig.connectionPoolSize = connectionPoolSize;
}
public int getSubscriptionsPerConnection() {
return subscriptionsPerConnection;
}
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) {
RedissonConfig.subscriptionsPerConnection = subscriptionsPerConnection;
}
public int getSubscriptionConnectionMinimumIdleSize() {
return subscriptionConnectionMinimumIdleSize;
}
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) {
RedissonConfig.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize;
}
public int getSubscriptionConnectionPoolSize() {
return subscriptionConnectionPoolSize;
}
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) {
RedissonConfig.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize;
}
// 其他配置
public int getDnsMonitoringInterval() {
return dnsMonitoringInterval;
}
public void setDnsMonitoringInterval(int dnsMonitoringInterval) {
this.dnsMonitoringInterval = dnsMonitoringInterval;
RedissonConfig.dnsMonitoringInterval = dnsMonitoringInterval;
}
public String getCodec() {
public int getThread() {
return thread;
}
public void setThread(int thread) {
RedissonConfig.thread = thread;
}
public static String getCodec() {
return codec;
}
public void setCodec(String codec) {
this.codec = codec;
RedissonConfig.codec = codec;
}
}
}

View File

@@ -33,13 +33,15 @@ public enum FileType {
EPUB("epubFilePreviewImpl"),
BPMN("bpmnFilePreviewImpl"),
DCM("dcmFilePreviewImpl"),
MSG("msgFilePreviewImpl"),
DRAWIO("drawioFilePreviewImpl");
private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla","pages", "eps"};
private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp"};
private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla","pages", "eps", "pptm"};
private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp", "heic", "avif", "heif"};
private static final String[] ARCHIVE_TYPES = {"rar", "zip", "jar", "7-zip", "tar", "gzip", "7z"};
private static final String[] ONLINE3D_TYPES = {"obj", "3ds", "stl", "ply", "off", "3dm", "fbx", "dae", "wrl", "3mf", "ifc","glb","o3dv","gltf","stp","bim","fcstd","step","iges","brep"};
private static final String[] EML_TYPES = {"eml"};
private static final String[] MSG_TYPES = {"msg"};
private static final String[] XMIND_TYPES = {"xmind"};
private static final String[] EPUB_TYPES = {"epub"};
private static final String[] DCM_TYPES = {"dcm"};
@@ -96,6 +98,9 @@ public enum FileType {
for (String eml : EML_TYPES) {
FILE_TYPE_MAPPER.put(eml, FileType.EML);
}
for (String msg : MSG_TYPES) {
FILE_TYPE_MAPPER.put(msg, FileType.MSG);
}
for (String xmind : XMIND_TYPES) {
FILE_TYPE_MAPPER.put(xmind, FileType.XMIND);
}

File diff suppressed because it is too large Load Diff

View File

@@ -78,7 +78,7 @@ public class CompressFileReader {
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), "UTF-8"));
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), StandardCharsets.UTF_8).replaceAll("%2F", "/"));
}
}
}

View File

@@ -4,35 +4,18 @@ import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.FileType;
import cn.keking.service.cache.CacheService;
import cn.keking.service.cache.NotResourceCache;
import cn.keking.utils.EncodingDetects;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.UrlEncoderUtils;
import cn.keking.utils.WebUtils;
import cn.keking.utils.*;
import cn.keking.web.filter.BaseUrlFilter;
import com.aspose.cad.*;
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
import com.aspose.cad.imageoptions.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
import org.apache.poi.EncryptedDocumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
@@ -41,7 +24,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.stream.IntStream;
/**
@@ -50,10 +32,10 @@ import java.util.stream.IntStream;
*/
@Component
@DependsOn(ConfigConstants.BEAN_NAME)
public class FileHandlerService implements InitializingBean {
public class FileHandlerService {
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
private static final String PDF_PASSWORD_MSG = "password";
private final Logger logger = LoggerFactory.getLogger(FileHandlerService.class);
private final String fileDir = ConfigConstants.getFileDir();
private final CacheService cacheService;
@@ -147,15 +129,6 @@ public class FileHandlerService implements InitializingBean {
cacheService.putImgCache(fileKey, imgs);
}
/**
* cad定义线程池
*/
private ExecutorService pool = null;
@Override
public void afterPropertiesSet() throws Exception {
pool = Executors.newFixedThreadPool(ConfigConstants.getCadThread());
}
/**
* 对转换后的文件进行操作(改变编码方式)
@@ -195,18 +168,20 @@ public class FileHandlerService implements InitializingBean {
* @param index 图片索引
* @return 图片访问地址
*/
private String getPdf2jpgUrl(String pdfFilePath, int index) {
public String getPdf2jpgUrl(String pdfFilePath, int index) {
String baseUrl = BaseUrlFilter.getBaseUrl();
pdfFilePath = pdfFilePath.replace(fileDir, "");
String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4);
String urlPrefix;
try {
urlPrefix = baseUrl + URLEncoder.encode(pdfFolder, uriEncoding).replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
logger.error("UnsupportedEncodingException", e);
urlPrefix = baseUrl + pdfFolder;
}
return urlPrefix + "/" + index + PDF2JPG_IMAGE_FORMAT;
// 对整个路径进行编码,包括特殊字符
String encodedPath = URLEncoder.encode(pdfFolder, StandardCharsets.UTF_8);
encodedPath = encodedPath
.replaceAll("%2F", "/") // 恢复斜杠
.replaceAll("%5C", "/") // 恢复反斜杠
.replaceAll("\\+", "%20"); // 空格处理
// 构建URL使用_作为分隔符这是kkFileView压缩包预览的常见格式
String url = baseUrl + encodedPath + "/" + index + PDF2JPG_IMAGE_FORMAT;
return url;
}
/**
@@ -215,225 +190,20 @@ public class FileHandlerService implements InitializingBean {
* @param pdfFilePath pdf文件路径
* @return 图片访问集合
*/
private List<String> loadPdf2jpgCache(String pdfFilePath) {
public List<String> loadPdf2jpgCache(String pdfFilePath) { // 移除 static 修饰符
List<String> imageUrls = new ArrayList<>();
Integer imageCount = this.getPdf2jpgCache(pdfFilePath);
Integer imageCount = this.getPdf2jpgCache(pdfFilePath); // 使用 this. 调用
if (Objects.isNull(imageCount)) {
return imageUrls;
}
IntStream.range(0, imageCount).forEach(i -> {
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i);
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i); // 使用 this. 调用
imageUrls.add(imageUrl);
});
return imageUrls;
}
/**
* pdf文件转换成jpg图片集
* fileNameFilePath pdf文件路径
* pdfFilePath pdf输出文件路径
* pdfName pdf文件名称
* loadPdf2jpgCache 图片访问集合
*/
public List<String> pdf2jpg(String fileNameFilePath, String pdfFilePath, String pdfName, FileAttribute fileAttribute) throws Exception {
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
PDDocument doc;
final String[] pdfPassword = {null};
final int[] pageCount = new int[1];
if (!forceUpdatedCache) {
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
if (!CollectionUtils.isEmpty(cacheResult)) {
return cacheResult;
}
}
List<String> imageUrls = new ArrayList<>();
File pdfFile = new File(fileNameFilePath);
if (!pdfFile.exists()) {
return null;
}
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
File path = new File(folder);
if (!path.exists() && !path.mkdirs()) {
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
}
try {
doc = Loader.loadPDF(pdfFile, filePassword);
doc.setResourceCache(new NotResourceCache());
pageCount[0] = doc.getNumberOfPages();
} catch (IOException 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)) {
pdfPassword[0] = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
}
}
}
if (!PDF_PASSWORD_MSG.equals(pdfPassword[0])) { //该文件异常 错误原因非密码原因输出错误
logger.error("Convert pdf exception, pdfFilePath{}", pdfFilePath, e);
}
throw new Exception(e);
}
Callable <List<String>> call = () -> {
try {
String imageFilePath;
BufferedImage image = null;
PDFRenderer pdfRenderer = new PDFRenderer(doc);
pdfRenderer.setSubsamplingAllowed(true);
for (int pageIndex = 0; pageIndex < pageCount[0]; pageIndex++) {
imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
image = pdfRenderer.renderImageWithDPI(pageIndex, ConfigConstants.getPdf2JpgDpi(), ImageType.RGB);
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, pageIndex);
imageUrls.add(imageUrl);
}
image.flush();
} catch (IOException e) {
throw new Exception(e);
} finally {
doc.close();
}
return imageUrls;
};
Future<List<String>> result = pool.submit(call);
int pdftimeout;
if(pageCount[0] <=50){
pdftimeout = ConfigConstants.getPdfTimeout();
}else if(pageCount[0] <=200){
pdftimeout = ConfigConstants.getPdfTimeout80();
}else {
pdftimeout = ConfigConstants.getPdfTimeout200();
}
try {
result.get(pdftimeout, TimeUnit.SECONDS);
// 如果在超时时间内没有数据返回则抛出TimeoutException异常
} catch (InterruptedException | ExecutionException e) {
throw new Exception(e);
} catch (TimeoutException e) {
throw new Exception("overtime");
} finally {
//关闭
doc.close();
}
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) { //加密文件 判断是否启用缓存命令
this.addPdf2jpgCache(pdfFilePath, pageCount[0]);
}
return imageUrls;
}
/**
* cad文件转pdf
*
* @param inputFilePath cad文件路径
* @param outputFilePath pdf输出文件路径
* @return 转换是否成功
*/
public String cadToPdf(String inputFilePath, String outputFilePath, String cadPreviewType, FileAttribute fileAttribute) throws Exception {
final InterruptionTokenSource source = new InterruptionTokenSource();//CAD延时
final SvgOptions SvgOptions = new SvgOptions();
final PdfOptions pdfOptions = new PdfOptions();
final TiffOptions TiffOptions = new TiffOptions(TiffExpectedFormat.TiffJpegRgb);
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outputFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outputFilePath.substring(0, index);
File path = new File(folder);
//目录不存在 创建新的目录
if (!path.exists()) {
path.mkdirs();
}
}
File outputFile = new File(outputFilePath);
try {
LoadOptions opts = new LoadOptions();
opts.setSpecifiedEncoding(CodePages.SimpChinese);
final Image cadImage = Image.load(inputFilePath, opts);
try {
RasterizationQuality rasterizationQuality = new RasterizationQuality();
rasterizationQuality.setArc(RasterizationQualityValue.High);
rasterizationQuality.setHatch(RasterizationQualityValue.High);
rasterizationQuality.setText(RasterizationQualityValue.High);
rasterizationQuality.setOle(RasterizationQualityValue.High);
rasterizationQuality.setObjectsPrecision(RasterizationQualityValue.High);
rasterizationQuality.setTextThicknessNormalization(true);
CadRasterizationOptions cadRasterizationOptions = new CadRasterizationOptions();
cadRasterizationOptions.setBackgroundColor(Color.getWhite());
cadRasterizationOptions.setPageWidth(cadImage.getWidth());
cadRasterizationOptions.setPageHeight(cadImage.getHeight());
cadRasterizationOptions.setUnitType(cadImage.getUnitType());
cadRasterizationOptions.setAutomaticLayoutsScaling(false);
cadRasterizationOptions.setNoScaling(false);
cadRasterizationOptions.setQuality(rasterizationQuality);
cadRasterizationOptions.setDrawType(CadDrawTypeMode.UseObjectColor);
cadRasterizationOptions.setExportAllLayoutContent(true);
cadRasterizationOptions.setVisibilityMode(VisibilityMode.AsScreen);
switch (cadPreviewType) { //新增格式方法
case "svg":
SvgOptions.setVectorRasterizationOptions(cadRasterizationOptions);
SvgOptions.setInterruptionToken(source.getToken());
break;
case "pdf":
pdfOptions.setVectorRasterizationOptions(cadRasterizationOptions);
pdfOptions.setInterruptionToken(source.getToken());
break;
case "tif":
TiffOptions.setVectorRasterizationOptions(cadRasterizationOptions);
TiffOptions.setInterruptionToken(source.getToken());
break;
}
Callable<String> call = () -> {
try (OutputStream stream = new FileOutputStream(outputFile)) {
switch (cadPreviewType) {
case "svg":
cadImage.save(stream, SvgOptions);
break;
case "pdf":
cadImage.save(stream, pdfOptions);
break;
case "tif":
cadImage.save(stream, TiffOptions);
break;
}
} catch (IOException e) {
logger.error("CADFileNotFoundExceptioninputFilePath{}", inputFilePath, e);
return null;
} finally {
cadImage.dispose();
source.interrupt(); //结束任务
source.dispose();
}
return "true";
};
Future<String> result = pool.submit(call);
try {
result.get(Long.parseLong(ConfigConstants.getCadTimeout()), TimeUnit.SECONDS);
// 如果在超时时间内没有数据返回则抛出TimeoutException异常
} catch (InterruptedException e) {
logger.error("CAD转换文件异常", e);
return null;
} catch (ExecutionException e) {
logger.error("CAD转换在尝试取得任务结果时出错", e);
return null;
} catch (TimeoutException e) {
logger.error("CAD转换时间超时", e);
return null;
} finally {
source.interrupt(); //结束任务
source.dispose();
cadImage.dispose();
// pool.shutdownNow();
}
} finally {
source.dispose();
cadImage.dispose();
}
} finally {
source.dispose();
}
return "true";
}
/**
* @param str 原字符串(待截取原串)
@@ -474,7 +244,7 @@ public class FileHandlerService implements InitializingBean {
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
if (isCompressFile) { //判断是否使用特定压缩包符号
try {
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
originFileName = URLDecoder.decode(compressFilePath, uriEncoding); //转义的文件名 解下出原始文件名
attribute.setSkipDownLoad(true);
} catch (UnsupportedEncodingException e) {
logger.error("Failed to decode file name: {}", originFileName, e);
@@ -484,12 +254,23 @@ public class FileHandlerService implements InitializingBean {
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); //文件名处理
if (!KkFileUtils.validateFileNameLength(originFileName)) {
// 处理逻辑:抛出异常、记录日志、返回错误等
throw new IllegalArgumentException("文件名超过系统限制");
}
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");
String cacheFilePrefixName = null;
try {

View File

@@ -34,6 +34,10 @@ public interface FilePreview {
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
String XLSX_FILE_PREVIEW_PAGE = "officeweb";
String CSV_FILE_PREVIEW_PAGE = "csv";
String MSG_FILE_PREVIEW_PAGE = "msg";
String HEIC_FILE_PREVIEW_PAGE = "heic";
String CADVIEWER_FILE_PREVIEW_PAGE = "cadviewer";
String WAITING_FILE_PREVIEW_PAGE = "waiting";
String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -28,6 +30,7 @@ public class OfficeToPdfService {
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
Instant startTime = Instant.now();
File outputFile = new File(outputFilePath_end);
// 假如目标路径不存在,则新建该路径
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
@@ -65,7 +68,33 @@ public class OfficeToPdfService {
} else {
builder = LocalConverter.builder().storeProperties(customProperties);
}
builder.build().convert(inputFile).to(outputFile).execute();
try {
builder.build().convert(inputFile).to(outputFile).execute();
// 计算转换耗时
Instant endTime = Instant.now();
Duration duration = Duration.between(startTime, endTime);
// 格式化显示耗时(支持不同时间单位)
String durationFormatted;
if (duration.toMinutes() > 0) {
durationFormatted = String.format("%d分%d秒", duration.toMinutes(), duration.toSecondsPart());
} else if (duration.toSeconds() > 0) {
durationFormatted = String.format("%d.%03d秒",duration.toSeconds(), duration.toMillisPart());
} else {
durationFormatted = String.format("%d毫秒", duration.toMillis());
}
logger.info("文件转换成功:{} -> {},耗时:{}",
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());
throw e;
}
}
@@ -97,4 +126,4 @@ public class OfficeToPdfService {
return inputFilePath.substring(inputFilePath.lastIndexOf(".") + 1);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,473 @@
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;
import jakarta.annotation.PreDestroy;
import org.apache.commons.imaging.Imaging;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* TIF文件转换服务 - 虚拟线程版本 (JDK 21+)
*/
@Component
public class TifToPdfService {
private static final Logger logger = LoggerFactory.getLogger(TifToPdfService.class);
private static final String FILE_DIR = ConfigConstants.getFileDir();
// 虚拟线程执行器
private ExecutorService virtualThreadExecutor;
@PostConstruct
public void init() {
try {
// 创建虚拟线程执行器
this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
logger.info("TIF转换虚拟线程执行器初始化完成");
} catch (Exception e) {
logger.error("虚拟线程执行器初始化失败", e);
// 降级为固定线程池
this.virtualThreadExecutor = Executors.newFixedThreadPool(
Math.min(getMaxConcurrentConversions(), Runtime.getRuntime().availableProcessors() * 2)
);
logger.warn("使用固定线程池作为降级方案");
}
}
/**
* TIF转JPG - 虚拟线程版本
*/
public List<String> convertTif2Jpg(String strInputFile, String strOutputFile,String fileName,
String cacheName,
boolean forceUpdatedCache) throws Exception {
Instant startTime = Instant.now();
try {
List<String> result = performTifToJpgConversionVirtual(
strInputFile, strOutputFile, forceUpdatedCache,fileName,cacheName
);
Duration elapsedTime = Duration.between(startTime, Instant.now());
boolean success = result != null && !result.isEmpty();
logger.info("TIF转换{} - 文件: {}, 耗时: {}ms, 页数: {}",
success ? "成功" : "失败", fileName, elapsedTime.toMillis(),
result != null ? result.size() : 0);
return result != null ? result : Collections.emptyList();
} catch (Exception e) {
logger.error("TIF转JPG失败: {}, 耗时: {}ms", fileName,
Duration.between(startTime, Instant.now()).toMillis(), e);
throw e;
}
}
/**
* 虚拟线程执行TIF转JPG转换
*/
private List<String> performTifToJpgConversionVirtual(String strInputFile, String strOutputFile,
boolean forceUpdatedCache,String fileName,
String cacheName) throws Exception {
Instant totalStart = Instant.now();
String baseUrl = BaseUrlFilter.getBaseUrl();
String outputDirPath = strOutputFile.substring(0, strOutputFile.lastIndexOf('.'));
File tiffFile = new File(strInputFile);
if (!tiffFile.exists()) {
throw new FileNotFoundException("文件不存在: " + strInputFile);
}
File outputDir = new File(outputDirPath);
if (!outputDir.exists() && !outputDir.mkdirs()) {
throw new IOException("创建目录失败: " + outputDirPath);
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 30);
// 加载所有图片
List<BufferedImage> images;
try {
images = Imaging.getAllBufferedImages(tiffFile);
// logger.info("TIF文件加载完成共{}页,文件: {}",images.size(), strInputFile);
} catch (IOException e) {
handleImagingException(e, strInputFile);
throw e;
}
int pageCount = images.size();
if (pageCount == 0) {
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;
}
/**
* 使用虚拟线程并行转换页面
*/
private List<String> convertPagesVirtualThreads(List<BufferedImage> images, String outputDirPath,
String baseUrl, boolean forceUpdatedCache) {
int pageCount = images.size();
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger skipCount = new AtomicInteger(0);
AtomicInteger errorCount = new AtomicInteger(0);
// 用于收集结果的并发列表
List<String> imageUrls = Collections.synchronizedList(new ArrayList<>(pageCount));
Instant startTime = Instant.now();
try {
// 使用虚拟线程并行处理所有页面
List<CompletableFuture<Void>> futures = IntStream.range(0, pageCount)
.mapToObj(pageIndex -> CompletableFuture.runAsync(() -> {
try {
BufferedImage image = images.get(pageIndex);
// 使用PNG格式质量更好
String fileName = outputDirPath + File.separator + pageIndex + ".png";
File outputFile = new File(fileName);
if (forceUpdatedCache || !outputFile.exists()) {
// 创建目录
File parentDir = outputFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}
boolean writeSuccess = ImageIO.write(image, "png", outputFile);
if (!writeSuccess) {
throw new IOException("无法写入PNG格式");
}
successCount.incrementAndGet();
} else {
skipCount.incrementAndGet();
}
// 构建URL
String relativePath = fileName.replace(FILE_DIR, "");
String url = baseUrl + WebUtils.encodeFileName(relativePath);
imageUrls.add(url);
} catch (Exception e) {
logger.error("转换页 {} 失败: {}", pageIndex, e.getMessage());
errorCount.incrementAndGet();
}
}, virtualThreadExecutor))
.toList();
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.completeOnTimeout(null, getConversionTimeout(), TimeUnit.SECONDS)
.join();
} catch (Exception e) {
logger.error("虚拟线程并行转换异常", e);
}
Duration elapsedTime = Duration.between(startTime, Instant.now());
logger.info("TIF虚拟线程转换统计: 成功={}, 跳过={}, 失败={}, 总页数={}, 耗时={}ms",successCount.get(), skipCount.get(), errorCount.get(), pageCount,
elapsedTime.toMillis());
// 按页码排序
return imageUrls.stream()
.sorted(Comparator.comparing(url -> {
// 从URL中提取页码进行排序
String fileName = url.substring(url.lastIndexOf('/') + 1);
return Integer.parseInt(fileName.substring(0, fileName.lastIndexOf('.')));
}))
.collect(Collectors.toList());
}
/**
* TIF转PDF - 虚拟线程版本
*/
public void convertTif2Pdf(String strJpgFile, String strPdfFile,String fileName,
String cacheName,
boolean forceUpdatedCache) throws Exception {
Instant startTime = Instant.now();
try {
File pdfFile = new File(strPdfFile);
// 检查缓存
if (!forceUpdatedCache && pdfFile.exists()) {
logger.info("PDF文件已存在跳过转换: {}", strPdfFile);
return;
}
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",
result ? "成功" : "失败", fileName, elapsedTime.toMillis());
if (!result) {
throw new Exception("TIF转PDF失败");
}
} catch (Exception e) {
logger.error("TIF转PDF失败: {}, 耗时: {}ms", fileName,
Duration.between(startTime, Instant.now()).toMillis(), e);
throw e;
}
}
/**
* 虚拟线程执行TIF转PDF转换保持顺序
*/
private boolean performTifToPdfConversionVirtual(String strJpgFile, String strPdfFile,String fileName,String cacheName) throws Exception {
Instant totalStart = Instant.now();
File tiffFile = new File(strJpgFile);
try (PDDocument document = new PDDocument()) {
// 直接使用Imaging获取所有图像
List<BufferedImage> images = Imaging.getAllBufferedImages(tiffFile);
if (images.isEmpty()) {
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;
BufferedImage originalImage = images.get(pageIndex);
CompletableFuture<ProcessedPageResult> future = CompletableFuture.supplyAsync(() -> {
try {
// 处理图像(耗时的操作)
BufferedImage processedImage = processImageVirtualOptimized(originalImage);
// 返回处理结果,包含页码和处理后的图像
return new ProcessedPageResult(currentPageIndex, processedImage);
} catch (Exception e) {
logger.error("异步处理页 {} 失败", currentPageIndex + 1, e);
errorCount.incrementAndGet();
return null;
}
}, virtualThreadExecutor);
futures.add(future);
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 70);
// 等待所有任务完成
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// 设置超时
allFutures.completeOnTimeout(null, getConversionTimeout(), TimeUnit.SECONDS).join();
// 按顺序收集处理结果
List<ProcessedPageResult> results = new ArrayList<>(pageCount);
for (CompletableFuture<ProcessedPageResult> future : futures) {
ProcessedPageResult result = future.get();
if (result != null) {
results.add(result);
}
}
// 按页码排序(确保顺序)
results.sort(Comparator.comparingInt(ProcessedPageResult::pageIndex));
// 按顺序添加到PDF文档
for (ProcessedPageResult result : results) {
try {
// 创建页面
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
// 转换为PDImageXObject
PDImageXObject pdImage = LosslessFactory.createFromImage(document, result.processedImage());
// 计算位置并绘制图像
float[] position = calculateImagePositionOptimized(page, pdImage);
try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
contentStream.drawImage(pdImage, position[0], position[1], position[2], position[3]);
}
// 释放资源
result.processedImage().flush();
processedCount.incrementAndGet();
} catch (Exception e) {
logger.error("添加页 {} 到PDF失败", result.pageIndex() + 1, e);
errorCount.incrementAndGet();
}
}
FileConvertStatusManager.updateProgress(cacheName, "正在转换TIF为PDF", 100);
// 保存PDF
document.save(strPdfFile);
// Duration totalTime = Duration.between(totalStart, Instant.now());
//logger.info("PDF异步转换完成: {}, 总页数: {}, 成功: {}, 失败: {}, 总耗时: {}ms", strPdfFile, pageCount, processedCount.get(), errorCount.get(), totalTime.toMillis());
return processedCount.get() > 0;
}
}
/**
* 页面处理结果类
*/
private record ProcessedPageResult(int pageIndex, BufferedImage processedImage) {
}
/**
* 优化的图像处理方法
*/
private BufferedImage processImageVirtualOptimized(BufferedImage original) {
int targetDPI = 150;
float a4WidthInch = 8.27f;
float a4HeightInch = 11.69f;
int maxWidth = (int) (a4WidthInch * targetDPI);
int maxHeight = (int) (a4HeightInch * targetDPI);
if (original.getWidth() <= maxWidth && original.getHeight() <= maxHeight) {
return original;
}
double scaleX = (double) maxWidth / original.getWidth();
double scaleY = (double) maxHeight / original.getHeight();
double scale = Math.min(scaleX, scaleY);
int newWidth = (int) (original.getWidth() * scale);
int newHeight = (int) (original.getHeight() * scale);
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
g.drawImage(original, 0, 0, newWidth, newHeight, null);
g.dispose();
return resized;
}
/**
* 优化的位置计算方法
*/
private float[] calculateImagePositionOptimized(PDPage page, PDImageXObject pdImage) {
float margin = 5;
float pageWidth = page.getMediaBox().getWidth() - 2 * margin;
float pageHeight = page.getMediaBox().getHeight() - 2 * margin;
float imageWidth = pdImage.getWidth();
float imageHeight = pdImage.getHeight();
float scale = Math.min(pageWidth / imageWidth, pageHeight / imageHeight);
float scaledWidth = imageWidth * scale;
float scaledHeight = imageHeight * scale;
float x = (pageWidth - scaledWidth) / 2 + margin;
float y = (pageHeight - scaledHeight) / 2 + margin;
return new float[]{x, y, scaledWidth, scaledHeight};
}
/**
* 异常处理
*/
private void handleImagingException(IOException e, String filePath) {
String message = e.getMessage();
if (message != null && message.contains("Only sequential, baseline JPEGs are supported at the moment")) {
logger.warn("不支持的非基线JPEG格式文件{}", filePath, e);
} else {
logger.error("TIF转JPG异常文件路径{}", filePath, e);
}
}
/**
* 获取转换超时时间
*/
private long getConversionTimeout() {
try {
String timeoutStr = ConfigConstants.getTifTimeout();
if (timeoutStr != null && !timeoutStr.trim().isEmpty()) {
long timeout = Long.parseLong(timeoutStr);
if (timeout > 0) {
return Math.min(timeout, 600L);
}
}
} catch (NumberFormatException e) {
logger.warn("解析TIF转换超时时间失败使用默认值300秒", e);
}
return 300L;
}
/**
* 获取最大并发数
*/
private int getMaxConcurrentConversions() {
try {
int maxConcurrent = ConfigConstants.getTifThread();
if (maxConcurrent > 0) {
return Math.min(maxConcurrent, 50);
}
} catch (Exception e) {
logger.error("获取并发数配置失败,使用默认值", e);
}
return 4;
}
@PreDestroy
public void shutdown() {
logger.info("开始关闭TIF转换服务...");
if (virtualThreadExecutor != null && !virtualThreadExecutor.isShutdown()) {
try {
virtualThreadExecutor.shutdown();
if (!virtualThreadExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
virtualThreadExecutor.shutdownNow();
}
logger.info("虚拟线程执行器已关闭");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
virtualThreadExecutor.shutdownNow();
}
}
logger.info("TIF转换服务已关闭");
}
}

View File

@@ -3,11 +3,10 @@ package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.CadToPdfService;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import cn.keking.utils.*;
import cn.keking.web.filter.BaseUrlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,7 +14,9 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author chenjh
@@ -29,62 +30,157 @@ public class CadFilePreviewImpl implements FilePreview {
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
private final FileHandlerService fileHandlerService;
private final CadToPdfService cadtopdfservice;
private final OtherFilePreviewImpl otherFilePreview;
private final OfficeFilePreviewImpl officefilepreviewimpl;
public CadFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public CadFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
CadToPdfService cadtopdfservice,
OfficeFilePreviewImpl officefilepreviewimpl) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.cadtopdfservice = cadtopdfservice;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 预览Type参数传了就取参数的没传取系统默认
String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
String baseUrl = BaseUrlFilter.getBaseUrl();
String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileName = fileAttribute.getName();
String cadPreviewType = ConfigConstants.getCadPreviewType();
String cacheName = fileAttribute.getCacheName();
String outFilePath = fileAttribute.getOutFilePath();
// 查询转换状态
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
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();
String imageUrls = null;
int refreshSchedule = ConfigConstants.getTime();
if (StringUtils.hasText(outFilePath)) {
try {
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
// 启动异步转换,并添加回调处理
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
model.addAttribute("message", "文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to convert CAD file: {}", filePath, e);
}
if (imageUrls == null) {
logger.error("Failed to start CAD conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员");
}
//是否保留CAD源文件
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
}
}
cacheName= WebUtils.encodeFileName(cacheName);
// 如果已有缓存,直接渲染预览
return renderPreview(model, cacheName, outFilePath, officePreviewType, cadPreviewType, fileAttribute);
}
/**
* 启动异步转换,并在转换完成后处理后续操作
*/
private void startAsyncConversion(String filePath, String outFilePath,
String cacheName, FileAttribute fileAttribute) {
//获取cad转换路径
String cadConverterPath = ConfigConstants.getCadConverterPath();
int conversionModule= ConfigConstants.getConversionModule();
if(cadConverterPath.equals("false")){
//默认aspose-cad
conversionModule = 1;
}
CompletableFuture<Boolean> conversionFuture;
// 启动异步转换
if(conversionModule==2){
conversionFuture = cadtopdfservice.cadViewerConvert(
filePath,
outFilePath,
cadConverterPath,
ConfigConstants.getCadPreviewType(),
cacheName
);
}else {
conversionFuture = cadtopdfservice.cadToPdfAsync(
filePath,
outFilePath,
cacheName,
ConfigConstants.getCadPreviewType(),
fileAttribute
);
}
// 添加转换完成后的回调
conversionFuture.whenCompleteAsync((success, throwable) -> {
if (success != null && success) {
try {
// 1. 是否保留CAD源文件只在转换成功后才删除
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
// 2. 加入缓存(只在转换成功后才添加)
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(cacheName,
fileHandlerService.getRelativePath(outFilePath));
}
} catch (Exception e) {
logger.error("CAD转换后续处理失败: {}", filePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("CAD转换失败保留源文件: {}", filePath);
if (throwable != null) {
logger.error("转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 渲染预览页面
*/
private String renderPreview(Model model, String cacheName, String outFilePath,
String officePreviewType, String cadPreviewType,
FileAttribute fileAttribute) {
cacheName = WebUtils.encodeFileName(cacheName);
String baseUrl = BaseUrlFilter.getBaseUrl();
int conversionModule= ConfigConstants.getConversionModule();
if ("tif".equalsIgnoreCase(cadPreviewType)) {
model.addAttribute("currentUrl", cacheName);
return TIFF_FILE_PREVIEW_PAGE;
} else if ("svg".equalsIgnoreCase(cadPreviewType)) {
model.addAttribute("currentUrl", cacheName);
return SVG_FILE_PREVIEW_PAGE;
if(conversionModule==2){
return CADVIEWER_FILE_PREVIEW_PAGE;
}else {
return SVG_FILE_PREVIEW_PAGE;
}
}
if (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);
if (baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
return officefilepreviewimpl.getPreviewType(model, fileAttribute, officePreviewType, cacheName, outFilePath);
}
model.addAttribute("pdfUrl", cacheName);
return PDF_FILE_PREVIEW_PAGE;
}
}
}

View File

@@ -21,6 +21,9 @@ public class CodeFilePreviewImpl implements FilePreview {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
filePreviewHandle.filePreviewHandle(url, model, fileAttribute);
String suffix = fileAttribute.getSuffix();
boolean isHtmlFile = suffix.equalsIgnoreCase("htm") || suffix.equalsIgnoreCase("html") || suffix.equalsIgnoreCase("shtml");
model.addAttribute("isHtmlFile", isHtmlFile);
return CODE_FILE_PREVIEW_PAGE;
}
}

View File

@@ -1,5 +1,6 @@
package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.ArrayList;
import java.util.List;
@@ -32,17 +34,26 @@ public class CommonPreviewImpl implements FilePreview {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 不是http开头浏览器不能直接访问需下载到本地
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
String fileName = fileAttribute.getName(); //获取原始文件名
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
if (url != null && !url.toLowerCase().startsWith("http")) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, null);
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} else {
String file = fileHandlerService.getRelativePath(response.getContent());
model.addAttribute("currentUrl", file);
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(fileName, file);
}
}
} else {
model.addAttribute("currentUrl", url);
}
return null;
}
model.addAttribute("currentUrl", fileHandlerService.getConvertedFile(fileName));
return null;
}
}

View File

@@ -6,24 +6,25 @@ import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.Mediatomp4Service;
import cn.keking.utils.DownloadUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
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 org.springframework.util.ObjectUtils;
import java.io.File;
import java.util.concurrent.CompletableFuture;
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 {
@@ -31,11 +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 static final String mp4 = "mp4";
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,122 +56,140 @@ 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.equals(temp)) {
if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true;
break;
}
}
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的
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 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.listConvertedMedias().containsKey(cacheName)) {
model.addAttribute("mediaUrl", relativePath);
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();
if (mediaTypes) {
// 检查文件大小限制
if (isFileSizeExceeded(filePath)) {
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
}
String filePath = response.getContent();
String convertedUrl = null;
try {
if (mediaTypes) {
convertedUrl = convertToMp4(filePath, outFilePath, fileAttribute);
} else {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
}
// 启动异步转换,并添加回调处理
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
int refreshSchedule = ConfigConstants.getTime();
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
model.addAttribute("message", "视频文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to convert media file: {}", filePath, e);
}
if (convertedUrl == null) {
logger.error("Failed to start video conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
} else {
model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName));
// 不需要转换的文件,直接返回
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
return MEDIA_FILE_PREVIEW_PAGE;
}
return MEDIA_FILE_PREVIEW_PAGE;
}
if (type.equals(FileType.MEDIA)) { // 支持输出 只限默认格式
// HTTP协议的媒体文件直接播放
if (type.equals(FileType.MEDIA)) {
model.addAttribute("mediaUrl", url);
return MEDIA_FILE_PREVIEW_PAGE;
}
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
}
/**
* 检查视频文件转换是否已开启,以及当前文件是否需要转换
*
* @return
* 启动异步转换,并在转换完成后处理后续操作
*/
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);
}
/**
* 检查文件大小是否超过限制
*/
private boolean isFileSizeExceeded(String filePath) {
try {
File inputFile = new File(filePath);
if (inputFile.exists()) {
long fileSizeMB = inputFile.length() / (1024 * 1024);
int maxSizeMB = ConfigConstants.getMediaConvertMaxSize();
if (fileSizeMB > maxSizeMB) {
logger.warn("视频文件大小超过限制: {}MB > {}MB", fileSizeMB, maxSizeMB);
return true;
}
}
} catch (Exception e) {
logger.error("检查文件大小时出错: {}", filePath, e);
}
return false;
}
/**
* 检查是否需要转换
*/
private boolean checkNeedConvert(boolean mediaTypes) {
//1.检查开关是否开启
// 检查转换开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes;
}
return false;
}
private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception {
FFmpegFrameGrabber frameGrabber = FFmpegFrameGrabber.createDefault(filePath);
Frame captured_frame;
FFmpegFrameRecorder recorder = null;
try {
File desFile = new File(outFilePath);
//判断一下防止重复转换
if (desFile.exists()) {
return outFilePath;
}
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outFilePath.substring(0, index);
File path = new File(folder);
//目录不存在 创建新的目录
if (!path.exists()) {
path.mkdirs();
}
}
frameGrabber.start();
recorder = new FFmpegFrameRecorder(outFilePath, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
// recorder.setImageHeight(640);
// recorder.setImageWidth(480);
recorder.setFormat(mp4);
recorder.setFrameRate(frameGrabber.getFrameRate());
recorder.setSampleRate(frameGrabber.getSampleRate());
//视频编码属性配置 H.264 H.265 MPEG
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频比特率,单位:b
recorder.setVideoBitrate(frameGrabber.getVideoBitrate());
recorder.setAspectRatio(frameGrabber.getAspectRatio());
// 设置音频通用编码格式
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
//设置音频比特率,单位:b (比特率越高,清晰度/音质越好,当然文件也就越大 128000 = 182kb)
recorder.setAudioBitrate(frameGrabber.getAudioBitrate());
recorder.setAudioOptions(frameGrabber.getAudioOptions());
recorder.setAudioChannels(frameGrabber.getAudioChannels());
recorder.start();
while (true) {
captured_frame = frameGrabber.grabFrame();
if (captured_frame == null) {
System.out.println("转码完成:" + filePath);
break;
}
recorder.record(captured_frame);
}
} catch (Exception e) {
logger.error("Failed to convert video file to mp4: {}", filePath, e);
return null;
} finally {
if (recorder != null) { //关闭
recorder.stop();
recorder.close();
}
frameGrabber.stop();
frameGrabber.close();
}
return outFilePath;
}
}
}

View File

@@ -0,0 +1,25 @@
package cn.keking.service.impl;
import cn.keking.model.FileAttribute;
import cn.keking.service.FilePreview;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
/**
* Dcm 文件处理
*/
@Service
public class MsgFilePreviewImpl implements FilePreview {
private final CommonPreviewImpl commonPreview;
public MsgFilePreviewImpl(CommonPreviewImpl commonPreview) {
this.commonPreview = commonPreview;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
commonPreview.filePreviewHandle(url,model,fileAttribute);
return MSG_FILE_PREVIEW_PAGE;
}
}

View File

@@ -6,20 +6,25 @@ import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
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.
@@ -28,18 +33,23 @@ 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;
private final OtherFilePreviewImpl otherFilePreview;
private final PdfToJpgService pdftojpgservice;
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview) {
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) {
this.fileHandlerService = fileHandlerService;
this.officeToPdfService = officeToPdfService;
this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
}
@Override
@@ -51,12 +61,19 @@ 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(); //转换后生成文件的路径
// 查询转换状态
String convertStatusResult = checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (convertStatusResult != null) {
return convertStatusResult;
}
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;
@@ -67,14 +84,192 @@ 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);
int refreshSchedule = ConfigConstants.getTime();
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
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, 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);
@@ -106,43 +301,51 @@ 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;
}
static 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 = fileHandlerService.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();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
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) {
// 超时状态,检查是否有强制更新命令
if (forceUpdatedCache) {
// 强制更新命令,清除状态,允许重新转换
FileConvertStatusManager.convertSuccess(cacheName);
logger.info("强制更新命令跳过超时状态,允许重新转换: {}", cacheName);
return null; // 返回null表示继续执行
} else {
// 没有强制更新,不允许重新转换
return otherFilePreview.notSupportedFile(model, fileAttribute, "文件转换已超时,无法继续转换");
}
} else if (status.getStatus() == FileConvertStatusManager.Status.FAILED) {
// 失败状态,检查是否有强制更新命令
if (forceUpdatedCache) {
// 强制更新命令,清除状态,允许重新转换
FileConvertStatusManager.convertSuccess(cacheName);
logger.info("强制更新命令跳过失败状态,允许重新转换: {}", cacheName);
return null; // 返回null表示继续执行
} else {
// 没有强制更新,不允许重新转换
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

@@ -5,15 +5,24 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
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.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.
@@ -22,78 +31,251 @@ 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 static final String PDF_PASSWORD_MSG = "password";
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
private final PdfToJpgService pdftojpgservice;
private final OfficeFilePreviewImpl officefilepreviewimpl;
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public PdfFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
OfficeFilePreviewImpl officefilepreviewimpl,
PdfToJpgService pdftojpgservice) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@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 = pdfName+officePreviewType;
String filePassword = fileAttribute.getFilePassword(); // 获取密码
if("demo.pdf".equals(pdfName)){
return otherFilePreview.notSupportedFile(model, fileAttribute, "不能使用该文件名,请更换其他文件名在进行转换");
}
// 查询转换状态
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, pdfName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
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, 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", pdfName);
return EXEL_FILE_PREVIEW_PAGE;
}
}
try {
// 启动异步转换
startAsyncPdfConversion(originFilePath, outFilePath, cacheName, pdfName, fileAttribute);
int refreshSchedule = ConfigConstants.getTime();
// 返回等待页面
model.addAttribute("fileName", pdfName);
model.addAttribute("time", refreshSchedule);
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, fileAttribute);
}
List<String> imageUrls;
try {
imageUrls = fileHandlerService.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,
fileAttribute);
if (imageUrls != null && !imageUrls.isEmpty()) {
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || usePasswordCache)) {
fileHandlerService.addConvertedFile(cacheName, 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()) {
logger.error("PDF转换失败保留源文件: {}", originFilePath);
if (throwable != null) {
logger.error("转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 渲染预览页面
*/
private String renderPreview(Model model, String cacheName,
String outFilePath, String officePreviewType,
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));
model.addAttribute("currentUrl", imageUrls.getFirst());
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

@@ -27,16 +27,28 @@ public class PictureFilePreviewImpl extends CommonPreviewImpl {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
url= KkFileUtils.htmlEscape(url);
String suffix = fileAttribute.getSuffix();
List<String> imgUrls = new ArrayList<>();
imgUrls.add(url);
String compressFileKey = fileAttribute.getCompressFileKey();
List<String> zipImgUrls = fileHandlerService.getImgCache(compressFileKey);
if (!CollectionUtils.isEmpty(zipImgUrls)) {
imgUrls.addAll(zipImgUrls);
model.addAttribute("imgUrls", imgUrls);
}else {
// 不是http开头浏览器不能直接访问需下载到本地
super.filePreviewHandle(url, model, fileAttribute);
if ( url.toLowerCase().startsWith("file") || url.toLowerCase().startsWith("ftp")) {
model.addAttribute("imgUrls", fileAttribute.getName());
}else {
model.addAttribute("imgUrls", url);
}
}
if(suffix.equalsIgnoreCase("heic")||suffix.equalsIgnoreCase("heif")){
return HEIC_FILE_PREVIEW_PAGE;
}else {
return PICTURE_FILE_PREVIEW_PAGE;
}
// 不是http开头浏览器不能直接访问需下载到本地
super.filePreviewHandle(url, model, fileAttribute);
model.addAttribute("imgUrls", imgUrls);
return PICTURE_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

@@ -5,37 +5,60 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.utils.ConvertPicUtil;
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 图片文件处理
*
* @author kl (http://kailing.pub)
* @since 2021/2/8
*/
@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;
public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
private final 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);
@@ -43,66 +66,113 @@ 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);
int refreshSchedule = ConfigConstants.getTime();
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
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 {
ConvertPicUtil.convertJpg2Pdf(filePath, outFilePath);
} 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 = ConvertPicUtil.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()) {
@@ -116,12 +186,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

@@ -0,0 +1,79 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* AES加密解密工具类目前AES比DES和DES3更安全速度更快对称加密一般采用AES
*/
public class AESUtil {
private static final String aesKey = ConfigConstants.getaesKey();
/**
* AES解密
*/
public static String AesDecrypt(String url) {
if (!aesKey(aesKey)) {
return null;
}
try {
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] encrypted1 = Base64.getDecoder().decode(url);//先用base64解密
byte[] original = cipher.doFinal(encrypted1);
return new String(original, StandardCharsets.UTF_8);
} catch (Exception e) {
if (e.getMessage().contains("Given final block not properly padded. Such issues can arise if a bad key is used during decryption")) {
return "Keyerror";
}else if (e.getMessage().contains("Input byte array has incorrect ending byte")) {
return "byteerror";
}else if (e.getMessage().contains("Illegal base64 character")) {
return "base64error";
}else if (e.getMessage().contains("Input length must be multiple of 16 when decrypting with padded cipher")) {
return "byteerror";
}else {
System.out.println("ace错误:"+e);
return null;
}
}
}
/**
* AES加密
*/
public static String aesEncrypt(String url) {
if (!aesKey(aesKey)) {
return null;
}
try {
byte[] raw = aesKey.getBytes(StandardCharsets.UTF_8);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式"
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(url.getBytes(StandardCharsets.UTF_8));
return new String(Base64.getEncoder().encode(encrypted));//此处使用BASE64做转码功能同时能起到2次加密的作用。
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static boolean aesKey(String aesKey) {
if (aesKey == null) {
System.out.print("Key为空null");
return false;
}
// 判断Key是否为16位
if (aesKey.length() != 16) {
System.out.print("Key长度不是16位");
return false;
}
return true;
}
}

View File

@@ -1,138 +0,0 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import cn.keking.web.filter.BaseUrlFilter;
import com.itextpdf.text.Document;
import com.itextpdf.text.Image;
import com.itextpdf.text.io.FileChannelRandomAccessSource;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.RandomAccessFileOrArray;
import com.itextpdf.text.pdf.codec.TiffImage;
import com.sun.media.jai.codec.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.media.jai.JAI;
import javax.media.jai.RenderedOp;
import java.awt.image.RenderedImage;
import java.awt.image.renderable.ParameterBlock;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
public class ConvertPicUtil {
private static final int FIT_WIDTH = 500;
private static final int FIT_HEIGHT = 900;
private final static Logger logger = LoggerFactory.getLogger(ConvertPicUtil.class);
private final static String fileDir = ConfigConstants.getFileDir();
/**
* Tif 转 JPG。
*
* @param strInputFile 输入文件的路径和文件名
* @param strOutputFile 输出文件的路径和文件名
* @return boolean 是否转换成功
*/
public static List<String> convertTif2Jpg(String strInputFile, String strOutputFile, boolean forceUpdatedCache) throws Exception {
List<String> listImageFiles = new ArrayList<>();
String baseUrl = BaseUrlFilter.getBaseUrl();
if (!new File(strInputFile).exists()) {
logger.info("找不到文件【" + strInputFile + "");
return null;
}
strOutputFile = strOutputFile.replaceAll(".jpg", "");
FileSeekableStream fileSeekStream = null;
try {
JPEGEncodeParam jpegEncodeParam = new JPEGEncodeParam();
TIFFEncodeParam tiffEncodeParam = new TIFFEncodeParam();
tiffEncodeParam.setCompression(TIFFEncodeParam.COMPRESSION_GROUP4);
tiffEncodeParam.setLittleEndian(false);
fileSeekStream = new FileSeekableStream(strInputFile);
ImageDecoder imageDecoder = ImageCodec.createImageDecoder("TIFF", fileSeekStream, null);
int intTifCount = imageDecoder.getNumPages();
// logger.info("该tif文件共有【" + intTifCount + "】页");
// 处理目标文件夹,如果不存在则自动创建
File fileJpgPath = new File(strOutputFile);
if (!fileJpgPath.exists() && !fileJpgPath.mkdirs()) {
logger.error("{} 创建失败", strOutputFile);
}
// 循环处理每页tif文件转换为jpg
for (int i = 0; i < intTifCount; i++) {
String strJpg= strOutputFile + "/" + i + ".jpg";
File fileJpg = new File(strJpg);
// 如果文件不存在,则生成
if (forceUpdatedCache|| !fileJpg.exists()) {
RenderedImage renderedImage = imageDecoder.decodeAsRenderedImage(i);
ParameterBlock pb = new ParameterBlock();
pb.addSource(renderedImage);
pb.add(fileJpg.toString());
pb.add("JPEG");
pb.add(jpegEncodeParam);
RenderedOp renderedOp = JAI.create("filestore", pb);
renderedOp.dispose();
// logger.info("每页分别保存至: " + fileJpg.getCanonicalPath());
}
strJpg = baseUrl+strJpg.replace(fileDir, "");
listImageFiles.add(strJpg);
}
} catch (IOException e) {
if (!e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
logger.error("TIF转JPG异常文件路径" + strInputFile, e);
}
throw new Exception(e);
} finally {
if (fileSeekStream != null) {
fileSeekStream.close();
}
}
return listImageFiles;
}
/**
* 将Jpg图片转换为Pdf文件
*
* @param strJpgFile 输入的jpg的路径和文件名
* @param strPdfFile 输出的pdf的路径和文件名
*/
public static String convertJpg2Pdf(String strJpgFile, String strPdfFile) throws Exception {
Document document = new Document();
RandomAccessFileOrArray rafa = null;
FileOutputStream outputStream = null;
try {
RandomAccessFile aFile = new RandomAccessFile(strJpgFile, "r");
FileChannel inChannel = aFile.getChannel();
FileChannelRandomAccessSource fcra = new FileChannelRandomAccessSource(inChannel);
rafa = new RandomAccessFileOrArray(fcra);
int pages = TiffImage.getNumberOfPages(rafa);
outputStream = new FileOutputStream(strPdfFile);
PdfWriter.getInstance(document, outputStream);
document.open();
Image image;
for (int i = 1; i <= pages; i++) {
image = TiffImage.getTiffImage(rafa, i);
image.scaleToFit(FIT_WIDTH, FIT_HEIGHT);
document.add(image);
}
} catch (IOException e) {
if (!e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) {
logger.error("TIF转JPG异常文件路径" + strPdfFile, e);
}
throw new Exception(e);
} finally {
if (document != null) {
document.close();
}
if (rafa != null) {
rafa.close();
}
if (outputStream != null) {
outputStream.close();
}
}
return strPdfFile;
}
}

View File

@@ -3,31 +3,21 @@ package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.UUID;
import static cn.keking.utils.KkFileUtils.isFtpUrl;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
import static cn.keking.utils.KkFileUtils.*;
/**
* @author yudian-it
@@ -39,10 +29,7 @@ public class DownloadUtils {
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper();
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
/**
* @param fileAttribute fileAttribute
@@ -50,17 +37,17 @@ public class DownloadUtils {
* @return 本地文件绝对路径
*/
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
// 忽略ssl证书
String urlStr = null;
try {
SslUtils.ignoreSsl();
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
urlStr = fileAttribute.getUrl();
} catch (Exception e) {
logger.error("忽略SSL证书异常:", e);
logger.error("处理URL异常:", e);
}
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute);
// 获取文件后缀用于校验
final String fileSuffix = fileAttribute.getSuffix();
// 判断是否非法地址
if (KkFileUtils.isIllegalFileName(realPath)) {
response.setCode(1);
@@ -90,36 +77,33 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) {
File realFile = new File(realPath);
factory.setConnectionRequestTimeout(2000); //设置超时时间
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000);
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
factory.setHttpClient(httpClient); //加入重定向方法
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
String finalUrlStr = urlStr;
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
// 获取响应头中的Content-Type
String contentType = responseWrapper.getContentType();
// 如果是Office/设计文件需要校验MIME类型
if (WebUtils.isMimeCheckRequired(fileSuffix)) {
if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
logger.error("文件类型错误期望二进制文件但接收到文本类型url: {}, Content-Type: {}",
finalUrlStr, contentType);
responseWrapper.setHasError(true);
return;
}
}
};
try {
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
FileUtils.copyToFile(fileResponse.getBody(), realFile);
return null;
});
} catch (Exception e) {
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败:" + e);
return response;
}
// 保存文件
FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
});
} else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
FtpUtils.download(fileAttribute.getUrl(), realPath, ftpUsername, ftpPassword, ftpControlEncoding);
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
} else if (isFileUrl(url)) { // 添加对file协议的支持
handleFileProtocol(url, realPath);
} else {
response.setCode(1);
response.setMsg("url不能识别url" + urlStr);
@@ -138,9 +122,68 @@ public class DownloadUtils {
response.setMsg(e.getMessage());
}
return response;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 处理file协议的文件下载
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
File sourceFile = new File(url.getPath());
if (!sourceFile.exists()) {
throw new FileNotFoundException("本地文件不存在: " + url.getPath());
}
if (!sourceFile.isFile()) {
throw new IOException("路径不是文件: " + url.getPath());
}
File targetFile = new File(targetPath);
// 判断源文件和目标文件是否是同一个文件(防止自身复制覆盖)
if (isSameFile(sourceFile, targetFile)) {
// 如果是同一个文件,直接返回,不执行复制操作
logger.info("源文件和目标文件相同,跳过复制: {}", sourceFile.getAbsolutePath());
return;
}
// 确保目标目录存在
File parentDir = targetFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
// 复制文件
Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
* 判断两个文件是否是同一个文件
* 通过比较规范化路径来避免符号链接、相对路径等问题
*/
private static boolean isSameFile(File file1, File file2) {
try {
// 使用规范化路径比较,可以处理符号链接、相对路径等情况
String canonicalPath1 = file1.getCanonicalPath();
String canonicalPath2 = file2.getCanonicalPath();
// 如果是Windows系统忽略路径大小写
if (isWindows()) {
return canonicalPath1.equalsIgnoreCase(canonicalPath2);
}
return canonicalPath1.equals(canonicalPath2);
} catch (IOException e) {
// 如果获取规范化路径失败,使用绝对路径比较
logger.warn("无法获取文件的规范化路径,使用绝对路径比较: {}, {}", file1.getAbsolutePath(), file2.getAbsolutePath());
String absolutePath1 = file1.getAbsolutePath();
String absolutePath2 = file2.getAbsolutePath();
if (isWindows()) {
return absolutePath1.equalsIgnoreCase(absolutePath2);
}
return absolutePath1.equals(absolutePath2);
}
}
/**
* 获取真实文件绝对路径
@@ -164,5 +207,4 @@ public class DownloadUtils {
}
return realPath;
}
}
}

View File

@@ -0,0 +1,257 @@
package cn.keking.utils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 文件转换状态管理器(增强版)
* 支持实时状态跟踪和状态锁定机制
*/
public class FileConvertStatusManager {
// 存储转换状态key=文件名value=转换状态对象
private static final ConcurrentMap<String, ConvertState> STATUS_MAP = new ConcurrentHashMap<>();
// 记录最终状态(超时或异常),防止重复转换
private static final ConcurrentMap<String, Status> FINAL_STATUS_MAP = new ConcurrentHashMap<>();
/**
* 开始转换,创建初始状态
* @param fileName 文件名
*/
public static void startConvert(String fileName) {
STATUS_MAP.putIfAbsent(fileName, new ConvertState(Status.CONVERTING, "等待转换", 0));
// 清除可能存在的最终状态,因为要开始新的转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 更新转换进度
* @param fileName 文件名
* @param message 状态消息
* @param progress 进度百分比(0-100)
*/
public static void updateProgress(String fileName, String message, int progress) {
STATUS_MAP.computeIfPresent(fileName, (key, state) -> {
state.update(message, progress);
logger.debug("更新转换进度: {} -> {} ({}%)", fileName, message, progress);
return state;
});
}
/**
* 标记转换超时 - 记录为最终状态
* @param fileName 文件名
*/
public static void markTimeout(String fileName) {
STATUS_MAP.put(fileName, new ConvertState(Status.TIMEOUT, "转换超时,请重试", 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.TIMEOUT);
logger.warn("标记文件转换超时: {}", fileName);
}
/**
* 标记转换失败 - 记录为最终状态
* @param fileName 文件名
* @param errorMessage 错误信息
*/
public static void markError(String fileName, String errorMessage) {
STATUS_MAP.put(fileName, new ConvertState(Status.FAILED, errorMessage, 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.FAILED);
logger.warn("标记文件转换失败: {}, 错误: {}", fileName, errorMessage);
}
/**
* 查询文件转换状态
* @param fileName 文件名
* @return 转换状态对象如果不存在返回null
*/
public static ConvertStatus getConvertStatus(String fileName) {
// 先检查是否有最终状态
Status finalStatus = FINAL_STATUS_MAP.get(fileName);
if ((finalStatus == Status.TIMEOUT || finalStatus == Status.FAILED)) {
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
// 如果STATUS_MAP中没有创建一个最终状态
if (finalStatus == Status.TIMEOUT) {
return new ConvertStatus(Status.TIMEOUT, "转换超时,请重试", 0, 0);
} else {
return new ConvertStatus(Status.FAILED, "转换失败", 0, 0);
}
}
// 返回最终状态
return new ConvertStatus(state.status, state.message, state.progress, 0);
}
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
return null;
}
// 如果是转换中状态,计算已等待时间
long waitingSeconds = 0;
if (state.status == Status.CONVERTING) {
waitingSeconds = (System.currentTimeMillis() - state.startTime) / 1000;
}
return new ConvertStatus(
state.status,
state.message,
state.progress,
waitingSeconds
);
}
/**
* 转换成功
* @param fileName 文件名
*/
public static void convertSuccess(String fileName) {
STATUS_MAP.remove(fileName);
// 清除最终状态,允许重新转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 清理状态(强制重置,允许重新转换)
* @param fileName 文件名
* @return true: 清理成功; false: 清理失败
*/
public static boolean clearStatus(String fileName) {
boolean removed1 = STATUS_MAP.remove(fileName) != null;
boolean removed2 = FINAL_STATUS_MAP.remove(fileName) != null;
logger.info("清理文件状态: {}, STATUS_MAP: {}, FINAL_STATUS_MAP: {}",
fileName, removed1, removed2);
return removed1 || removed2;
}
/**
* 清理过期状态(长时间未清理的状态)
* @param expireHours 过期时间(小时)
* @return 清理的数量
*/
public static int cleanupExpiredStatus(int expireHours) {
long expireMillis = expireHours * 3600 * 1000L;
long currentTime = System.currentTimeMillis();
// 清理STATUS_MAP中的过期状态
int count1 = (int) STATUS_MAP.entrySet().stream()
.filter(entry -> {
ConvertState state = entry.getValue();
if (state.status == Status.CONVERTING) {
return false; // 转换中的不清理
}
long elapsed = currentTime - state.startTime;
return elapsed > expireMillis;
})
.count();
// 清理FINAL_STATUS_MAP中的过期状态
// 注意FINAL_STATUS_MAP没有时间戳无法基于时间清理
// 如果需要清理,可以设置一个独立的过期机制
logger.info("清理了 {} 个过期的转换状态", count1);
return count1;
}
/**
* 转换状态枚举
*/
public enum Status {
CONVERTING("转换中"),
FAILED("转换失败"),
TIMEOUT("转换超时"),
QUEUED("排队中");
private final String description;
Status(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 内部状态存储类
*/
private static class ConvertState {
private final Status status;
private String message;
private int progress; // 0-100
private final long startTime;
public ConvertState(Status status, String message, int progress) {
this.status = status;
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
this.startTime = System.currentTimeMillis();
}
public void update(String message, int progress) {
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
}
}
/**
* 对外暴露的转换状态封装类
*/
public static class ConvertStatus {
private final Status status;
private final String message;
private final int progress;
private final long waitingSeconds;
private final long timestamp;
public ConvertStatus(Status status, String message, int progress, long waitingSeconds) {
this.status = status;
this.message = message;
this.progress = progress;
this.waitingSeconds = waitingSeconds;
this.timestamp = System.currentTimeMillis();
}
// 获取实时状态信息
public String getRealTimeMessage() {
if (status == Status.CONVERTING) {
if (progress > 0) {
return String.format("%s: %s (进度: %d%%,已等待 %d 秒)",
status.getDescription(), message, progress, waitingSeconds);
} else {
return String.format("%s: %s已等待 %d 秒",
status.getDescription(), message, waitingSeconds);
}
}
return message;
}
// Getters
public Status getStatus() { return status; }
public String getMessage() { return message; }
public int getProgress() { return progress; }
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return "ConvertStatus{" +
"status=" + status +
", message='" + message + '\'' +
", progress=" + progress +
", waitingSeconds=" + waitingSeconds +
", timestamp=" + timestamp +
'}';
}
}
// 日志记录器
private static final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(FileConvertStatusManager.class);
}

View File

@@ -3,57 +3,225 @@ package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Objects;
/**
* @auther: chenjh
* @since: 2019/6/18 14:36
*/
public class FtpUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class);
public static FTPClient connect(String host, int port, String username, String password, String controlEncoding) throws IOException {
/**
* 从FTP服务器下载文件到本地
*/
public static void download(String ftpUrl, String ftpport, String localFilePath,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
// 获取FTP连接信息
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
LOGGER.debug("FTP下载 - url:{}, host:{}, port:{}, username:{}, 保存路径:{}",
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username, localFilePath);
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
connectionInfo.username, connectionInfo.password,
connectionInfo.controlEncoding);
try {
// 设置被动模式
ftpClient.enterLocalPassiveMode();
// 获取文件输入流
String encodedFilePath = new String(
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding), StandardCharsets.ISO_8859_1
);
// 方法1直接下载文件到本地
try (OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath))) {
boolean downloadResult = ftpClient.retrieveFile(encodedFilePath, outputStream);
LOGGER.debug("FTP下载结果: {}", downloadResult);
if (!downloadResult) {
throw new IOException("FTP文件下载失败返回码: " + ftpClient.getReplyCode());
}
}
} finally {
closeFtpClient(ftpClient);
}
}
/**
* 预览FTP文件 - 返回输入流(调用者需要关闭流)
*/
public static InputStream preview(String ftpUrl, String ftpport, String localFilePath,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
// 获取FTP连接信息
FtpConnectionInfo connectionInfo = parseFtpConnectionInfo(ftpUrl, ftpport, ftpUsername, ftpPassword, ftpControlEncoding);
LOGGER.debug("FTP预览 - url:{}, host:{}, port:{}, username:{}",
ftpUrl, connectionInfo.host, connectionInfo.port, connectionInfo.username);
FTPClient ftpClient = connect(connectionInfo.host, connectionInfo.port,
connectionInfo.username, connectionInfo.password,
connectionInfo.controlEncoding);
try {
// 设置被动模式
ftpClient.enterLocalPassiveMode();
// 获取文件输入流
String encodedFilePath = new String(
connectionInfo.remoteFilePath.getBytes(connectionInfo.controlEncoding), StandardCharsets.ISO_8859_1
);
// 获取文件输入流
InputStream inputStream = ftpClient.retrieveFileStream(encodedFilePath);
if (inputStream == null) {
closeFtpClient(ftpClient);
throw new IOException("无法获取FTP文件流可能文件不存在或无权限");
}
// 包装输入流在流关闭时自动断开FTP连接
return new FtpAutoCloseInputStream(inputStream, ftpClient);
} catch (IOException e) {
// 发生异常时确保关闭连接
closeFtpClient(ftpClient);
throw e;
}
}
/**
* 解析FTP连接信息抽取公共逻辑
*/
private static FtpConnectionInfo parseFtpConnectionInfo(String ftpUrl, String ftpport,
String ftpUsername, String ftpPassword,
String ftpControlEncoding) throws IOException {
FtpConnectionInfo info = new FtpConnectionInfo();
// 从配置获取默认连接参数
String basic = ConfigConstants.getFtpUsername();
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
String[] params = WebUtils.namePass(ftpUrl, basic);
if (params != null && params.length >= 5) {
info.port = Integer.parseInt(params[1]);
info.username = params[2];
info.password = params[3];
info.controlEncoding = params[4];
}
}
// 使用传入参数覆盖默认值
if (!StringUtils.isEmpty(ftpport)) {
info.port = Integer.parseInt(ftpport);
}
if (!StringUtils.isEmpty(ftpUsername)) {
info.username = ftpUsername;
}
if (!StringUtils.isEmpty(ftpPassword)) {
info.password = ftpPassword;
}
if (!StringUtils.isEmpty(ftpControlEncoding)) {
info.controlEncoding = ftpControlEncoding;
}
// 设置默认值
if (info.port == 0) {
info.port = 21;
}
if (StringUtils.isEmpty(info.controlEncoding)) {
info.controlEncoding = "UTF-8";
}
// 解析URL
try {
URI uri = new URI(ftpUrl);
info.host = uri.getHost();
info.remoteFilePath = uri.getPath();
} catch (URISyntaxException e) {
throw new IOException("无效的FTP URL: " + ftpUrl, e);
}
return info;
}
/**
* FTP连接信息对象
*/
private static class FtpConnectionInfo {
String host;
int port = 21;
String username;
String password;
String controlEncoding = "UTF-8";
String remoteFilePath;
}
/**
* 自动关闭FTP连接的输入流包装类
*/
private static class FtpAutoCloseInputStream extends FilterInputStream {
private final FTPClient ftpClient;
protected FtpAutoCloseInputStream(InputStream in, FTPClient ftpClient) {
super(in);
this.ftpClient = ftpClient;
}
@Override
public void close() throws IOException {
try {
super.close();
// 确保FTP命令完成
if (ftpClient != null) {
ftpClient.completePendingCommand();
}
} finally {
closeFtpClient(ftpClient);
}
}
}
/**
* 安全关闭FTP连接
*/
private static void closeFtpClient(FTPClient ftpClient) {
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
LOGGER.warn("关闭FTP连接时发生异常", e);
}
}
}
/**
* 连接FTP服务器
*/
private static FTPClient connect(String host, int port, String username,
String password, String controlEncoding) throws IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.connect(host, port);
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
ftpClient.login(username, password);
}
int reply = ftpClient.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect();
}
ftpClient.setControlEncoding(controlEncoding);
ftpClient.connect(host, port);
if (!ftpClient.login(username, password)) {
throw new IOException("FTP登录失败用户名或密码错误");
}
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
return ftpClient;
}
public static void download(String ftpUrl, String localFilePath, String ftpUsername, String ftpPassword, String ftpControlEncoding) throws IOException {
String username = StringUtils.isEmpty(ftpUsername) ? ConfigConstants.getFtpUsername() : ftpUsername;
String password = StringUtils.isEmpty(ftpPassword) ? ConfigConstants.getFtpPassword() : ftpPassword;
String controlEncoding = StringUtils.isEmpty(ftpControlEncoding) ? ConfigConstants.getFtpControlEncoding() : ftpControlEncoding;
URL url = new URL(ftpUrl);
String host = url.getHost();
int port = (url.getPort() == -1) ? url.getDefaultPort() : url.getPort();
String remoteFilePath = url.getPath();
LOGGER.debug("FTP connection url:{}, username:{}, password:{}, controlEncoding:{}, localFilePath:{}", ftpUrl, username, password, controlEncoding, localFilePath);
FTPClient ftpClient = connect(host, port, username, password, controlEncoding);
OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath));
ftpClient.enterLocalPassiveMode();
boolean downloadResult = ftpClient.retrieveFile(new String(remoteFilePath.getBytes(controlEncoding), StandardCharsets.ISO_8859_1), outputStream);
LOGGER.debug("FTP download result {}", downloadResult);
outputStream.flush();
outputStream.close();
ftpClient.logout();
ftpClient.disconnect();
}
}

View File

@@ -0,0 +1,412 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* HTTP请求工具类统一处理HTTP请求逻辑
* 优化版本:支持连接复用,减少开销
*/
public class HttpRequestUtils {
private static final Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class);
private static final ObjectMapper mapper = new ObjectMapper();
// 连接池管理器(静态变量,全局共享)
private static volatile PoolingHttpClientConnectionManager connectionManager;
// 用于缓存不同配置的HttpClient实例
private static final Map<String, CloseableHttpClient> httpClientCache = new ConcurrentHashMap<>();
// 用于缓存不同配置的RestTemplate实例
private static final Map<String, RestTemplate> restTemplateCache = new ConcurrentHashMap<>();
// 默认连接池配置
private static final int DEFAULT_MAX_TOTAL = 200; // 最大连接数
private static final int DEFAULT_MAX_PER_ROUTE = 50; // 每个路由最大连接数
/**
* 判断是否为客户端中断连接的异常
*/
public static boolean isClientAbortException(Throwable e) {
if (e == null) {
return false;
}
// 检查异常链
Throwable cause = e;
while (cause != null) {
// 检查异常消息
if (cause instanceof IOException) {
String message = cause.getMessage();
if (message != null && (
message.contains("你的主机中的软件中止了一个已建立的连接") ||
message.contains("Broken pipe") ||
message.contains("Connection reset by peer") ||
message.contains("ClientAbortException"))) {
return true;
}
}
// 检查异常类型
String className = cause.getClass().getName();
if (className.contains("ClientAbortException") ||
className.contains("AbortedException") ||
className.contains("AsyncRequestNotUsableException")) {
return true;
}
cause = cause.getCause();
}
return false;
}
/**
* 初始化连接池管理器(懒加载)
*/
private static PoolingHttpClientConnectionManager getConnectionManager() throws Exception {
if (connectionManager == null) {
synchronized (HttpRequestUtils.class) {
if (connectionManager == null) {
// 创建连接池管理器
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create();
// 如果配置忽略SSL使用自定义TLS策略
if (ConfigConstants.isIgnoreSSL()) {
SSLContext sslContext = SslUtils.createIgnoreVerifySSL();
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext, NoopHostnameVerifier.INSTANCE);
builder.setTlsSocketStrategy(tlsStrategy);
}
// 设置连接池参数
builder.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
.setConnPoolPolicy(PoolReusePolicy.LIFO)
.setMaxConnTotal(DEFAULT_MAX_TOTAL)
.setMaxConnPerRoute(DEFAULT_MAX_PER_ROUTE);
// 设置Socket配置
SocketConfig socketConfig = SocketConfig.custom()
.setTcpNoDelay(true)
.setSoKeepAlive(true)
.setSoReuseAddress(true)
.setSoTimeout(Timeout.ofSeconds(30))
.build();
builder.setDefaultSocketConfig(socketConfig);
// 设置连接配置
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofSeconds(10))
.setSocketTimeout(Timeout.ofSeconds(30))
.setTimeToLive(TimeValue.ofMinutes(5))
.build();
builder.setDefaultConnectionConfig(connectionConfig);
connectionManager = builder.build();
// 启动空闲连接清理线程
startIdleConnectionMonitor();
logger.info("HTTP连接池管理器初始化完成最大连接数{},每个路由最大连接数:{}",
DEFAULT_MAX_TOTAL, DEFAULT_MAX_PER_ROUTE);
}
}
}
return connectionManager;
}
/**
* 启动空闲连接监控线程
*/
private static void startIdleConnectionMonitor() {
Thread monitorThread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
synchronized (HttpRequestUtils.class) {
Thread.sleep(30000); // 每30秒检查一次
if (connectionManager != null) {
// 关闭过期的连接
connectionManager.closeExpired();
// 关闭空闲超过30秒的连接
connectionManager.closeIdle(TimeValue.ofSeconds(30));
// 可选:打印连接池状态
if (logger.isDebugEnabled()) {
logger.debug("连接池状态:最大连接数={}, 每个路由最大连接数={}",
connectionManager.getMaxTotal(),
connectionManager.getDefaultMaxPerRoute());
}
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.info("连接池监控线程被中断");
} catch (Exception e) {
logger.error("连接池监控异常", e);
}
});
monitorThread.setDaemon(true);
monitorThread.setName("http-connection-monitor");
monitorThread.start();
}
/**
* 创建根据配置定制的HttpClient支持复用
*/
public static CloseableHttpClient createConfiguredHttpClient() throws Exception {
String cacheKey = buildHttpClientCacheKey();
// 尝试从缓存获取
CloseableHttpClient cachedClient = httpClientCache.get(cacheKey);
if (cachedClient != null) {
// HttpClient 5.x 没有 isClosed() 方法,我们需要通过其他方式判断
// 暂时假设缓存的客户端都是可用的,如果有问题会在使用时报错
return cachedClient;
}
// 创建新的HttpClient
synchronized (httpClientCache) {
// 双重检查
cachedClient = httpClientCache.get(cacheKey);
if (cachedClient != null) {
return cachedClient;
}
// 构建HttpClientBuilder
HttpClientBuilder httpClientBuilder = HttpClients.custom()
.setConnectionManager(getConnectionManager())
.setConnectionManagerShared(true); // 共享连接管理器
// 使用SslUtils配置HttpClientBuilder
CloseableHttpClient httpClient = SslUtils.configureHttpClientBuilder(
httpClientBuilder,
ConfigConstants.isIgnoreSSL(),
ConfigConstants.isEnableRedirect()
).build();
// 缓存HttpClient
httpClientCache.put(cacheKey, httpClient);
logger.debug("创建并缓存新的HttpClient实例缓存键{}", cacheKey);
return httpClient;
}
}
/**
* 构建HttpClient缓存键
*/
private static String buildHttpClientCacheKey() {
return String.format("ignoreSSL_%s_enableRedirect_%s",
ConfigConstants.isIgnoreSSL(),
ConfigConstants.isEnableRedirect());
}
/**
* 获取缓存的RestTemplate减少对象创建
*/
private static RestTemplate getCachedRestTemplate(CloseableHttpClient httpClient) {
String cacheKey = "restTemplate_" + System.identityHashCode(httpClient);
return restTemplateCache.computeIfAbsent(cacheKey, key -> {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient);
// 设置连接超时和读取超时
factory.setConnectTimeout(30000);
factory.setReadTimeout(30000);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(factory);
logger.debug("创建并缓存新的RestTemplate实例缓存键{}", cacheKey);
return restTemplate;
});
}
/**
* 执行HTTP请求使用连接池
*/
public static void executeHttpRequest(java.net.URL url, CloseableHttpClient httpClient,
FileAttribute fileAttribute, FileResponseHandler handler) throws Exception {
// 获取缓存的RestTemplate
RestTemplate restTemplate = getCachedRestTemplate(httpClient);
String finalUrlStr = url.toString();
RequestCallback requestCallback = createRequestCallback(finalUrlStr, fileAttribute);
try {
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, response -> {
FileResponseWrapper wrapper = new FileResponseWrapper();
wrapper.setInputStream(response.getBody());
wrapper.setContentType(WebUtils.headersType(response));
try {
handler.handleResponse(wrapper);
} catch (Exception e) {
// 如果是客户端中断连接,不再记录为错误
if (isClientAbortException(e)) {
if (logger.isDebugEnabled()) {
logger.debug("客户端中断连接可能用户取消了下载URL: {}", url);
}
} else {
logger.error("处理文件响应时出错", e);
}
try {
throw e;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
return null;
});
} catch (Exception e) {
// 如果是客户端中断连接,不再记录为错误
if (isClientAbortException(e)) {
if (logger.isDebugEnabled()) {
logger.debug("客户端中断连接URL: {}", url);
}
throw e; // 重新抛出,让调用者处理
}
// 如果是SSL证书错误给出建议
if (e.getMessage() != null &&
(e.getMessage().contains("SSL") ||
e.getMessage().contains("证书") ||
e.getMessage().contains("certificate")) &&
!ConfigConstants.isIgnoreSSL()) {
logger.warn("SSL证书验证失败建议启用SSL忽略功能或检查证书");
}
throw e;
}
// 注意不再关闭HttpClient由连接池管理
}
/**
* 创建请求回调
*/
private static RequestCallback createRequestCallback(String finalUrlStr, FileAttribute fileAttribute) {
return request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
WebUtils.applyBasicAuthHeaders(request.getHeaders(), finalUrlStr);
// 添加Keep-Alive头
request.getHeaders().set("Connection", "keep-alive");
request.getHeaders().set("Keep-Alive", "timeout=60");
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if (StringUtils.hasText(proxyAuthorization)) {
Map<String, String> proxyAuthorizationMap = mapper.readValue(
proxyAuthorization,
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
}
};
}
/**
* 清理资源(在应用关闭时调用)
*/
public static void shutdown() {
logger.info("开始清理HTTP连接池资源...");
// 关闭所有缓存的HttpClient
httpClientCache.values().forEach(client -> {
try {
client.close();
} catch (Exception e) {
logger.warn("关闭HttpClient失败", e);
}
});
httpClientCache.clear();
// 关闭连接池管理器
if (connectionManager != null) {
try {
connectionManager.close();
logger.info("HTTP连接池管理器已关闭");
} catch (Exception e) {
logger.warn("关闭连接池管理器失败", e);
}
connectionManager = null;
}
// 清空RestTemplate缓存
restTemplateCache.clear();
logger.info("HTTP连接池资源清理完成");
}
/**
* 文件响应处理器接口
*/
public interface FileResponseHandler {
void handleResponse(FileResponseWrapper responseWrapper) throws Exception;
}
/**
* 文件响应包装器
*/
public static class FileResponseWrapper {
private InputStream inputStream;
private String contentType;
private boolean hasError;
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public boolean isHasError() {
return hasError;
}
public void setHasError(boolean hasError) {
this.hasError = hasError;
}
}
}

View File

@@ -9,9 +9,9 @@ import org.springframework.web.util.HtmlUtils;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class KkFileUtils {
@@ -19,17 +19,31 @@ public class KkFileUtils {
public static final String DEFAULT_FILE_ENCODING = "UTF-8";
private static final List<String> illegalFileStrList = new ArrayList<>();
// 路径遍历关键字列表
private static final Set<String> illegalFileStrList;
static {
illegalFileStrList.add("../");
illegalFileStrList.add("./");
illegalFileStrList.add("..\\");
illegalFileStrList.add(".\\");
illegalFileStrList.add("\\..");
illegalFileStrList.add("\\.");
illegalFileStrList.add("..");
illegalFileStrList.add("...");
Set<String> set = new HashSet<>();
// 基本路径遍历
Collections.addAll(set, "../", "./", "..\\", ".\\", "\\..", "\\.", "..", "...", "....", ".....");
// URL编码的路径遍历
Collections.addAll(set, "%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "%2e%2e\\", "..%5c",
"%252e%252e%252f", "%252e%252e/", "..%252f");
// Unicode编码绕过
Collections.addAll(set, "\\u002e\\u002e\\u002f", "\\U002e\\U002e\\U002f",
"\u00c0\u00ae\u00c0\u00ae", "\u00c1\u009c\u00c1\u009c");
// 特殊分隔符
Collections.addAll(set, "|..|", "|../|", "|..\\|");
// Windows特殊路径
Collections.addAll(set, "\\\\?\\", "\\\\.\\");
// 转换为不可变集合
illegalFileStrList = Collections.unmodifiableSet(set);
}
/**
@@ -46,6 +60,18 @@ public class KkFileUtils {
}
return false;
}
public static boolean validateFileNameLength(String fileName) {
if (fileName == null) {
return false;
}
// 文件名长度限制255个字符不包含路径
int windowsMaxLength = 255;
if (fileName.length() > windowsMaxLength) {
System.err.println("文件名长度超过限制255个字符");
return false;
}
return true;
}
/**
* 检查是否是数字
@@ -68,7 +94,22 @@ public class KkFileUtils {
* @return 是否http
*/
public static boolean isHttpUrl(URL url) {
return url.getProtocol().toLowerCase().startsWith("file") || url.getProtocol().toLowerCase().startsWith("http");
return url.getProtocol().toLowerCase().startsWith("http") || url.getProtocol().toLowerCase().startsWith("https");
}
/**
* 判断url是否是file资源
*
*/
public static boolean isFileUrl(URL url) {
return url.getProtocol().toLowerCase().startsWith("file");
}
/**
* 判断当前操作系统是否为Windows
*/
static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
/**
@@ -208,5 +249,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

@@ -89,6 +89,9 @@ public class LocalOfficeUtils {
"/opt/libreoffice24.2",
"/opt/libreoffice24.8",
"/opt/libreoffice25.2",
"/opt/libreoffice25.8",
"/opt/libreoffice26.2",
"/opt/libreoffice26.8",
"/usr/lib64/libreoffice",
"/usr/lib/libreoffice",
"/usr/local/lib64/libreoffice",

View File

@@ -0,0 +1,115 @@
package cn.keking.utils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class RemoveSvgAdSimple {
/**
* 更可靠的版本使用DOM解析思路但用正则实现
* @param svgContent SVG内容字符串
* @return 清理后的SVG内容
*/
public static String removeSvgAdReliable(String svgContent) {
StringBuilder cleaned = new StringBuilder();
// 找到XML声明
if (svgContent.contains("<?xml")) {
int xmlEnd = svgContent.indexOf("?>") + 2;
cleaned.append(svgContent, 0, xmlEnd).append("\n");
}
// 找到SVG开始标签
int svgStart = svgContent.indexOf("<svg");
if (svgStart == -1) return svgContent;
int svgEnd = svgContent.indexOf(">", svgStart) + 1;
cleaned.append(svgContent, svgStart, svgEnd).append("\n");
// 解析剩余内容
String remaining = svgContent.substring(svgEnd);
int pos = 0;
while (pos < remaining.length()) {
// 查找下一个<g>标签
int gStart = remaining.indexOf("<g", pos);
if (gStart == -1) break;
// 检查这个<g>标签是否包含transform属性
int gTagEnd = remaining.indexOf(">", gStart);
String gTag = remaining.substring(gStart, gTagEnd + 1);
if (gTag.contains("transform")) {
// 找到对应的</g>标签
int depth = 1;
int searchPos = gTagEnd + 1;
int gClose = -1;
while (searchPos < remaining.length()) {
int nextOpen = remaining.indexOf("<g", searchPos);
int nextClose = remaining.indexOf("</g>", searchPos);
if (nextClose != -1 && (nextClose < nextOpen || nextOpen == -1)) {
depth--;
if (depth == 0) {
gClose = nextClose + 4; // 包括</g>的4个字符
break;
}
searchPos = nextClose + 4;
} else if (nextOpen != -1 && nextOpen < nextClose) {
depth++;
searchPos = nextOpen + 2;
} else {
break;
}
}
if (gClose != -1) {
cleaned.append(remaining, gStart, gClose).append("\n");
pos = gClose;
} else {
pos = gTagEnd + 1;
}
} else {
// 跳过这个<g>元素及其内容
pos = gTagEnd + 1;
}
}
cleaned.append("</svg>");
return cleaned.toString();
}
/**
* 从文件路径读取SVG内容清理广告然后替换原文件
* @param filePath SVG文件路径
* @throws IOException 文件读写异常
*/
public static void removeSvgAdFromFile(String filePath) throws IOException {
removeSvgAdFromFile(filePath, filePath);
}
/**
* 从文件路径读取SVG内容清理广告然后写入目标文件
* @param sourceFilePath 源SVG文件路径
* @param targetFilePath 目标SVG文件路径
* @throws IOException 文件读写异常
*/
public static void removeSvgAdFromFile(String sourceFilePath, String targetFilePath) throws IOException {
// 读取文件内容
Path sourcePath = Paths.get(sourceFilePath);
String svgContent = Files.readString(sourcePath);
// 清理SVG广告
String cleanedContent = removeSvgAdReliable(svgContent);
// 写入目标文件
Path targetPath = Paths.get(targetFilePath);
Files.writeString(targetPath, cleanedContent);
System.out.println("SVG广告清理完成");
}
}

View File

@@ -1,42 +1,110 @@
package cn.keking.utils;
import javax.net.ssl.*;
import java.security.cert.CertificateException;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
* @author 鞠玉果
* @author 高雄
*/
public class SslUtils {
private static void trustAllHttpsCertificates() throws Exception {
TrustManager[] trustAllCerts = new TrustManager[1];
TrustManager tm = new miTM();
trustAllCerts[0] = tm;
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
}
static class miTM implements TrustManager, X509TrustManager {
public X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
/**
* 创建忽略SSL验证的HttpClient适用于HttpClient 5.6
*/
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
return configureHttpClientBuilder(HttpClients.custom(), true, true).build();
}
/**
* 忽略HTTPS请求的SSL证书必须在openConnection之前调用
* 配置HttpClientBuilder支持SSL和重定向配置
* @param builder HttpClientBuilder
* @param ignoreSSL 是否忽略SSL验证
* @param enableRedirect 是否启用重定向
* @return 配置好的HttpClientBuilder
*/
public static void ignoreSsl() throws Exception {
HostnameVerifier hv = (urlHostName, session) -> true;
trustAllHttpsCertificates();
HttpsURLConnection.setDefaultHostnameVerifier(hv);
public static HttpClientBuilder configureHttpClientBuilder(HttpClientBuilder builder,
boolean ignoreSSL,
boolean enableRedirect) throws Exception {
// 配置SSL
if (ignoreSSL) {
// 创建自定义的SSL上下文
SSLContext sslContext = createIgnoreVerifySSL();
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext, NoopHostnameVerifier.INSTANCE);
// 使用连接管理器构建器
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
// 配置连接池参数
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
builder.setConnectionManager(connectionManager);
}
// 配置请求参数
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(72))
.setConnectTimeout(Timeout.ofSeconds(2))
.setRedirectsEnabled(enableRedirect)
.setMaxRedirects(5)
.build();
builder.setDefaultRequestConfig(requestConfig);
if (!enableRedirect) {
builder.disableRedirectHandling();
}
return builder;
}
}
/**
* 创建忽略SSL验证的SSLContext
* 修改为public访问权限
*/
public static SSLContext createIgnoreVerifySSL() throws Exception {
// 使用TLSv1.2或TLSv1.3
SSLContext sc = SSLContext.getInstance("TLSv1.2");
// 实现一个X509TrustManager忽略所有证书验证
X509TrustManager trustManager = new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// 信任所有客户端证书
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// 信任所有服务器证书
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
sc.init(null, new javax.net.ssl.TrustManager[]{trustManager}, new java.security.SecureRandom());
return sc;
}
}

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

@@ -1,10 +1,15 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;
@@ -17,8 +22,7 @@ import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,6 +34,18 @@ public class WebUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
private static final String BASE64_MSG = "base64";
private static final String URL_PARAM_BASIC_NAME = "basic.name";
private static final String URL_PARAM_BASIC_PASS = "basic.pass";
private static final Map<String, String> ERROR_MESSAGES = Map.of(
"base64", "KK提醒您:接入方法错误未使用BASE64",
"base641", "KK提醒您:BASE64解码异常,确认是否正确使用BASE64编码",
"Keyerror", "KK提醒您:AES解码错误请检测你的秘钥是否正确",
"base64error", "KK提醒您:你选用的是ASE加密实际用了BASE64加密接入",
"byteerror", "KK提醒您:解码异常,检测你接入方法是否正确"
);
private static final String EMPTY_URL_MSG = "KK提醒您:地址不能为空";
private static final String INVALID_URL_MSG = "KK提醒您:请正确使用URL(必须包括https ftp file 协议)";
/**
* 获取标准的URL
*
@@ -46,11 +62,10 @@ public class WebUtils {
*
*/
public static String encodeFileName(String name) {
try {
name = URLEncoder.encode(name, "UTF-8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
return null;
}
name = URLEncoder.encode(name, StandardCharsets.UTF_8)
.replaceAll("%2F", "/") // 恢复斜杠
.replaceAll("%5C", "/") // 恢复反斜杠
.replaceAll("\\+", "%20"); // 空格处理
return name;
}
@@ -215,11 +230,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);
}
@@ -234,17 +245,18 @@ public class WebUtils {
String urls = request.getParameter("urls");
String currentUrl = request.getParameter("currentUrl");
String urlPath = request.getParameter("urlPath");
String encryption = request.getParameter("encryption");
if (StringUtils.isNotBlank(url)) {
return decodeUrl(url);
return decodeUrl(url,encryption);
}
if (StringUtils.isNotBlank(currentUrl)) {
return decodeUrl(currentUrl);
return decodeUrl(currentUrl,encryption);
}
if (StringUtils.isNotBlank(urlPath)) {
return decodeUrl(urlPath);
return decodeUrl(urlPath,encryption);
}
if (StringUtils.isNotBlank(urls)) {
urls = decodeUrl(urls);
urls = decodeUrl(urls,encryption);
String[] images = urls.split("\\|");
return images[0];
}
@@ -268,13 +280,20 @@ public class WebUtils {
*
* aHR0cHM6Ly9maWxlLmtla2luZy5jbi9kZW1vL%2BS4reaWhy5wcHR4 -> https://file.keking.cn/demo/%E4%B8%AD%E6%96%87.pptx -> https://file.keking.cn/demo/中文.pptx
*/
public static String decodeUrl(String source) {
String url = decodeBase64String(source, StandardCharsets.UTF_8);
if (! StringUtils.isNotBlank(url)){
return null;
public static String decodeUrl(String source,String encryption) {
String url;
if(ObjectUtils.isEmpty(encryption) || Objects.equals(ConfigConstants.getaesKey(), "false")){
encryption = "base64";
}
if(Objects.equals(encryption.toLowerCase(), "aes")){
return AESUtil.AesDecrypt(source);
}else {
url = decodeBase64String(source, StandardCharsets.UTF_8);
if(!isValidUrl(url)){
url="base641";
}
return url;
}
return url;
}
/**
@@ -301,6 +320,30 @@ public class WebUtils {
}
}
public static String urlSecurity(String url) {
if (ObjectUtils.isEmpty(url)) {
return EMPTY_URL_MSG;
}
// 检查已知的错误类型
String errorMsg = ERROR_MESSAGES.get(url);
if (errorMsg != null) {
return errorMsg;
}
// 验证URL格式
if (!isValidUrl(url)) {
return INVALID_URL_MSG;
}
// file协议特殊处理
if (url.toLowerCase().startsWith("file://")) {
// 对于本地文件可以返回URL本身或进行特殊处理
// 根据业务需求决定返回URL、返回特殊标识或进行本地文件安全检查
return url; // 或者返回特殊标识如 "file-protocol"
}
// 提取主机名
return getHost(url);
}
/**
* 获取 url 的 host
* @param urlStr url
@@ -371,4 +414,165 @@ public class WebUtils {
}
session.removeAttribute(key);
}
public static boolean validateKey(String key) {
String configKey = ConfigConstants.getKey();
return !"false".equals(configKey) && !configKey.equals(key);
}
public static String getContentTypeByFilename(String filename) {
String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
switch (extension) {
case "pdf": return "application/pdf";
case "jpg": case "jpeg": return "image/jpeg";
case "png": return "image/png";
case "gif": return "image/gif";
case "svg": return "image/svg+xml";
case "txt": return "text/plain";
case "html": case "htm": return "text/html";
case "xml": return "application/xml";
case "json": return "application/json";
default: return null;
}
}
/**
* name pass 获取用户名 和密码
*/
public static String[] namePass(String url,String name) {
url= getHost(url);
String[] items = name.split(",\\s*");
String toRemove = ":";
String names = null;
String[] parts = null;
try {
for (String item : items) {
int index = item.indexOf(toRemove);
if (index != -1) {
String result = item.substring(0, index);
if (Objects.equals(result, url)) {
names = item;
}
}
}
if (names !=null){
parts = names.split(toRemove);
}
} catch (Exception e) {
LOGGER.error("获取认证权限错误:",e);
}
return parts;
}
/**
* 获取Content-Type
*/
public static String headersType(ClientHttpResponse fileResponse) {
if (fileResponse == null) {
return null;
}
HttpHeaders headers = fileResponse.getHeaders();
if (headers == null) {
return null;
}
String contentTypeStr = null;
try {
// 直接获取Content-Type头字符串
contentTypeStr = headers.getFirst(HttpHeaders.CONTENT_TYPE);
if (contentTypeStr == null || contentTypeStr.isEmpty()) {
return null;
}
// 解析为MediaType对象
MediaType mediaType = MediaType.parseMediaType(contentTypeStr);
// 返回主类型和子类型,忽略参数
return mediaType.getType() + "/" + mediaType.getSubtype();
} catch (Exception e) {
// 如果解析失败,尝试简单的字符串处理
if (contentTypeStr != null) {
// 移除分号及后面的参数
int semicolonIndex = contentTypeStr.indexOf(';');
if (semicolonIndex > 0) {
return contentTypeStr.substring(0, semicolonIndex).trim();
}
return contentTypeStr.trim();
}
return null;
}
}
/**
* 判断文件是否需要校验MIME类型
* @param suffix 文件后缀
* @return 是否需要校验
*/
public static boolean isMimeCheckRequired(String suffix) {
if (suffix == null) {
return false;
}
String lowerSuffix = suffix.toLowerCase();
return Arrays.asList(
"doc", "docx", "ppt", "pptx", "pdf", "dwg",
"dxf", "dwf", "psd", "wps", "xlsx", "xls",
"rar", "zip"
).contains(lowerSuffix);
}
/**
* 校验文件MIME类型是否有效
* @param contentType 响应头中的Content-Type
* @param suffix 文件后缀
* @return 是否有效
*/
public static boolean isValidMimeType(String contentType, String suffix) {
if (contentType == null || suffix == null) {
return true;
}
// 如果检测到是HTML、文本或JSON格式则认为是错误响应
String lowerContentType = contentType.toLowerCase();
return !lowerContentType.contains("text/html")
&& !lowerContentType.contains("text/plain")
&& !lowerContentType.contains("application/json");
}
/**
* 支持basic 下载方法
*/
public static void applyBasicAuthHeaders(HttpHeaders headers, String 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";
if (!StringUtils.isEmpty(customUserAgent) && !Objects.equals(customUserAgent, "false")) {
userAgent = customUserAgent;
}
headers.set("User-Agent", userAgent);
// 获取用户名和密码
String username = null;
String password = null;
// 从basic配置获取
String basic = ConfigConstants.getBasicName();
if (!StringUtils.isEmpty(basic) && !Objects.equals(basic, "false")) {
String[] urlUser = namePass(url, basic);
if (urlUser != null && urlUser.length >= 3) {
username = urlUser[1];
password = urlUser[2];
}
}
// URL参数优先
String basicUsername = getUrlParameterReg(url, URL_PARAM_BASIC_NAME);
String basicPassword = getUrlParameterReg(url, URL_PARAM_BASIC_PASS);
if (!StringUtils.isEmpty(basicUsername)) {
username = basicUsername;
password = basicPassword;
}
// 设置Basic Auth
if (!StringUtils.isEmpty(username)) {
String plainCredentials = username + ":" + (password != null ? password : "");
String base64Credentials = java.util.Base64.getEncoder().encodeToString(plainCredentials.getBytes());
headers.set("Authorization", "Basic " + base64Credentials);
}
}
}

View File

@@ -1,32 +1,25 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import cn.keking.utils.*;
import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -35,9 +28,10 @@ import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
import static cn.keking.utils.KkFileUtils.isFtpUrl;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
/**
* @author yudian-it
@@ -45,16 +39,19 @@ import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
@Controller
public class OnlinePreviewController {
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private final Logger logger = LoggerFactory.getLogger(OnlinePreviewController.class);
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private static final String ILLEGAL_ACCESS_MSG = "访问不合法:访问密码不正确";
private static final String INTERFACE_CLOSED_MSG = "接口关闭,禁止访问!";
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private final FilePreviewFactory previewFactory;
private final CacheService cacheService;
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper();
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
this.previewFactory = filePreviewFactory;
@@ -64,16 +61,31 @@ public class OnlinePreviewController {
}
@GetMapping( "/onlinePreview")
public String onlinePreview(String url, Model model, HttpServletRequest req) {
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) {
// 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(url);
fileUrl = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
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());
@@ -85,10 +97,22 @@ public class OnlinePreviewController {
}
@GetMapping( "/picturesPreview")
public String picturesPreview(String urls, Model model, HttpServletRequest req) {
public String picturesPreview(@RequestParam String urls,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption,
Model model,
HttpServletRequest req) {
// 1. 验证接口是否开启
if (!ConfigConstants.getPicturesPreview()) {
return otherFilePreview.notSupportedFile(model, INTERFACE_CLOSED_MSG);
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
return otherFilePreview.notSupportedFile(model, ILLEGAL_ACCESS_MSG);
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(urls);
fileUrls = WebUtils.decodeUrl(urls, encryption);
// 防止XSS攻击
fileUrls = KkFileUtils.htmlEscape(fileUrls);
} catch (Exception ex) {
@@ -103,7 +127,7 @@ public class OnlinePreviewController {
String currentUrl = req.getParameter("currentUrl");
if (StringUtils.hasText(currentUrl)) {
String decodedCurrentUrl = new String(Base64.decodeBase64(currentUrl));
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
decodedCurrentUrl = KkFileUtils.htmlEscape(decodedCurrentUrl); // 防止XSS攻击
model.addAttribute("currentUrl", decodedCurrentUrl);
} else {
model.addAttribute("currentUrl", imgUrls.get(0));
@@ -119,51 +143,54 @@ public class OnlinePreviewController {
* @param response response
*/
@GetMapping("/getCorsFile")
public void getCorsFile(String urlPath, HttpServletResponse response,FileAttribute fileAttribute) throws IOException {
public void getCorsFile(@RequestParam String urlPath,
@RequestParam(required = false) String key,
HttpServletResponse response,
HttpServletRequest req,
@RequestParam(required = false) String encryption) throws Exception {
// 1. 验证接口是否开启
if (!ConfigConstants.getGetCorsFile()) {
logger.info("接口关闭,禁止访问!url{}", urlPath);
return;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
logger.info("访问不合法:访问密码不正确!url{}", urlPath);
return;
}
URL url;
try {
urlPath = WebUtils.decodeUrl(urlPath);
urlPath = WebUtils.decodeUrl(urlPath, encryption);
url = WebUtils.normalizedURL(urlPath);
} catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex);
return;
}
assert urlPath != null;
if (!urlPath.toLowerCase().startsWith("http") && !urlPath.toLowerCase().startsWith("https") && !urlPath.toLowerCase().startsWith("ftp")) {
if (!isHttpUrl(url) && !isFtpUrl(url)) {
logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return;
}
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath);
if (!urlPath.toLowerCase().startsWith("ftp:")) {
factory.setConnectionRequestTimeout(2000);
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000);
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
if(StringUtils.hasText(proxyAuthorization)){
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
}
};
logger.info("读取跨域文件url{}", urlPath);
if (!isFtpUrl(url)) {
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
} else {
try {
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
IOUtils.copy(fileResponse.getBody(), response.getOutputStream());
return null;
});
} catch (Exception e) {
System.out.println(e);
}
}else{
try {
if(urlPath.contains(".svg")) {
response.setContentType("image/svg+xml");
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
String contentType = WebUtils.getContentTypeByFilename(filename);
if (contentType != null) {
response.setContentType(contentType);
}
inputStream = (url).openStream();
String ftpUsername = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING);
String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT);
inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
IOUtils.copy(inputStream, response.getOutputStream());
} catch (IOException e) {
logger.error("读取跨域文件异常url{}", urlPath);
@@ -180,9 +207,32 @@ public class OnlinePreviewController {
*/
@GetMapping("/addTask")
@ResponseBody
public String addQueueTask(String url) {
logger.info("添加转码队列url{}", url);
cacheService.addQueueTask(url);
public String addQueueTask(@RequestParam String url,
@RequestParam(required = false) String key,
@RequestParam(required = false) String encryption) {
// 1. 验证接口是否开启
if (!ConfigConstants.getAddTask()) {
String errorMsg = "接口关闭,禁止访问!";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
String fileUrls;
try {
fileUrls = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) {
String errorMsg = "Url解析错误";
logger.info("{}url{}", errorMsg, url);
return errorMsg;
}
//2. 验证访问权限
if (WebUtils.validateKey(key)) {
String errorMsg = "访问不合法:访问密码不正确!";
logger.info("{}url{}", errorMsg, fileUrls);
return errorMsg;
}
logger.info("添加转码队列url{}", fileUrls);
cacheService.addQueueTask(fileUrls);
return "success";
}
}
}

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());
}
/**

View File

@@ -1,6 +1,8 @@
package cn.keking.web.filter;
import cn.keking.config.ConfigConstants;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import io.mola.galimatias.GalimatiasParseException;
import org.jodconverter.core.util.OSUtils;
@@ -11,6 +13,8 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import jakarta.servlet.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
@@ -56,18 +60,42 @@ public class TrustDirFilter implements Filter {
}
private boolean allowPreview(String urlPath) {
//判断URL是否合法
if(!StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false ;
// 判断URL是否合法
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false;
}
try {
URL url = WebUtils.normalizedURL(urlPath);
if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) {
String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
if (OSUtils.IS_OS_WINDOWS) {
filePath = filePath.replaceAll("/", "\\\\");
// 将文件路径转换为File对象
File targetFile = new File(filePath);
// 将配置目录也转换为File对象
File fileDir = new File(ConfigConstants.getFileDir());
File localPreviewDir = new File(ConfigConstants.getLocalPreviewDir());
try {
// 获取规范路径(系统会自动处理大小写、符号链接、相对路径等)
String canonicalFilePath = targetFile.getCanonicalPath();
String canonicalFileDir = fileDir.getCanonicalPath();
String canonicalLocalPreviewDir = localPreviewDir.getCanonicalPath();
// 检查文件是否在配置目录下
return isSubDirectory(canonicalFileDir, canonicalFilePath) || isSubDirectory(canonicalLocalPreviewDir, canonicalFilePath);
} catch (IOException e) {
logger.warn("获取规范路径失败,使用原始路径比较", e);
// 如果获取规范路径失败,回退到原始路径比较
String absFilePath = targetFile.getAbsolutePath();
String absFileDir = fileDir.getAbsolutePath();
String absLocalPreviewDir = localPreviewDir.getAbsolutePath();
// 统一路径分隔符
absFilePath = absFilePath.replace('\\', '/');
absFileDir = absFileDir.replace('\\', '/');
absLocalPreviewDir = absLocalPreviewDir.replace('\\', '/');
// 确保目录以斜杠结尾
if (!absFileDir.endsWith("/")) absFileDir += "/";
if (!absLocalPreviewDir.endsWith("/")) absLocalPreviewDir += "/";
return absFilePath.startsWith(absFileDir) || absFilePath.startsWith(absLocalPreviewDir);
}
return filePath.startsWith(ConfigConstants.getFileDir()) || filePath.startsWith(ConfigConstants.getLocalPreviewDir());
}
return true;
} catch (IOException | GalimatiasParseException e) {
@@ -75,4 +103,26 @@ public class TrustDirFilter implements Filter {
return false;
}
}
/**
* 检查子路径是否在父路径下(跨平台)
*/
private boolean isSubDirectory(String parentDir, String childPath) {
try {
File parent = new File(parentDir);
File child = new File(childPath);
// 获取规范路径
String canonicalParent = parent.getCanonicalPath();
String canonicalChild = child.getCanonicalPath();
// 确保父目录以路径分隔符结尾
if (!canonicalParent.endsWith(File.separator)) {
canonicalParent += File.separator;
}
// 比较路径
return canonicalChild.startsWith(canonicalParent);
} catch (IOException e) {
logger.warn("检查子路径失败", e);
return false;
}
}
}

View File

@@ -50,7 +50,7 @@ public class TrustHostFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = WebUtils.getSourceUrl(request);
String host = WebUtils.getHost(url);
if (isNotTrustHost(host)) {
if (isNotTrustHost(host) || !WebUtils.isValidUrl(url)) {
String currentHost = host == null ? "UNKNOWN" : host;
if (response instanceof HttpServletResponse httpServletResponse) {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
@@ -78,7 +78,6 @@ public class TrustHostFilter implements Filter {
&& matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) {
return true;
}
// 如果配置了白名单,检查是否在白名单中
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
// 支持通配符 * 表示允许所有主机

View File

@@ -8,7 +8,7 @@
=> Java Version :: ${java.version}
=> Spring Boot :: ${spring-boot.version}
=> kkFileView :: 4.4.0
=> kkFileView :: 5.0
=> Home site :: https://kkview.cn
=> Github :: https://github.com/kekingcn/kkFileView
=> Gitee :: https://gitee.com/kekingcn/file-online-preview

View File

@@ -0,0 +1,479 @@
/*!
* Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.4/customize/)
*/
/*!
* Bootstrap v3.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
audio,
canvas,
progress,
video {
display: inline-block;
vertical-align: baseline;
}
audio:not([controls]) {
display: none;
height: 0;
}
[hidden],
template {
display: none;
}
a {
background-color: transparent;
}
a:active,
a:hover {
outline: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
mark {
background: #ff0;
color: #000;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
img {
border: 0;
}
svg:not(:root) {
overflow: hidden;
}
figure {
margin: 1em 40px;
}
hr {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
pre {
overflow: auto;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0;
}
button {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html input[type="button"],
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button;
cursor: pointer;
}
button[disabled],
html input[disabled] {
cursor: default;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
input {
line-height: normal;
}
input[type="checkbox"],
input[type="radio"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
input[type="search"] {
-webkit-appearance: textfield;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
border: 0;
padding: 0;
}
textarea {
overflow: auto;
}
optgroup {
font-weight: bold;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
*:before,
*:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
font-size: 10px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #333333;
background-color: #ffffff;
}
input,
button,
select,
textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
a {
color: #337ab7;
text-decoration: none;
}
a:hover,
a:focus {
color: #23527c;
text-decoration: underline;
}
a:focus {
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
figure {
margin: 0;
}
img {
vertical-align: middle;
}
.img-responsive {
display: block;
max-width: 100%;
height: auto;
}
.img-rounded {
border-radius: 6px;
}
.img-thumbnail {
padding: 4px;
line-height: 1.42857143;
background-color: #ffffff;
border: 1px solid #dddddd;
border-radius: 4px;
-webkit-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
display: inline-block;
max-width: 100%;
height: auto;
}
.img-circle {
border-radius: 50%;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eeeeee;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
[role="button"] {
cursor: pointer;
}
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid \9;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.dropup,
.dropdown {
position: relative;
}
.dropdown-toggle:focus {
outline: 0;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #ffffff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.dropdown-menu.pull-right {
right: 0;
left: auto;
}
.dropdown-menu .divider {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
}
.dropdown-menu > li > a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333333;
white-space: nowrap;
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
color: #ffffff;
text-decoration: none;
background-color: #337ab7;
outline: 0;
}
.dropdown-menu > .disabled > a,
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
color: #777777;
}
.dropdown-menu > .disabled > a:hover,
.dropdown-menu > .disabled > a:focus {
text-decoration: none;
cursor: not-allowed;
background-color: transparent;
background-image: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.open > .dropdown-menu {
display: block;
}
.open > a {
outline: 0;
}
.dropdown-menu-right {
right: 0;
left: auto;
}
.dropdown-menu-left {
right: auto;
left: 0;
}
.dropdown-header {
display: block;
padding: 3px 20px;
font-size: 12px;
line-height: 1.42857143;
color: #777777;
white-space: nowrap;
}
.dropdown-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 990;
}
.pull-right > .dropdown-menu {
right: 0;
left: auto;
}
.dropup .caret,
.navbar-fixed-bottom .dropdown .caret {
content: "";
border-top: 0;
border-bottom: 4px dashed;
border-bottom: 4px solid \9;
}
.dropup .dropdown-menu,
.navbar-fixed-bottom .dropdown .dropdown-menu {
top: auto;
bottom: 100%;
margin-bottom: 2px;
}
@media (min-width: 768px) {
.navbar-right .dropdown-menu {
right: 0;
left: auto;
}
.navbar-right .dropdown-menu-left {
right: auto;
left: 0;
}
}
.clearfix:before,
.clearfix:after {
display: table;
content: " ";
}
.clearfix:after {
clear: both;
}
.center-block {
display: block;
margin-right: auto;
margin-left: auto;
}
.pull-right {
float: right !important;
}
.pull-left {
float: left !important;
}
.hide {
display: none !important;
}
.show {
display: block !important;
}
.invisible {
visibility: hidden;
}
.text-hide {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.hidden {
display: none !important;
}
.affix {
position: fixed;
}

View File

@@ -0,0 +1 @@
span.multiselect-native-select{position:relative}span.multiselect-native-select select{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px -1px -1px -3px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;left:50%;top:30px}.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container .multiselect-reset .input-group{width:93%}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.checkbox,.multiselect-container>li>a>label.radio{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0}

View File

@@ -0,0 +1,444 @@
.cadviewer-bootstrap {
/*!
* Generated using the Bootstrap Customizer (https://getbootstrap.com/docs/3.4/customize/)
*/
/*!
* Bootstrap v3.4.1 (https://getbootstrap.com/)
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
}
.cadviewer-bootstrap html {
font-family: sans-serif;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
.cadviewer-bootstrap body {
margin: 0;
}
.cadviewer-bootstrap article, .cadviewer-bootstrap aside, .cadviewer-bootstrap details, .cadviewer-bootstrap figcaption, .cadviewer-bootstrap figure, .cadviewer-bootstrap footer, .cadviewer-bootstrap header, .cadviewer-bootstrap hgroup, .cadviewer-bootstrap main, .cadviewer-bootstrap menu, .cadviewer-bootstrap nav, .cadviewer-bootstrap section, .cadviewer-bootstrap summary {
display: block;
}
.cadviewer-bootstrap audio, .cadviewer-bootstrap canvas, .cadviewer-bootstrap progress, .cadviewer-bootstrap video {
display: inline-block;
vertical-align: baseline;
}
.cadviewer-bootstrap audio:not([controls]) {
display: none;
height: 0;
}
.cadviewer-bootstrap [hidden], .cadviewer-bootstrap template {
display: none;
}
.cadviewer-bootstrap a {
background-color: transparent;
}
.cadviewer-bootstrap a:active, .cadviewer-bootstrap a:hover {
outline: 0;
}
.cadviewer-bootstrap abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
.cadviewer-bootstrap b, .cadviewer-bootstrap strong {
font-weight: bold;
}
.cadviewer-bootstrap dfn {
font-style: italic;
}
.cadviewer-bootstrap h1 {
font-size: 2em;
margin: 0.67em 0;
}
.cadviewer-bootstrap mark {
background: #ff0;
color: #000;
}
.cadviewer-bootstrap small {
font-size: 80%;
}
.cadviewer-bootstrap sub, .cadviewer-bootstrap sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.cadviewer-bootstrap sup {
top: -0.5em;
}
.cadviewer-bootstrap sub {
bottom: -0.25em;
}
.cadviewer-bootstrap img {
border: 0;
}
.cadviewer-bootstrap svg:not(:root) {
overflow: hidden;
}
.cadviewer-bootstrap figure {
margin: 1em 40px;
}
.cadviewer-bootstrap hr {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
.cadviewer-bootstrap pre {
overflow: auto;
}
.cadviewer-bootstrap code, .cadviewer-bootstrap kbd, .cadviewer-bootstrap pre, .cadviewer-bootstrap samp {
font-family: monospace, monospace;
font-size: 1em;
}
.cadviewer-bootstrap button, .cadviewer-bootstrap input, .cadviewer-bootstrap optgroup, .cadviewer-bootstrap select, .cadviewer-bootstrap textarea {
color: inherit;
font: inherit;
margin: 0;
/* 8.45.2 */
/*8.45.2*/
border-radius: 0px !important;
border-width : 1px !important;
border-color: #000 !important;
}
.cadviewer-bootstrap button {
overflow: visible;
}
.cadviewer-bootstrap button, .cadviewer-bootstrap select {
text-transform: none;
}
.cadviewer-bootstrap button, .cadviewer-bootstrap html input[type="button"], .cadviewer-bootstrap input[type="reset"], .cadviewer-bootstrap input[type="submit"] {
-webkit-appearance: button;
cursor: pointer;
}
.cadviewer-bootstrap button[disabled], .cadviewer-bootstrap html input[disabled] {
cursor: default;
}
.cadviewer-bootstrap button::-moz-focus-inner, .cadviewer-bootstrap input::-moz-focus-inner {
border: 0;
padding: 0;
}
.cadviewer-bootstrap input {
line-height: 22px;
/*8.45.2*/
border-radius: 0px !important;
border-width : 1px !important;
border-color: #000 !important;
}
.cadviewer-bootstrap input[type="checkbox"], .cadviewer-bootstrap input[type="radio"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 0;
border-radius: 0px;
height: 20px !important;
}
.cadviewer-bootstrap input[type="number"]::-webkit-inner-spin-button, .cadviewer-bootstrap input[type="number"]::-webkit-outer-spin-button {
height: auto;
border-radius: 3px;
}
.cadviewer-bootstrap input[type="search"] {
-webkit-appearance: textfield;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.cadviewer-bootstrap input[type="search"]::-webkit-search-cancel-button, .cadviewer-bootstrap input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
.cadviewer-bootstrap fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
.cadviewer-bootstrap legend {
border: 0;
padding: 0;
}
.cadviewer-bootstrap textarea {
overflow: auto;
}
.cadviewer-bootstrap optgroup {
font-weight: bold;
}
.cadviewer-bootstrap table {
border-collapse: collapse;
border-spacing: 0;
}
.cadviewer-bootstrap td, .cadviewer-bootstrap th {
padding: 0;
}
.cadviewer-bootstrap * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.cadviewer-bootstrap *:before, .cadviewer-bootstrap *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.cadviewer-bootstrap html {
font-size: 10px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.cadviewer-bootstrap input, .cadviewer-bootstrap button, .cadviewer-bootstrap select, .cadviewer-bootstrap textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
/*8.45.2*/
border-radius: 0px !important;
border-width : 1px !important;
border-color: #000 !important;
}
.cadviewer-bootstrap a {
color: #337ab7;
text-decoration: none;
}
.cadviewer-bootstrap #floorPlan_svg a:hover, .cadviewer-bootstrap #floorPlan_svg a:focus {
color: #23527c;
text-decoration: underline;
}
.cadviewer-bootstrap a:focus {
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
.cadviewer-bootstrap figure {
margin: 0;
}
.cadviewer-bootstrap img {
vertical-align: middle;
}
.cadviewer-bootstrap .img-responsive {
display: block;
max-width: 100%;
height: auto;
}
.cadviewer-bootstrap .img-rounded {
border-radius: 6px;
}
.cadviewer-bootstrap .img-thumbnail {
padding: 4px;
line-height: 1.42857143;
background-color: #ffffff;
border: 1px solid #dddddd;
border-radius: 4px;
-webkit-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
display: inline-block;
max-width: 100%;
height: auto;
}
.cadviewer-bootstrap .img-circle {
border-radius: 50%;
}
.cadviewer-bootstrap hr {
margin-top: 20px;
margin-bottom: 20px;
border: 0;
border-top: 1px solid #eeeeee;
}
.cadviewer-bootstrap .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.cadviewer-bootstrap .sr-only-focusable:active, .cadviewer-bootstrap .sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
.cadviewer-bootstrap [role="button"] {
cursor: pointer;
}
.cadviewer-bootstrap .caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid \9;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
.cadviewer-bootstrap .dropup, .cadviewer-bootstrap .dropdown {
position: relative;
}
.cadviewer-bootstrap .dropdown-toggle:focus {
outline: 0;
}
.cadviewer-bootstrap .dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #ffffff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
.cadviewer-bootstrap .dropdown-menu.pull-right {
right: 0;
left: auto;
}
.cadviewer-bootstrap .dropdown-menu .divider {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
}
.cadviewer-bootstrap .dropdown-menu > li > a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333333;
white-space: nowrap;
}
.cadviewer-bootstrap .dropdown-menu > li > a:hover, .cadviewer-bootstrap .dropdown-menu > li > a:focus {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}
.cadviewer-bootstrap .dropdown-menu > .active > a, .cadviewer-bootstrap .dropdown-menu > .active > a:hover, .cadviewer-bootstrap .dropdown-menu > .active > a:focus {
color: #ffffff;
text-decoration: none;
background-color: #337ab7;
outline: 0;
}
.cadviewer-bootstrap .dropdown-menu > .disabled > a, .cadviewer-bootstrap .dropdown-menu > .disabled > a:hover, .cadviewer-bootstrap .dropdown-menu > .disabled > a:focus {
color: #777777;
}
.cadviewer-bootstrap .dropdown-menu > .disabled > a:hover, .cadviewer-bootstrap .dropdown-menu > .disabled > a:focus {
text-decoration: none;
cursor: not-allowed;
background-color: transparent;
background-image: none;
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
}
.cadviewer-bootstrap .open > .dropdown-menu {
display: block;
}
.cadviewer-bootstrap .open > a {
outline: 0;
}
.cadviewer-bootstrap .dropdown-menu-right {
right: 0;
left: auto;
}
.cadviewer-bootstrap .dropdown-menu-left {
right: auto;
left: 0;
}
.cadviewer-bootstrap .dropdown-header {
display: block;
padding: 3px 20px;
font-size: 12px;
line-height: 1.42857143;
color: #777777;
white-space: nowrap;
}
.cadviewer-bootstrap .dropdown-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 990;
}
.cadviewer-bootstrap .pull-right > .dropdown-menu {
right: 0;
left: auto;
}
.cadviewer-bootstrap .dropup .caret, .cadviewer-bootstrap .navbar-fixed-bottom .dropdown .caret {
content: "";
border-top: 0;
border-bottom: 4px dashed;
border-bottom: 4px solid \9;
}
.cadviewer-bootstrap .dropup .dropdown-menu, .cadviewer-bootstrap .navbar-fixed-bottom .dropdown .dropdown-menu {
top: auto;
bottom: 100%;
margin-bottom: 2px;
}
@media (min-width: 768px) {
.cadviewer-bootstrap .navbar-right .dropdown-menu {
right: 0;
left: auto;
}
.cadviewer-bootstrap .navbar-right .dropdown-menu-left {
right: auto;
left: 0;
}
}
.cadviewer-bootstrap .clearfix:before, .cadviewer-bootstrap .clearfix:after {
display: table;
content: " ";
}
.cadviewer-bootstrap .clearfix:after {
clear: both;
}
.cadviewer-bootstrap .center-block {
display: block;
margin-right: auto;
margin-left: auto;
}
.cadviewer-bootstrap .pull-right {
float: right !important;
}
.cadviewer-bootstrap .pull-left {
float: left !important;
}
.cadviewer-bootstrap .hide {
display: none !important;
}
.cadviewer-bootstrap .show {
display: block !important;
}
.cadviewer-bootstrap .invisible {
visibility: hidden;
}
.cadviewer-bootstrap .text-hide {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
.cadviewer-bootstrap .hidden {
display: none !important;
}
.cadviewer-bootstrap .affix {
position: fixed;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,391 @@
{
"vars": {
"@gray-base": "#000",
"@gray-darker": "lighten(@gray-base, 13.5%)",
"@gray-dark": "lighten(@gray-base, 20%)",
"@gray": "lighten(@gray-base, 33.5%)",
"@gray-light": "lighten(@gray-base, 46.7%)",
"@gray-lighter": "lighten(@gray-base, 93.5%)",
"@brand-primary": "darken(#428bca, 6.5%)",
"@brand-success": "#5cb85c",
"@brand-info": "#5bc0de",
"@brand-warning": "#f0ad4e",
"@brand-danger": "#d9534f",
"@body-bg": "#fff",
"@text-color": "@gray-dark",
"@link-color": "@brand-primary",
"@link-hover-color": "darken(@link-color, 15%)",
"@link-hover-decoration": "underline",
"@font-family-sans-serif": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
"@font-family-serif": "Georgia, \"Times New Roman\", Times, serif",
"@font-family-monospace": "Menlo, Monaco, Consolas, \"Courier New\", monospace",
"@font-family-base": "@font-family-sans-serif",
"@font-size-base": "14px",
"@font-size-large": "ceil((@font-size-base * 1.25))",
"@font-size-small": "ceil((@font-size-base * .85))",
"@font-size-h1": "floor((@font-size-base * 2.6))",
"@font-size-h2": "floor((@font-size-base * 2.15))",
"@font-size-h3": "ceil((@font-size-base * 1.7))",
"@font-size-h4": "ceil((@font-size-base * 1.25))",
"@font-size-h5": "@font-size-base",
"@font-size-h6": "ceil((@font-size-base * .85))",
"@line-height-base": "1.428571429",
"@line-height-computed": "floor((@font-size-base * @line-height-base))",
"@headings-font-family": "inherit",
"@headings-font-weight": "500",
"@headings-line-height": "1.1",
"@headings-color": "inherit",
"@icon-font-path": "\"../fonts/\"",
"@icon-font-name": "\"glyphicons-halflings-regular\"",
"@icon-font-svg-id": "\"glyphicons_halflingsregular\"",
"@padding-base-vertical": "6px",
"@padding-base-horizontal": "12px",
"@padding-large-vertical": "10px",
"@padding-large-horizontal": "16px",
"@padding-small-vertical": "5px",
"@padding-small-horizontal": "10px",
"@padding-xs-vertical": "1px",
"@padding-xs-horizontal": "5px",
"@line-height-large": "1.3333333",
"@line-height-small": "1.5",
"@border-radius-base": "4px",
"@border-radius-large": "6px",
"@border-radius-small": "3px",
"@component-active-color": "#fff",
"@component-active-bg": "@brand-primary",
"@caret-width-base": "4px",
"@caret-width-large": "5px",
"@table-cell-padding": "8px",
"@table-condensed-cell-padding": "5px",
"@table-bg": "transparent",
"@table-bg-accent": "#f9f9f9",
"@table-bg-hover": "#f5f5f5",
"@table-bg-active": "@table-bg-hover",
"@table-border-color": "#ddd",
"@btn-font-weight": "normal",
"@btn-default-color": "#333",
"@btn-default-bg": "#fff",
"@btn-default-border": "#ccc",
"@btn-primary-color": "#fff",
"@btn-primary-bg": "@brand-primary",
"@btn-primary-border": "darken(@btn-primary-bg, 5%)",
"@btn-success-color": "#fff",
"@btn-success-bg": "@brand-success",
"@btn-success-border": "darken(@btn-success-bg, 5%)",
"@btn-info-color": "#fff",
"@btn-info-bg": "@brand-info",
"@btn-info-border": "darken(@btn-info-bg, 5%)",
"@btn-warning-color": "#fff",
"@btn-warning-bg": "@brand-warning",
"@btn-warning-border": "darken(@btn-warning-bg, 5%)",
"@btn-danger-color": "#fff",
"@btn-danger-bg": "@brand-danger",
"@btn-danger-border": "darken(@btn-danger-bg, 5%)",
"@btn-link-disabled-color": "@gray-light",
"@btn-border-radius-base": "@border-radius-base",
"@btn-border-radius-large": "@border-radius-large",
"@btn-border-radius-small": "@border-radius-small",
"@input-bg": "#fff",
"@input-bg-disabled": "@gray-lighter",
"@input-color": "@gray",
"@input-border": "#ccc",
"@input-border-radius": "@border-radius-base",
"@input-border-radius-large": "@border-radius-large",
"@input-border-radius-small": "@border-radius-small",
"@input-border-focus": "#66afe9",
"@input-color-placeholder": "#999",
"@input-height-base": "(@line-height-computed + (@padding-base-vertical * 2) + 2)",
"@input-height-large": "(ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2)",
"@input-height-small": "(floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2)",
"@form-group-margin-bottom": "15px",
"@legend-color": "@gray-dark",
"@legend-border-color": "#e5e5e5",
"@input-group-addon-bg": "@gray-lighter",
"@input-group-addon-border-color": "@input-border",
"@cursor-disabled": "not-allowed",
"@dropdown-bg": "#fff",
"@dropdown-border": "rgba(0, 0, 0, .15)",
"@dropdown-fallback-border": "#ccc",
"@dropdown-divider-bg": "#e5e5e5",
"@dropdown-link-color": "@gray-dark",
"@dropdown-link-hover-color": "darken(@gray-dark, 5%)",
"@dropdown-link-hover-bg": "#f5f5f5",
"@dropdown-link-active-color": "@component-active-color",
"@dropdown-link-active-bg": "@component-active-bg",
"@dropdown-link-disabled-color": "@gray-light",
"@dropdown-header-color": "@gray-light",
"@dropdown-caret-color": "#000",
"@screen-xs": "480px",
"@screen-xs-min": "@screen-xs",
"@screen-phone": "@screen-xs-min",
"@screen-sm": "768px",
"@screen-sm-min": "@screen-sm",
"@screen-tablet": "@screen-sm-min",
"@screen-md": "992px",
"@screen-md-min": "@screen-md",
"@screen-desktop": "@screen-md-min",
"@screen-lg": "1200px",
"@screen-lg-min": "@screen-lg",
"@screen-lg-desktop": "@screen-lg-min",
"@screen-xs-max": "(@screen-sm-min - 1)",
"@screen-sm-max": "(@screen-md-min - 1)",
"@screen-md-max": "(@screen-lg-min - 1)",
"@grid-columns": "12",
"@grid-gutter-width": "30px",
"@grid-float-breakpoint": "@screen-sm-min",
"@grid-float-breakpoint-max": "(@grid-float-breakpoint - 1)",
"@container-tablet": "(720px + @grid-gutter-width)",
"@container-sm": "@container-tablet",
"@container-desktop": "(940px + @grid-gutter-width)",
"@container-md": "@container-desktop",
"@container-large-desktop": "(1140px + @grid-gutter-width)",
"@container-lg": "@container-large-desktop",
"@navbar-height": "50px",
"@navbar-margin-bottom": "@line-height-computed",
"@navbar-border-radius": "@border-radius-base",
"@navbar-padding-horizontal": "floor((@grid-gutter-width / 2))",
"@navbar-padding-vertical": "((@navbar-height - @line-height-computed) / 2)",
"@navbar-collapse-max-height": "340px",
"@navbar-default-color": "#777",
"@navbar-default-bg": "#f8f8f8",
"@navbar-default-border": "darken(@navbar-default-bg, 6.5%)",
"@navbar-default-link-color": "#777",
"@navbar-default-link-hover-color": "#333",
"@navbar-default-link-hover-bg": "transparent",
"@navbar-default-link-active-color": "#555",
"@navbar-default-link-active-bg": "darken(@navbar-default-bg, 6.5%)",
"@navbar-default-link-disabled-color": "#ccc",
"@navbar-default-link-disabled-bg": "transparent",
"@navbar-default-brand-color": "@navbar-default-link-color",
"@navbar-default-brand-hover-color": "darken(@navbar-default-brand-color, 10%)",
"@navbar-default-brand-hover-bg": "transparent",
"@navbar-default-toggle-hover-bg": "#ddd",
"@navbar-default-toggle-icon-bar-bg": "#888",
"@navbar-default-toggle-border-color": "#ddd",
"@navbar-inverse-color": "lighten(@gray-light, 15%)",
"@navbar-inverse-bg": "#222",
"@navbar-inverse-border": "darken(@navbar-inverse-bg, 10%)",
"@navbar-inverse-link-color": "lighten(@gray-light, 15%)",
"@navbar-inverse-link-hover-color": "#fff",
"@navbar-inverse-link-hover-bg": "transparent",
"@navbar-inverse-link-active-color": "@navbar-inverse-link-hover-color",
"@navbar-inverse-link-active-bg": "darken(@navbar-inverse-bg, 10%)",
"@navbar-inverse-link-disabled-color": "#444",
"@navbar-inverse-link-disabled-bg": "transparent",
"@navbar-inverse-brand-color": "@navbar-inverse-link-color",
"@navbar-inverse-brand-hover-color": "#fff",
"@navbar-inverse-brand-hover-bg": "transparent",
"@navbar-inverse-toggle-hover-bg": "#333",
"@navbar-inverse-toggle-icon-bar-bg": "#fff",
"@navbar-inverse-toggle-border-color": "#333",
"@nav-link-padding": "10px 15px",
"@nav-link-hover-bg": "@gray-lighter",
"@nav-disabled-link-color": "@gray-light",
"@nav-disabled-link-hover-color": "@gray-light",
"@nav-tabs-border-color": "#ddd",
"@nav-tabs-link-hover-border-color": "@gray-lighter",
"@nav-tabs-active-link-hover-bg": "@body-bg",
"@nav-tabs-active-link-hover-color": "@gray",
"@nav-tabs-active-link-hover-border-color": "#ddd",
"@nav-tabs-justified-link-border-color": "#ddd",
"@nav-tabs-justified-active-link-border-color": "@body-bg",
"@nav-pills-border-radius": "@border-radius-base",
"@nav-pills-active-link-hover-bg": "@component-active-bg",
"@nav-pills-active-link-hover-color": "@component-active-color",
"@pagination-color": "@link-color",
"@pagination-bg": "#fff",
"@pagination-border": "#ddd",
"@pagination-hover-color": "@link-hover-color",
"@pagination-hover-bg": "@gray-lighter",
"@pagination-hover-border": "#ddd",
"@pagination-active-color": "#fff",
"@pagination-active-bg": "@brand-primary",
"@pagination-active-border": "@brand-primary",
"@pagination-disabled-color": "@gray-light",
"@pagination-disabled-bg": "#fff",
"@pagination-disabled-border": "#ddd",
"@pager-bg": "@pagination-bg",
"@pager-border": "@pagination-border",
"@pager-border-radius": "15px",
"@pager-hover-bg": "@pagination-hover-bg",
"@pager-active-bg": "@pagination-active-bg",
"@pager-active-color": "@pagination-active-color",
"@pager-disabled-color": "@pagination-disabled-color",
"@jumbotron-padding": "30px",
"@jumbotron-color": "inherit",
"@jumbotron-bg": "@gray-lighter",
"@jumbotron-heading-color": "inherit",
"@jumbotron-font-size": "ceil((@font-size-base * 1.5))",
"@jumbotron-heading-font-size": "ceil((@font-size-base * 4.5))",
"@state-success-text": "#3c763d",
"@state-success-bg": "#dff0d8",
"@state-success-border": "darken(spin(@state-success-bg, -10), 5%)",
"@state-info-text": "#31708f",
"@state-info-bg": "#d9edf7",
"@state-info-border": "darken(spin(@state-info-bg, -10), 7%)",
"@state-warning-text": "#8a6d3b",
"@state-warning-bg": "#fcf8e3",
"@state-warning-border": "darken(spin(@state-warning-bg, -10), 5%)",
"@state-danger-text": "#a94442",
"@state-danger-bg": "#f2dede",
"@state-danger-border": "darken(spin(@state-danger-bg, -10), 5%)",
"@tooltip-max-width": "200px",
"@tooltip-color": "#fff",
"@tooltip-bg": "#000",
"@tooltip-opacity": ".9",
"@tooltip-arrow-width": "5px",
"@tooltip-arrow-color": "@tooltip-bg",
"@popover-bg": "#fff",
"@popover-max-width": "276px",
"@popover-border-color": "rgba(0, 0, 0, .2)",
"@popover-fallback-border-color": "#ccc",
"@popover-title-bg": "darken(@popover-bg, 3%)",
"@popover-arrow-width": "10px",
"@popover-arrow-color": "@popover-bg",
"@popover-arrow-outer-width": "(@popover-arrow-width + 1)",
"@popover-arrow-outer-color": "fadein(@popover-border-color, 5%)",
"@popover-arrow-outer-fallback-color": "darken(@popover-fallback-border-color, 20%)",
"@label-default-bg": "@gray-light",
"@label-primary-bg": "@brand-primary",
"@label-success-bg": "@brand-success",
"@label-info-bg": "@brand-info",
"@label-warning-bg": "@brand-warning",
"@label-danger-bg": "@brand-danger",
"@label-color": "#fff",
"@label-link-hover-color": "#fff",
"@modal-inner-padding": "15px",
"@modal-title-padding": "15px",
"@modal-title-line-height": "@line-height-base",
"@modal-content-bg": "#fff",
"@modal-content-border-color": "rgba(0, 0, 0, .2)",
"@modal-content-fallback-border-color": "#999",
"@modal-backdrop-bg": "#000",
"@modal-backdrop-opacity": ".5",
"@modal-header-border-color": "#e5e5e5",
"@modal-footer-border-color": "@modal-header-border-color",
"@modal-lg": "900px",
"@modal-md": "600px",
"@modal-sm": "300px",
"@alert-padding": "15px",
"@alert-border-radius": "@border-radius-base",
"@alert-link-font-weight": "bold",
"@alert-success-bg": "@state-success-bg",
"@alert-success-text": "@state-success-text",
"@alert-success-border": "@state-success-border",
"@alert-info-bg": "@state-info-bg",
"@alert-info-text": "@state-info-text",
"@alert-info-border": "@state-info-border",
"@alert-warning-bg": "@state-warning-bg",
"@alert-warning-text": "@state-warning-text",
"@alert-warning-border": "@state-warning-border",
"@alert-danger-bg": "@state-danger-bg",
"@alert-danger-text": "@state-danger-text",
"@alert-danger-border": "@state-danger-border",
"@progress-bg": "#f5f5f5",
"@progress-bar-color": "#fff",
"@progress-border-radius": "@border-radius-base",
"@progress-bar-bg": "@brand-primary",
"@progress-bar-success-bg": "@brand-success",
"@progress-bar-warning-bg": "@brand-warning",
"@progress-bar-danger-bg": "@brand-danger",
"@progress-bar-info-bg": "@brand-info",
"@list-group-bg": "#fff",
"@list-group-border": "#ddd",
"@list-group-border-radius": "@border-radius-base",
"@list-group-hover-bg": "#f5f5f5",
"@list-group-active-color": "@component-active-color",
"@list-group-active-bg": "@component-active-bg",
"@list-group-active-border": "@list-group-active-bg",
"@list-group-active-text-color": "lighten(@list-group-active-bg, 40%)",
"@list-group-disabled-color": "@gray-light",
"@list-group-disabled-bg": "@gray-lighter",
"@list-group-disabled-text-color": "@list-group-disabled-color",
"@list-group-link-color": "#555",
"@list-group-link-hover-color": "@list-group-link-color",
"@list-group-link-heading-color": "#333",
"@panel-bg": "#fff",
"@panel-body-padding": "15px",
"@panel-heading-padding": "10px 15px",
"@panel-footer-padding": "@panel-heading-padding",
"@panel-border-radius": "@border-radius-base",
"@panel-inner-border": "#ddd",
"@panel-footer-bg": "#f5f5f5",
"@panel-default-text": "@gray-dark",
"@panel-default-border": "#ddd",
"@panel-default-heading-bg": "#f5f5f5",
"@panel-primary-text": "#fff",
"@panel-primary-border": "@brand-primary",
"@panel-primary-heading-bg": "@brand-primary",
"@panel-success-text": "@state-success-text",
"@panel-success-border": "@state-success-border",
"@panel-success-heading-bg": "@state-success-bg",
"@panel-info-text": "@state-info-text",
"@panel-info-border": "@state-info-border",
"@panel-info-heading-bg": "@state-info-bg",
"@panel-warning-text": "@state-warning-text",
"@panel-warning-border": "@state-warning-border",
"@panel-warning-heading-bg": "@state-warning-bg",
"@panel-danger-text": "@state-danger-text",
"@panel-danger-border": "@state-danger-border",
"@panel-danger-heading-bg": "@state-danger-bg",
"@thumbnail-padding": "4px",
"@thumbnail-bg": "@body-bg",
"@thumbnail-border": "#ddd",
"@thumbnail-border-radius": "@border-radius-base",
"@thumbnail-caption-color": "@text-color",
"@thumbnail-caption-padding": "9px",
"@well-bg": "#f5f5f5",
"@well-border": "darken(@well-bg, 7%)",
"@badge-color": "#fff",
"@badge-link-hover-color": "#fff",
"@badge-bg": "@gray-light",
"@badge-active-color": "@link-color",
"@badge-active-bg": "#fff",
"@badge-font-weight": "bold",
"@badge-line-height": "1",
"@badge-border-radius": "10px",
"@breadcrumb-padding-vertical": "8px",
"@breadcrumb-padding-horizontal": "15px",
"@breadcrumb-bg": "#f5f5f5",
"@breadcrumb-color": "#ccc",
"@breadcrumb-active-color": "@gray-light",
"@breadcrumb-separator": "\"/\"",
"@carousel-text-shadow": "0 1px 2px rgba(0, 0, 0, .6)",
"@carousel-control-color": "#fff",
"@carousel-control-width": "15%",
"@carousel-control-opacity": ".5",
"@carousel-control-font-size": "20px",
"@carousel-indicator-active-bg": "#fff",
"@carousel-indicator-border-color": "#fff",
"@carousel-caption-color": "#fff",
"@close-font-weight": "bold",
"@close-color": "#000",
"@close-text-shadow": "0 1px 0 #fff",
"@code-color": "#c7254e",
"@code-bg": "#f9f2f4",
"@kbd-color": "#fff",
"@kbd-bg": "#333",
"@pre-bg": "#f5f5f5",
"@pre-color": "@gray-dark",
"@pre-border-color": "#ccc",
"@pre-scrollable-max-height": "340px",
"@component-offset-horizontal": "180px",
"@text-muted": "@gray-light",
"@abbr-border-color": "@gray-light",
"@headings-small-color": "@gray-light",
"@blockquote-small-color": "@gray-light",
"@blockquote-font-size": "(@font-size-base * 1.25)",
"@blockquote-border-color": "@gray-lighter",
"@page-header-border-color": "@gray-lighter",
"@dl-horizontal-offset": "@component-offset-horizontal",
"@dl-horizontal-breakpoint": "@grid-float-breakpoint",
"@hr-border": "@gray-lighter"
},
"css": [
"dropdowns.less"
],
"js": [
"button.js",
"dropdown.js"
]
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More