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

@@ -1,6 +1,6 @@
# kkFileView # kkFileView
文档在线预览项目解决方案项目使用流行的spring boot搭建易上手和部署万能的文件预览开源项目基本支持主流文档格式预览 文档在线预览项目解决方案项目使用流行的spring boot搭建易上手和部署万能的文件预览开源项目基本支持主流文档格式预览
1. 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla ,pages Office 办公文档 1. 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla ,pages ,pptm Office 办公文档
2. 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档 2. 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档
3. 支持 odt, ods, ots, odp, otp, six, ott, fodt, fods 等OpenOfficeLibreOffice 办公文档 3. 支持 odt, ods, ots, odp, otp, six, ott, fodt, fods 等OpenOfficeLibreOffice 办公文档
4. 支持 vsd, vsdx Visio 流程图文件 4. 支持 vsd, vsdx Visio 流程图文件
@@ -9,18 +9,18 @@
7. 支持 pdf ,ofd, rtf 等文档 7. 支持 pdf ,ofd, rtf 等文档
8. 支持 xmind 软件模型文件 8. 支持 xmind 软件模型文件
9. 支持 bpmn 工作流文件 9. 支持 bpmn 工作流文件
10. 支持 eml 邮件文件 10. 支持 eml, msg 邮件文件
11. 支持 epub 图书文档 11. 支持 epub 图书文档
12. 支持 obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim 3D 模型文件 12. 支持 obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim 3D 模型文件
13. 支持 dwg, dxf, dwf, iges , igs, dwt, dng, ifc, dwfx, stl, cf2, plt CAD 模型文件 13. 支持 dwg, dxf, dwf, iges , igs, dwt, dng, ifc, dwfx, stl, cf2, plt CAD 模型文件
14. 支持 txt, xml(渲染), xbrl(渲染), md(渲染), java, php, py, js, css 等所有纯文本 14. 支持 txt, xml(渲染), xbrl(渲染), md(渲染), java, php, py, js, css 等所有纯文本
15. 支持 zip, rar, jar, tar, gzip, 7z 等压缩包 15. 支持 zip, rar, jar, tar, gzip, 7z 等压缩包
16. 支持 jpg, jpeg, png, gif, bmp, ico, jfif, webp 等图片预览翻转缩放镜像 16. 支持 jpg, jpeg, png, gif, bmp, ico, jfif, webp ,heic ,heif等图片预览翻转缩放镜像
17. 支持 tif, tiff 图信息模型文件 17. 支持 tif, tiff 图信息模型文件
18. 支持 tga 图像格式文件 18. 支持 tga 图像格式文件
19. 支持 svg 矢量图像格式文件 19. 支持 svg 矢量图像格式文件
20. 支持 mp3,wav,mp4,flv 等音视频格式文件 20. 支持 mp3,wav,mp4,flv 等音视频格式文件
21. 支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts,swf 等视频格式转码预览 21. 支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts 等视频格式转码预览
22. 支持 dcm 等医疗数位影像预览 22. 支持 dcm 等医疗数位影像预览
23. 支持 drawio 绘图预览 23. 支持 drawio 绘图预览
@@ -149,6 +149,47 @@ pdf预览模式预览效果如下
### 历史更新记录 ### 历史更新记录
#### > 2026年01月20日v5.0 版本发布
#### 优化内容
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能
2. 图片解析优化 - 改进图片处理机制
3. tif 解析优化 - 增强TIF格式支持
4. svg 解析优化 - 优化SVG矢量图渲染
5. json 解析优化 - 改进JSON文件处理
6. ftp多客户端接入优化 - 提升FTP服务兼容性
7. 首页目录访问优化 - 采用post服务端分页机制
8. marked 解析优化 - 改进Markdown渲染
#### 新增功能
1. msg邮件解析 - 新增msg格式邮件文件预览支持
2. heic图片解析 - 新增HEIC格式图片预览支持
3. 跨域方法 - 新增跨域处理机制
4. 高亮方法 - 新增文本高亮功能
5. 页码方法 - 新增文档页码控制
6. AES加密方法 - 新增AES加密支持
7. Basic鉴权方法 - 新增Basic认证机制
8. 秘钥方法 - 新增密钥管理功能
9. 防重复转换 - 新增重复文件转换防护
10. 异步等待 - 新增异步处理机制
11. 上传限制 - 新增不支持文件上传限制
12. cadviewer转换方法 - 新增CAD查看器转换功能
#### 修复问题
1. 压缩包路径问题 - 修复压缩包内部路径处理
2. 安全问题 - 修复安全漏洞
3. 图片水印不全问题 - 修复水印显示不完整
4. SSL自签证书接入问题 - 修复自签名证书兼容性
#### 更新内容
1. JDK版本要求 - 强制要求JDK 21及以上版本
2. pdf前端解析更新 - 升级PDF前端渲染组件
3. odf前端解析更新 - 升级ODF文档前端渲染
4. 3D模型前端解析更新 - 升级3D模型查看器
5. pdf后端异步转换优化 - 实现多线程异步转换
6. tif后端异步转换优化 - 实现多线程异步转换
7. 视频后端异步转换优化 - 实现多线程异步转换
8. CAD后端异步转换优化 - 实现多线程异步转换
#### > 2025年01月16日v4.4.0 版本发布 #### > 2025年01月16日v4.4.0 版本发布
### 新增功能 ### 新增功能

174
README.md
View File

@@ -4,7 +4,7 @@
Document online preview project solution, built using the popular Spring Boot framework for easy setup and deployment. This versatile open source project provides basic support for a wide range of document formats, including: Document online preview project solution, built using the popular Spring Boot framework for easy setup and deployment. This versatile open source project provides basic support for a wide range of document formats, including:
1. Supports Office documents such as `doc`, `docx`, `xls`, `xlsx`, `xlsm`, `ppt`, `pptx`, `csv`, `tsv`, , `dotm`, `xlt`, `xltm`, `dot`, `xlam`, `dotx`, `xla,` ,`pages` etc. 1. Supports Office documents such as `doc`, `docx`, `xls`, `xlsx`, `xlsm`, `ppt`, `pptx`, `csv`, `tsv`, , `dotm`, `xlt`, `xltm`, `dot`, `xlam`, `dotx`, `xla,` ,`pages` ,`pptm` etc.
2. Supports domestic WPS Office documents such as `wps`, `dps`, `et` , `ett`, ` wpt`. 2. Supports domestic WPS Office documents such as `wps`, `dps`, `et` , `ett`, ` wpt`.
3. Supports OpenOffice, LibreOffice office documents such as `odt`, `ods`, `ots`, `odp`, `otp`, `six`, `ott`, `fodt` and `fods`. 3. Supports OpenOffice, LibreOffice office documents such as `odt`, `ods`, `ots`, `odp`, `otp`, `six`, `ott`, `fodt` and `fods`.
4. Supports Visio flowchart files such as `vsd`, `vsdx`. 4. Supports Visio flowchart files such as `vsd`, `vsdx`.
@@ -13,13 +13,13 @@ Document online preview project solution, built using the popular Spring Boot fr
7. Supports document formats like `pdf`, `ofd`, and `rtf`. 7. Supports document formats like `pdf`, `ofd`, and `rtf`.
8. Supports software model files like `xmind`. 8. Supports software model files like `xmind`.
9. Support for `bpmn` workflow files. 9. Support for `bpmn` workflow files.
10. Support for `eml` mail files 10. Support for `eml` , `msg` mail files
11. Support for `epub` book documents 11. Support for `epub` book documents
12. Supports 3D model files like `obj`, `3ds`, `stl`, `ply`, `gltf`, `glb`, `off`, `3dm`, `fbx`, `dae`, `wrl`, `3mf`, `ifc`, `brep`, `step`, `iges`, `fcstd`, `bim`, etc. 12. Supports 3D model files like `obj`, `3ds`, `stl`, `ply`, `gltf`, `glb`, `off`, `3dm`, `fbx`, `dae`, `wrl`, `3mf`, `ifc`, `brep`, `step`, `iges`, `fcstd`, `bim`, etc.
13. Supports CAD model files such as `dwg`, `dxf`, `dwf` `iges` ,` igs`, `dwt` , `dng` , `ifc` , `dwfx` , `stl` , `cf2` , `plt`, etc. 13. Supports CAD model files such as `dwg`, `dxf`, `dwf` `iges` ,` igs`, `dwt` , `dng` , `ifc` , `dwfx` , `stl` , `cf2` , `plt`, etc.
14. Supports all plain text files such as `txt`, `xml` (rendering), `md` (rendering), `java`, `php`, `py`, `js`, `css`, etc. 14. Supports all plain text files such as `txt`, `xml` (rendering), `md` (rendering), `java`, `php`, `py`, `js`, `css`, etc.
15. Supports compressed packages such as `zip`, `rar`, `jar`, `tar`, `gzip`, `7z`, etc. 15. Supports compressed packages such as `zip`, `rar`, `jar`, `tar`, `gzip`, `7z`, etc.
16. Supports image previewing (flip, zoom, mirror) of `jpg`, `jpeg`, `png`, `gif`, `bmp`, `ico`, `jfif`, `webp`, etc. 16. Supports image previewing (flip, zoom, mirror) of `jpg`, `jpeg`, `png`, `gif`, `bmp`, `ico`, `jfif`, `webp`, `heic`, ,`heif` etc.
17. Supports image information model files such as `tif` and `tiff`. 17. Supports image information model files such as `tif` and `tiff`.
18. Supports image format files such as `tga`. 18. Supports image format files such as `tga`.
19. Supports vector image format files such as `svg`. 19. Supports vector image format files such as `svg`.
@@ -63,6 +63,174 @@ URL[https://file.kkview.cn](https://file.kkview.cn)
2. second stepRun the main method of `/server/src/main/java/cn/keking/ServerMain.java`. After starting,visit `http://localhost:8012/`. 2. second stepRun the main method of `/server/src/main/java/cn/keking/ServerMain.java`. After starting,visit `http://localhost:8012/`.
## Change History
### Version 5.0 (January 20, 2026)
#### Optimizations
1. Enhanced xlsx front-end parsing - Improved Excel file front-end rendering performance
2. Optimized image parsing - Enhanced image processing mechanism
3. Improved tif parsing - Enhanced TIF format support
4. Enhanced svg parsing - Optimized SVG vector image rendering
5. Improved json parsing - Enhanced JSON file processing
6. Optimized ftp multi-client access - Improved FTP service compatibility
7. Enhanced home page directory access - Implemented post server-side pagination mechanism
8. Improved marked parsing - Enhanced Markdown rendering
#### New Features
1. msg email parsing - Added support for msg format email file preview
2. heic image parsing - Added support for HEIC format image preview
3. Cross-domain methods - Added cross-domain processing mechanism
4. Highlighting methods - Added text highlighting functionality
5. Pagination methods - Added document page control
6. AES encryption methods - Added AES encryption support
7. Basic authentication methods - Added Basic authentication mechanism
8. Key management methods - Added key management functionality
9. Anti-duplicate conversion - Added duplicate file conversion protection
10. Async waiting - Added asynchronous processing mechanism
11. Upload restrictions - Added restrictions for unsupported file uploads
12. cadviewer conversion methods - Added CAD viewer conversion functionality
#### Fixed Issues
1. Compressed file path issues - Fixed internal path handling in compressed files
2. Security issues - Fixed security vulnerabilities
3. Incomplete image watermark issues - Fixed incomplete watermark display
4. SSL self-signed certificate access issues - Fixed compatibility with self-signed certificates
#### Updates
1. JDK version requirement - Mandatory requirement for JDK 21 or higher
2. pdf front-end parsing update - Upgraded PDF front-end rendering component
3. odf front-end parsing update - Upgraded ODF document front-end rendering
4. 3D model front-end parsing update - Upgraded 3D model viewer
5. pdf backend async conversion optimization - Implemented multi-threaded asynchronous conversion
6. tif backend async conversion optimization - Implemented multi-threaded asynchronous conversion
7. Video backend async conversion optimization - Implemented multi-threaded asynchronous conversion
8. CAD backend async conversion optimization - Implemented multi-threaded asynchronous conversion
### Version 4.4.0 (January 16, 2025)
#### New Features
1. xlsx printing support
2. Added GZIP compression enablement in configuration
3. CAD format now supports conversion to SVG and TIF formats, added timeout termination and thread management
4. Added captcha verification for file deletion
5. Added xbrl format preview support
6. PDF preview added control over signatures, drawings, illustration control, search positioning pagination, and display content definition
7. Added CSV format front-end parsing support
8. Added Docker image support for ARM64
9. Added Office preview conversion timeout property setting
10. Added preview file host blacklist mechanism
#### Optimizations
1. Optimized OFD mobile preview page adaptability
2. Updated xlsx front-end parsing component to accelerate parsing speed
3. Upgraded CAD component
4. Office function adjustments, supporting comments, conversion page limit, watermark generation, etc.
5. Upgraded markdown component
6. Upgraded dcm parsing component
7. Upgraded PDF.JS parsing component
8. Changed video player plugin to ckplayer
9. Smarter tif parsing, supporting modified image formats
10. Improved character encoding detection accuracy for large and small text files, handling concurrency vulnerabilities
11. Refactored file download code, added general file server authentication access design
12. Updated bootstrap component and streamlined unnecessary files
13. Updated epub version, optimized epub display effect
14. Fixed issue where scheduled cache cleanup only deleted disk cache files for multimedia file types
15. Auto-detection of installed Office components, added default paths for LibreOffice 7.5 & 7.6 versions
16. Changed drawio default to preview mode
17. Added PDF thread management, timeout management, memory cache management, updated PDF parsing component version
18. Optimized Dockerfile for true cross-platform image building
#### Fixes
1. Fixed forceUpdatedCache property setting issue where local cache files weren't updated
2. Fixed PDF decryption error after successful encrypted file conversion
3. Fixed BPMN cross-domain support issue
4. Fixed special character error in compressed package secondary reverse proxy
5. Fixed video cross-domain configuration causing video preview failure
6. Fixed TXT text pagination secondary loading issue
7. Fixed Drawio missing Base64 component issue
8. Fixed Markdown escaping issue
9. Fixed EPUB cross-domain error
10. Fixed URL special character issues
11. Fixed compressed package traversal vulnerability
12. Fixed compressed file path errors, image collection path errors, watermark issues, etc.
13. Fixed front-end parsing XLSX containing EMF format file errors
### Version 4.3.0 (July 5, 2023)
#### New Features
1. Added DCM medical digital imaging preview
2. Added drawio drawing preview
3. Added command to regenerate with cache enabled: &forceUpdatedCache=true
4. Added dwg CAD file preview
5. Added PDF file password support
6. Added DPI customization for PDF file image generation
7. Added configuration to delete converted OFFICE, CAD, TIFF, compressed package source files (enabled by default to save disk space)
8. Added front-end xlsx parsing method
9. Added support for pages, eps, iges, igs, dwt, dng, ifc, dwfx, stl, cf2, plt and other formats
#### Optimizations
1. Modified generated PDF file names to include file extensions to prevent duplicate names
2. Adjusted SQL file preview method
3. Optimized OFD preview compatibility
4. Beautified TXT text pagination box display
5. Upgraded Linux/Docker built-in office to LibreOffice-7.5.3
6. Upgraded Windows built-in office to LibreOffice-7.5.3 Portable
7. Other functional optimizations
#### Fixes
1. Fixed compressed package path errors in reverse proxy scenarios
2. Fixed .click error when image preview URLs contain &
3. Fixed known OFD preview issues
4. Fixed page error when clicking on file directories (tree nodes) in compressed package preview
5. Other known issue fixes
### Version 4.2.1 (April 18, 2023)
#### Change Log
1. Fixed null pointer bug in dwg file preview
### Version 4.2.0 (April 13, 2023)
#### New Features
1. Added SVG format file preview support
2. Added encrypted Office file preview support
3. Added encrypted zip, rar, and other compressed package file preview support
4. Added xmind software model file preview support
5. Added BPMN workflow model file preview support
6. Added eml email file preview support
7. Added EPUB e-book file preview support
8. Added office document format support: dotm, ett, xlt, xltm, wpt, dot, xlam, xla, dotx, etc.
9. Added 3D model file support: obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim, etc.
10. Added configurable high-risk file upload restrictions (e.g., exe files)
11. Added configurable site filing information
12. Added password requirement for demo site file deletion
#### Optimizations
1. Added caching for text document preview
2. Beautified 404, 500 error pages
3. Optimized invoice and other OFD file preview seal rendering compatibility
4. Removed office-plugin module, using new jodconverter component
5. Optimized Excel file preview effect
6. Optimized CAD file preview effect
7. Updated xstream, junrar, pdfbox, and other dependency versions
8. Updated TIF to PDF conversion plugin, added conversion cache
9. Optimized demo page UI deployment
10. Compressed package file preview supports directories
#### Fixes
1. Fixed XSS issues in some interfaces
2. Fixed console printed demo address not following content-path configuration
3. Fixed OFD file preview cross-domain issues
4. Fixed internal self-signed certificate HTTPS URL file download issues
5. Fixed special character file deletion issues
6. Fixed OOM caused by unreclaimed memory in PDF to image conversion
7. Fixed garbled preview for xlsx 7.4+ version files
8. Fixed TrustHostFilter not intercepting cross-domain interfaces (security issue - upgrade required if using TrustHost)
9. Fixed compressed package file preview filename garbled issue on Linux systems
10. Fixed OFD file preview only displaying 10 pages
### Changelog ### Changelog
> December 14, 2022, version 4.1.0 released: > December 14, 2022, version 4.1.0 released:

90
pom.xml
View File

@@ -6,54 +6,75 @@
<groupId>cn.keking</groupId> <groupId>cn.keking</groupId>
<artifactId>kkFileView-parent</artifactId> <artifactId>kkFileView-parent</artifactId>
<version>4.4.0</version> <version>5.0</version>
<properties> <properties>
<!-- ========== Java 和编译配置 ========== -->
<java.version>21</java.version> <java.version>21</java.version>
<jodconverter.version>4.4.6</jodconverter.version>
<spring.boot.version>3.5.6</spring.boot.version>
<poi.version>5.2.2</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<xstream.version>1.4.20</xstream.version>
<junrar.version>7.5.5</junrar.version>
<redisson.version>3.22.0</redisson.version>
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
<jchardet.version>1.0</jchardet.version>
<antlr.version>2.7.7</antlr.version>
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
<rocksdb.version>5.17.2</rocksdb.version>
<pdfbox.version>3.0.2</pdfbox.version>
<jai-imageio.version>1.4.0</jai-imageio.version>
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
<galimatias.version>0.2.1</galimatias.version>
<bytedeco.version>1.5.2</bytedeco.version>
<opencv.version>4.1.2-1.5.2</opencv.version>
<openblas.version>0.3.6-1.5.1</openblas.version>
<ffmpeg.version>4.2.1-1.5.2</ffmpeg.version>
<itextpdf.version>5.5.13.3</itextpdf.version>
<httpclient.version>3.1</httpclient.version>
<aspose-cad.version>23.9</aspose-cad.version>
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
<juniversalchardet.version>1.0.3</juniversalchardet.version>
<httpcomponents.version>4.5.14</httpcomponents.version>
<commons-cli.version>1.5.0</commons-cli.version>
<commons-net.version>3.9.0</commons-net.version>
<commons-lang3.version>3.13.0</commons-lang3.version>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.release>${java.version}</maven.compiler.release> <maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- ========== Spring 框架 ========== -->
<spring.boot.version>3.5.6</spring.boot.version>
<!-- ========== 文档转换和Office处理 ========== -->
<jodconverter.version>4.4.11</jodconverter.version>
<poi.version>5.2.5</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<aspose-cad.version>25.10</aspose-cad.version>
<!-- ========== PDF 处理 ========== -->
<pdfbox.version>3.0.6</pdfbox.version>
<!-- ========== 图像处理 ========== -->
<jai-imageio.version>1.4.0</jai-imageio.version>
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
<commons-imaging.version>1.0.0-alpha6</commons-imaging.version>
<!-- ========== 视频处理 (JavaCV) ========== -->
<bytedeco.version>1.5.12</bytedeco.version>
<opencv.version>4.11.0-1.5.12</opencv.version>
<openblas.version>0.3.30-1.5.12</openblas.version>
<ffmpeg.version>7.1.1-1.5.12</ffmpeg.version>
<!-- ========== 压缩文件处理 ========== -->
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
<junrar.version>7.5.5</junrar.version>
<!-- ========== 缓存和存储 ========== -->
<redisson.version>4.0.0</redisson.version>
<rocksdb.version>5.17.2</rocksdb.version>
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
<!-- ========== 网络通信 ========== -->
<httpcomponents.version>4.5.16</httpcomponents.version>
<commons-net.version>3.12.0</commons-net.version>
<!-- ========== Apache Commons 工具库 ========== -->
<commons-lang3.version>3.20.0</commons-lang3.version>
<commons-cli.version>1.11.0</commons-cli.version>
<!-- ========== 编码和字符处理 ========== -->
<juniversalchardet.version>1.0.3</juniversalchardet.version>
<jchardet.version>1.0</jchardet.version>
<!-- ========== 安全相关 ========== -->
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
<!-- ========== 其他工具库 ========== -->
<xstream.version>1.4.21</xstream.version>
<antlr.version>2.7.7</antlr.version>
<galimatias.version>0.2.1</galimatias.version>
</properties> </properties>
<modules> <modules>
<module>server</module> <module>server</module>
</modules> </modules>
<!-- ========== 项目信息 ========== -->
<name>kkFileView-parent</name> <name>kkFileView-parent</name>
<description>专注文件在线预览服务</description> <description>专注文件在线预览服务</description>
<url>https://github.com/kekingcn/kkFileView</url> <url>https://github.com/kekingcn/kkFileView</url>
@@ -89,5 +110,4 @@
<system>github</system> <system>github</system>
<url>https://github.com/kekingcn/kkFileView/issues</url> <url>https://github.com/kekingcn/kkFileView/issues</url>
</issueManagement> </issueManagement>
</project> </project>

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<artifactId>kkFileView-parent</artifactId> <artifactId>kkFileView-parent</artifactId>
<groupId>cn.keking</groupId> <groupId>cn.keking</groupId>
<version>4.4.0</version> <version>5.0</version>
</parent> </parent>
<artifactId>kkFileView</artifactId> <artifactId>kkFileView</artifactId>
@@ -24,28 +24,26 @@
</dependencyManagement> </dependencyManagement>
<repositories> <repositories>
<!-- Aspose 仓库且只启用 releases -->
<repository> <repository>
<id>aspose-maven-repository</id> <id>aspose-maven-repository</id>
<url>https://repository.aspose.com/repo</url> <url>https://repository.aspose.com/repo</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots> <snapshots>
<enabled>false</enabled> <enabled>false</enabled>
</snapshots> </snapshots>
</repository> </repository>
</repositories> </repositories>
<dependencies> <dependencies>
<dependency> <!-- ========== Spring Boot 框架依赖 ========== -->
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>${jodconverter.version}</version>
</dependency>
<!-- web start -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId> <artifactId>spring-boot-starter-freemarker</artifactId>
@@ -54,9 +52,15 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- web end -->
<!-- poi start --> <!-- ========== 文档格式转换 ========== -->
<dependency>
<groupId>org.jodconverter</groupId>
<artifactId>jodconverter-local</artifactId>
<version>${jodconverter.version}</version>
</dependency>
<!-- ========== Office文档处理 (POI相关) ========== -->
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId> <artifactId>poi</artifactId>
@@ -93,85 +97,13 @@
<artifactId>fr.opensagres.xdocreport.document</artifactId> <artifactId>fr.opensagres.xdocreport.document</artifactId>
<version>${xdocreport.version}</version> <version>${xdocreport.version}</version>
</dependency> </dependency>
<!-- poi start -->
<dependency> <dependency>
<groupId>org.apache.httpcomponents.client5</groupId> <groupId>com.aspose</groupId>
<artifactId>httpclient5</artifactId> <artifactId>aspose-cad</artifactId>
<version>${aspose-cad.version}</version>
</dependency> </dependency>
<!-- rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; --> <!-- ========== PDF处理 ========== -->
<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>
<dependency> <dependency>
<groupId>org.apache.pdfbox</groupId> <groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId> <artifactId>pdfbox</artifactId>
@@ -188,6 +120,25 @@
<artifactId>pdfbox-tools</artifactId> <artifactId>pdfbox-tools</artifactId>
<version>${pdfbox.version}</version> <version>${pdfbox.version}</version>
</dependency> </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> <dependency>
<groupId>com.github.jai-imageio</groupId> <groupId>com.github.jai-imageio</groupId>
<artifactId>jai-imageio-jpeg2000</artifactId> <artifactId>jai-imageio-jpeg2000</artifactId>
@@ -204,78 +155,11 @@
<version>${jbig2-imageio.version}</version> <version>${jbig2-imageio.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.aspose</groupId> <groupId>org.apache.commons</groupId>
<artifactId>aspose-cad</artifactId> <artifactId>commons-imaging</artifactId>
<version>${aspose-cad.version}</version> <version>${commons-imaging.version}</version>
</dependency> </dependency>
<!-- 密钥算法 --> <!-- JAI 系统依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bcprov-jdk15on.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>
<dependency> <dependency>
<groupId>javax.media</groupId> <groupId>javax.media</groupId>
<artifactId>jai_core</artifactId> <artifactId>jai_core</artifactId>
@@ -291,25 +175,134 @@
<systemPath>${pom.basedir}/lib/jai_codec-1.1.3.jar</systemPath> <systemPath>${pom.basedir}/lib/jai_codec-1.1.3.jar</systemPath>
</dependency> </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> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </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> </dependencies>
<build> <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 org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.TimeUnit; import java.util.concurrent.*;
/**
* @auther: chenjh
* @time: 2019/4/10 16:16
* @description 每隔1s读取并更新一次配置文件
*/
@Component @Component
public class ConfigRefreshComponent { public class ConfigRefreshComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class); 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 @PostConstruct
void refresh() { void init() {
Thread configRefreshThread = new Thread(new ConfigRefreshThread()); loadConfig();
configRefreshThread.start(); watchServiceExecutor.submit(this::watchConfigFile);
} }
static class ConfigRefreshThread implements Runnable { @PreDestroy
@Override void destroy() {
public void run() { 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 { try {
Properties properties = new Properties(); 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 configFilePath = ConfigUtils.getCustomizedConfigPath();
String baseUrl; Path configPath = Paths.get(configFilePath);
String trustHost; if (!Files.exists(configPath)) {
String notTrustHost; LOGGER.warn("配置文件不存在: {}", configFilePath);
String pdfPresentationModeDisable; return;
String pdfOpenFileDisable; }
String pdfPrintDisable;
String pdfDownloadDisable; try (BufferedReader bufferedReader = new BufferedReader(new FileReader(configFilePath))) {
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);
properties.load(bufferedReader); properties.load(bufferedReader);
ConfigUtils.restorePropertiesFromEnvFormat(properties); ConfigUtils.restorePropertiesFromEnvFormat(properties);
cacheEnabled = Boolean.parseBoolean(properties.getProperty("cache.enabled", ConfigConstants.DEFAULT_CACHE_ENABLED)); updateConfigConstants(properties);
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);
setWatermarkConfig(properties); setWatermarkConfig(properties);
bufferedReader.close(); LOGGER.info("配置文件重新加载完成");
fileReader.close();
TimeUnit.SECONDS.sleep(1);
} }
} catch (IOException | InterruptedException e) { } catch (IOException e) {
LOGGER.error("读取配置文件异常", e); LOGGER.error("读取配置文件异常", e);
} }
} }
}
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) { private void setWatermarkConfig(Properties properties) {
String watermarkTxt = properties.getProperty("watermark.txt", WatermarkConfigConstants.DEFAULT_WATERMARK_TXT); WatermarkConfigConstants.setWatermarkTxtValue(getProperty(properties, "watermark.txt", WatermarkConfigConstants.DEFAULT_WATERMARK_TXT));
String watermarkXSpace = properties.getProperty("watermark.x.space", WatermarkConfigConstants.DEFAULT_WATERMARK_X_SPACE); WatermarkConfigConstants.setWatermarkXSpaceValue(getProperty(properties, "watermark.x.space", WatermarkConfigConstants.DEFAULT_WATERMARK_X_SPACE));
String watermarkYSpace = properties.getProperty("watermark.y.space", WatermarkConfigConstants.DEFAULT_WATERMARK_Y_SPACE); WatermarkConfigConstants.setWatermarkYSpaceValue(getProperty(properties, "watermark.y.space", WatermarkConfigConstants.DEFAULT_WATERMARK_Y_SPACE));
String watermarkFont = properties.getProperty("watermark.font", WatermarkConfigConstants.DEFAULT_WATERMARK_FONT); WatermarkConfigConstants.setWatermarkFontValue(getProperty(properties, "watermark.font", WatermarkConfigConstants.DEFAULT_WATERMARK_FONT));
String watermarkFontsize = properties.getProperty("watermark.fontsize", WatermarkConfigConstants.DEFAULT_WATERMARK_FONTSIZE); WatermarkConfigConstants.setWatermarkFontsizeValue(getProperty(properties, "watermark.fontsize", WatermarkConfigConstants.DEFAULT_WATERMARK_FONTSIZE));
String watermarkColor = properties.getProperty("watermark.color", WatermarkConfigConstants.DEFAULT_WATERMARK_COLOR); WatermarkConfigConstants.setWatermarkColorValue(getProperty(properties, "watermark.color", WatermarkConfigConstants.DEFAULT_WATERMARK_COLOR));
String watermarkAlpha = properties.getProperty("watermark.alpha", WatermarkConfigConstants.DEFAULT_WATERMARK_ALPHA); WatermarkConfigConstants.setWatermarkAlphaValue(getProperty(properties, "watermark.alpha", WatermarkConfigConstants.DEFAULT_WATERMARK_ALPHA));
String watermarkWidth = properties.getProperty("watermark.width", WatermarkConfigConstants.DEFAULT_WATERMARK_WIDTH); WatermarkConfigConstants.setWatermarkWidthValue(getProperty(properties, "watermark.width", WatermarkConfigConstants.DEFAULT_WATERMARK_WIDTH));
String watermarkHeight = properties.getProperty("watermark.height", WatermarkConfigConstants.DEFAULT_WATERMARK_HEIGHT); WatermarkConfigConstants.setWatermarkHeightValue(getProperty(properties, "watermark.height", WatermarkConfigConstants.DEFAULT_WATERMARK_HEIGHT));
String watermarkAngle = properties.getProperty("watermark.angle", WatermarkConfigConstants.DEFAULT_WATERMARK_ANGLE); WatermarkConfigConstants.setWatermarkAngleValue(getProperty(properties, "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);
}
} }
} }

View File

@@ -2,6 +2,8 @@ package cn.keking.config;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.Codec; import org.redisson.client.codec.Codec;
import org.redisson.config.Config; import org.redisson.config.Config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
@@ -11,42 +13,123 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
/** /**
* Redisson 客户端配置
* Created by kl on 2017/09/26. * Created by kl on 2017/09/26.
* redisson 客户端配置
*/ */
@ConditionalOnExpression("'${cache.type:default}'.equals('redis')") @ConditionalOnExpression("'${cache.type:default}'.equals('redis')")
@ConfigurationProperties(prefix = "spring.redisson") @ConfigurationProperties(prefix = "spring.redisson")
@Configuration @Configuration
public class RedissonConfig { public class RedissonConfig {
private String address; // ========================== 连接配置 ==========================
private int connectionMinimumIdleSize = 10; private static String address;
private int idleConnectionTimeout=10000; private static String password;
private int pingTimeout=1000; private static String clientName;
private int connectTimeout=10000; private static int database = 0;
private int timeout=3000; private static String mode = "single";
private int retryAttempts=3; private static String masterName = "kkfile";
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 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 @Bean
Config config() throws Exception { public static RedissonClient config() throws Exception {
Config config = new Config(); 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) .setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize) .setConnectionPoolSize(connectionPoolSize)
.setDatabase(database) .setDatabase(database)
@@ -61,91 +144,35 @@ public class RedissonConfig {
.setConnectTimeout(connectTimeout) .setConnectTimeout(connectTimeout)
.setIdleConnectionTimeout(idleConnectionTimeout) .setIdleConnectionTimeout(idleConnectionTimeout)
.setPassword(StringUtils.trimToNull(password)); .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.setThreads(thread);
config.setEventLoopGroup(new NioEventLoopGroup()); 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) { // ========================== Getter和Setter方法 ==========================
this.thread = thread;
}
// 连接配置
public String getAddress() { public String getAddress() {
return address; return address;
} }
public void setAddress(String address) { public void setAddress(String address) {
this.address = address; RedissonConfig.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;
} }
public String getPassword() { public String getPassword() {
@@ -153,15 +180,7 @@ public class RedissonConfig {
} }
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; RedissonConfig.password = password;
}
public int getSubscriptionsPerConnection() {
return subscriptionsPerConnection;
}
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) {
this.subscriptionsPerConnection = subscriptionsPerConnection;
} }
public String getClientName() { public String getClientName() {
@@ -169,39 +188,7 @@ public class RedissonConfig {
} }
public void setClientName(String clientName) { public void setClientName(String clientName) {
this.clientName = clientName; RedissonConfig.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;
} }
public int getDatabase() { public int getDatabase() {
@@ -209,30 +196,130 @@ public class RedissonConfig {
} }
public void setDatabase(int database) { public void setDatabase(int database) {
this.database = database; RedissonConfig.database = database;
} }
public boolean isDnsMonitoring() { public static String getMode() {
return dnsMonitoring; return mode;
} }
public void setDnsMonitoring(boolean dnsMonitoring) { public void setMode(String mode) {
this.dnsMonitoring = dnsMonitoring; 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() { public int getDnsMonitoringInterval() {
return dnsMonitoringInterval; return dnsMonitoringInterval;
} }
public void setDnsMonitoringInterval(int 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; return codec;
} }
public void setCodec(String codec) { public void setCodec(String codec) {
this.codec = codec; RedissonConfig.codec = codec;
} }
} }

View File

@@ -33,13 +33,15 @@ public enum FileType {
EPUB("epubFilePreviewImpl"), EPUB("epubFilePreviewImpl"),
BPMN("bpmnFilePreviewImpl"), BPMN("bpmnFilePreviewImpl"),
DCM("dcmFilePreviewImpl"), DCM("dcmFilePreviewImpl"),
MSG("msgFilePreviewImpl"),
DRAWIO("drawioFilePreviewImpl"); 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[] 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"}; 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[] 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[] 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[] EML_TYPES = {"eml"};
private static final String[] MSG_TYPES = {"msg"};
private static final String[] XMIND_TYPES = {"xmind"}; private static final String[] XMIND_TYPES = {"xmind"};
private static final String[] EPUB_TYPES = {"epub"}; private static final String[] EPUB_TYPES = {"epub"};
private static final String[] DCM_TYPES = {"dcm"}; private static final String[] DCM_TYPES = {"dcm"};
@@ -96,6 +98,9 @@ public enum FileType {
for (String eml : EML_TYPES) { for (String eml : EML_TYPES) {
FILE_TYPE_MAPPER.put(eml, FileType.EML); FILE_TYPE_MAPPER.put(eml, FileType.EML);
} }
for (String msg : MSG_TYPES) {
FILE_TYPE_MAPPER.put(msg, FileType.MSG);
}
for (String xmind : XMIND_TYPES) { for (String xmind : XMIND_TYPES) {
FILE_TYPE_MAPPER.put(xmind, FileType.XMIND); 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()); FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码 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.FileAttribute;
import cn.keking.model.FileType; import cn.keking.model.FileType;
import cn.keking.service.cache.CacheService; import cn.keking.service.cache.CacheService;
import cn.keking.service.cache.NotResourceCache; import cn.keking.utils.*;
import cn.keking.utils.EncodingDetects;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.UrlEncoderUtils;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*; import java.io.*;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
@@ -41,7 +24,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.*;
import java.util.stream.IntStream; import java.util.stream.IntStream;
/** /**
@@ -50,10 +32,10 @@ import java.util.stream.IntStream;
*/ */
@Component @Component
@DependsOn(ConfigConstants.BEAN_NAME) @DependsOn(ConfigConstants.BEAN_NAME)
public class FileHandlerService implements InitializingBean { public class FileHandlerService {
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg"; 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 Logger logger = LoggerFactory.getLogger(FileHandlerService.class);
private final String fileDir = ConfigConstants.getFileDir(); private final String fileDir = ConfigConstants.getFileDir();
private final CacheService cacheService; private final CacheService cacheService;
@@ -147,15 +129,6 @@ public class FileHandlerService implements InitializingBean {
cacheService.putImgCache(fileKey, imgs); 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 图片索引 * @param index 图片索引
* @return 图片访问地址 * @return 图片访问地址
*/ */
private String getPdf2jpgUrl(String pdfFilePath, int index) { public String getPdf2jpgUrl(String pdfFilePath, int index) {
String baseUrl = BaseUrlFilter.getBaseUrl(); String baseUrl = BaseUrlFilter.getBaseUrl();
pdfFilePath = pdfFilePath.replace(fileDir, ""); pdfFilePath = pdfFilePath.replace(fileDir, "");
String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4); String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4);
String urlPrefix; // 对整个路径进行编码,包括特殊字符
try { String encodedPath = URLEncoder.encode(pdfFolder, StandardCharsets.UTF_8);
urlPrefix = baseUrl + URLEncoder.encode(pdfFolder, uriEncoding).replaceAll("\\+", "%20"); encodedPath = encodedPath
} catch (UnsupportedEncodingException e) { .replaceAll("%2F", "/") // 恢复斜杠
logger.error("UnsupportedEncodingException", e); .replaceAll("%5C", "/") // 恢复反斜杠
urlPrefix = baseUrl + pdfFolder; .replaceAll("\\+", "%20"); // 空格处理
} // 构建URL使用_作为分隔符这是kkFileView压缩包预览的常见格式
return urlPrefix + "/" + index + PDF2JPG_IMAGE_FORMAT; String url = baseUrl + encodedPath + "/" + index + PDF2JPG_IMAGE_FORMAT;
return url;
} }
/** /**
@@ -215,225 +190,20 @@ public class FileHandlerService implements InitializingBean {
* @param pdfFilePath pdf文件路径 * @param pdfFilePath pdf文件路径
* @return 图片访问集合 * @return 图片访问集合
*/ */
private List<String> loadPdf2jpgCache(String pdfFilePath) { public List<String> loadPdf2jpgCache(String pdfFilePath) { // 移除 static 修饰符
List<String> imageUrls = new ArrayList<>(); List<String> imageUrls = new ArrayList<>();
Integer imageCount = this.getPdf2jpgCache(pdfFilePath); Integer imageCount = this.getPdf2jpgCache(pdfFilePath); // 使用 this. 调用
if (Objects.isNull(imageCount)) { if (Objects.isNull(imageCount)) {
return imageUrls; return imageUrls;
} }
IntStream.range(0, imageCount).forEach(i -> { IntStream.range(0, imageCount).forEach(i -> {
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i); String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i); // 使用 this. 调用
imageUrls.add(imageUrl); imageUrls.add(imageUrl);
}); });
return imageUrls; 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 原字符串(待截取原串) * @param str 原字符串(待截取原串)
@@ -474,7 +244,7 @@ public class FileHandlerService implements InitializingBean {
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey); boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
if (isCompressFile) { //判断是否使用特定压缩包符号 if (isCompressFile) { //判断是否使用特定压缩包符号
try { try {
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名 originFileName = URLDecoder.decode(compressFilePath, uriEncoding); //转义的文件名 解下出原始文件名
attribute.setSkipDownLoad(true); attribute.setSkipDownLoad(true);
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
logger.error("Failed to decode file name: {}", originFileName, e); logger.error("Failed to decode file name: {}", originFileName, e);
@@ -484,12 +254,23 @@ public class FileHandlerService implements InitializingBean {
try { try {
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名 originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
logger.error("Failed to decode file name: {}", originFileName, e); e.printStackTrace();
} }
}else { }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); //文件名处理 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"); 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; String cacheFilePrefixName = null;
try { try {

View File

@@ -34,6 +34,10 @@ public interface FilePreview {
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported"; String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
String XLSX_FILE_PREVIEW_PAGE = "officeweb"; String XLSX_FILE_PREVIEW_PAGE = "officeweb";
String CSV_FILE_PREVIEW_PAGE = "csv"; 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); 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 org.springframework.stereotype.Component;
import java.io.File; import java.io.File;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@@ -28,6 +30,7 @@ public class OfficeToPdfService {
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException { public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
Instant startTime = Instant.now();
File outputFile = new File(outputFilePath_end); File outputFile = new File(outputFilePath_end);
// 假如目标路径不存在,则新建该路径 // 假如目标路径不存在,则新建该路径
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) { if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
@@ -65,7 +68,33 @@ public class OfficeToPdfService {
} else { } else {
builder = LocalConverter.builder().storeProperties(customProperties); builder = LocalConverter.builder().storeProperties(customProperties);
} }
try {
builder.build().convert(inputFile).to(outputFile).execute(); 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;
}
} }

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.config.ConfigConstants;
import cn.keking.model.FileAttribute; import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.CadToPdfService;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.*;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter; import cn.keking.web.filter.BaseUrlFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -15,7 +14,9 @@ import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.StringUtils; 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 * @author chenjh
@@ -29,60 +30,155 @@ public class CadFilePreviewImpl implements FilePreview {
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages"; private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final CadToPdfService cadtopdfservice;
private final OtherFilePreviewImpl otherFilePreview; 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.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.cadtopdfservice = cadtopdfservice;
this.officefilepreviewimpl = officefilepreviewimpl;
} }
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 预览Type参数传了就取参数的没传取系统默认 // 预览Type参数传了就取参数的没传取系统默认
String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType(); String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
String baseUrl = BaseUrlFilter.getBaseUrl(); ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileName = fileAttribute.getName(); String fileName = fileAttribute.getName();
String cadPreviewType = ConfigConstants.getCadPreviewType(); String cadPreviewType = ConfigConstants.getCadPreviewType();
String cacheName = fileAttribute.getCacheName(); String cacheName = fileAttribute.getCacheName();
String outFilePath = fileAttribute.getOutFilePath(); 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); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
String filePath = response.getContent(); String filePath = response.getContent();
String imageUrls = null; int refreshSchedule = ConfigConstants.getTime();
if (StringUtils.hasText(outFilePath)) { if (StringUtils.hasText(outFilePath)) {
try { 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) { } catch (Exception e) {
logger.error("Failed to convert CAD file: {}", filePath, e); logger.error("Failed to start CAD conversion: {}", filePath, e);
}
if (imageUrls == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员"); return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员");
} }
//是否保留CAD源文件 }
}
// 如果已有缓存,直接渲染预览
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()) { if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath); KkFileUtils.deleteFileByPath(filePath);
} }
// 2. 加入缓存(只在转换成功后才添加)
if (ConfigConstants.isCacheEnabled()) { if (ConfigConstants.isCacheEnabled()) {
// 加入缓存 fileHandlerService.addConvertedFile(cacheName,
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); fileHandlerService.getRelativePath(outFilePath));
}
} catch (Exception e) {
logger.error("CAD转换后续处理失败: {}", filePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("CAD转换失败保留源文件: {}", filePath);
if (throwable != null) {
logger.error("转换失败原因: ", throwable);
} }
} }
}, callbackExecutor);
} }
cacheName= WebUtils.encodeFileName(cacheName);
/**
* 渲染预览页面
*/
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)) { if ("tif".equalsIgnoreCase(cadPreviewType)) {
model.addAttribute("currentUrl", cacheName); model.addAttribute("currentUrl", cacheName);
return TIFF_FILE_PREVIEW_PAGE; return TIFF_FILE_PREVIEW_PAGE;
} else if ("svg".equalsIgnoreCase(cadPreviewType)) { } else if ("svg".equalsIgnoreCase(cadPreviewType)) {
model.addAttribute("currentUrl", cacheName); model.addAttribute("currentUrl", cacheName);
if(conversionModule==2){
return CADVIEWER_FILE_PREVIEW_PAGE;
}else {
return SVG_FILE_PREVIEW_PAGE; 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); model.addAttribute("pdfUrl", cacheName);
return PDF_FILE_PREVIEW_PAGE; return PDF_FILE_PREVIEW_PAGE;

View File

@@ -21,6 +21,9 @@ public class CodeFilePreviewImpl implements FilePreview {
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
filePreviewHandle.filePreviewHandle(url, model, 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; return CODE_FILE_PREVIEW_PAGE;
} }
} }

View File

@@ -1,5 +1,6 @@
package cn.keking.service.impl; package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute; import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -32,17 +34,26 @@ public class CommonPreviewImpl implements FilePreview {
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
// 不是http开头浏览器不能直接访问需下载到本地 // 不是http开头浏览器不能直接访问需下载到本地
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
String fileName = fileAttribute.getName(); //获取原始文件名
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
if (url != null && !url.toLowerCase().startsWith("http")) { if (url != null && !url.toLowerCase().startsWith("http")) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, null); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} else { } else {
String file = fileHandlerService.getRelativePath(response.getContent()); String file = fileHandlerService.getRelativePath(response.getContent());
model.addAttribute("currentUrl", file); model.addAttribute("currentUrl", file);
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(fileName, file);
}
} }
} else { } else {
model.addAttribute("currentUrl", url); model.addAttribute("currentUrl", url);
} }
return null; 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.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.Mediatomp4Service;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import org.bytedeco.ffmpeg.global.avcodec; import cn.keking.utils.FileConvertStatusManager;
import org.bytedeco.javacv.FFmpegFrameGrabber; import cn.keking.utils.KkFileUtils;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import java.io.File; import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** /**
* @author : kl * @author : kl
* @authorboke : kailing.pub * @authorboke : kailing.pub
* @create : 2018-03-25 上午11:58 * @create : 2018-03-25 上午11:58
* @description: * @description: 异步视频文件预览实现
**/ **/
@Service @Service
public class MediaFilePreviewImpl implements FilePreview { public class MediaFilePreviewImpl implements FilePreview {
@@ -31,11 +32,20 @@ public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class); private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; 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.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.mediatomp4Service = mediatomp4Service;
this.officefilepreviewimpl = officefilepreviewimpl;
} }
@Override @Override
@@ -46,122 +56,140 @@ public class MediaFilePreviewImpl implements FilePreview {
String outFilePath = fileAttribute.getOutFilePath(); String outFilePath = fileAttribute.getOutFilePath();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
FileType type = fileAttribute.getType(); FileType type = fileAttribute.getType();
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
// 检查是否是需要转换的视频格式
boolean mediaTypes = false; boolean mediaTypes = false;
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES;
for (String temp : mediaTypesConvert) { for (String temp : mediaTypesConvert) {
if (suffix.equals(temp)) { if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true; mediaTypes = true;
break; break;
} }
} }
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的 // 查询转换状态
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存 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); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
String filePath = response.getContent(); String filePath = response.getContent();
String convertedUrl = null;
try {
if (mediaTypes) { if (mediaTypes) {
convertedUrl = convertToMp4(filePath, outFilePath, fileAttribute); // 检查文件大小限制
} else { if (isFileSizeExceeded(filePath)) {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出 return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
} }
try {
// 启动异步转换,并添加回调处理
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) { } catch (Exception e) {
logger.error("Failed to convert media file: {}", filePath, e); logger.error("Failed to start video conversion: {}", filePath, e);
}
if (convertedUrl == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员"); return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
} }
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
} else { } 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); model.addAttribute("mediaUrl", url);
return MEDIA_FILE_PREVIEW_PAGE; return MEDIA_FILE_PREVIEW_PAGE;
} }
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览"); 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) { private boolean checkNeedConvert(boolean mediaTypes) {
//1.检查开关是否开启 // 检查转换开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) { if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes; return mediaTypes;
} }
return false; 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.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.OfficeToPdfService; import cn.keking.service.OfficeToPdfService;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils; import cn.keking.utils.KkFileUtils;
import cn.keking.utils.OfficeUtils; import cn.keking.utils.OfficeUtils;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter; 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.jodconverter.core.office.OfficeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.List; 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. * Created by kl on 2018/1/17.
@@ -28,18 +33,23 @@ import java.util.List;
@Service @Service
public class OfficeFilePreviewImpl implements FilePreview { 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_IMAGE = "image";
public static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages"; 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 FileHandlerService fileHandlerService;
private final OfficeToPdfService officeToPdfService; private final OfficeToPdfService officeToPdfService;
private final OtherFilePreviewImpl otherFilePreview; 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.fileHandlerService = fileHandlerService;
this.officeToPdfService = officeToPdfService; this.officeToPdfService = officeToPdfService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
} }
@Override @Override
@@ -51,12 +61,19 @@ public class OfficeFilePreviewImpl implements FilePreview {
String suffix = fileAttribute.getSuffix(); //获取文件后缀 String suffix = fileAttribute.getSuffix(); //获取文件后缀
String fileName = fileAttribute.getName(); //获取文件原始名称 String fileName = fileAttribute.getName(); //获取文件原始名称
String filePassword = fileAttribute.getFilePassword(); //获取密码 String filePassword = fileAttribute.getFilePassword(); //获取密码
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html
String cacheName = fileAttribute.getCacheName(); //转换后的文件名 String cacheName = fileAttribute.getCacheName(); //转换后的文件名
String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径 String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径
// 查询转换状态
String convertStatusResult = checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (convertStatusResult != null) {
return convertStatusResult;
}
if (!officePreviewType.equalsIgnoreCase("html")) { if (!officePreviewType.equalsIgnoreCase("html")) {
if (ConfigConstants.getOfficeTypeWeb() .equalsIgnoreCase("web")) { if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
if (suffix.equalsIgnoreCase("xlsx")) { if (suffix.equalsIgnoreCase("xlsx")) {
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理 model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
return XLSX_FILE_PREVIEW_PAGE; return XLSX_FILE_PREVIEW_PAGE;
@@ -67,13 +84,191 @@ public class OfficeFilePreviewImpl implements FilePreview {
} }
} }
} }
if (forceUpdatedCache|| !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
// 图片预览模式(异步转换)
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); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
String filePath = response.getContent(); String filePath = response.getContent();
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件 boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) { if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
// 加密文件需要密码 // 加密文件需要密码
@@ -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识别 model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); //输出转义文件名 方便url识别
return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE; 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; public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute) {
try { FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
imageUrls = fileHandlerService.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute); int refreshSchedule = ConfigConstants.getTime();
} catch (Exception e) { boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
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;
}
}
}
}
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;
}
}
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, "文件转换失败,无法继续转换");
}
}
}
return null;
}
} }

View File

@@ -5,15 +5,24 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.PdfToJpgService;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException; import org.apache.poi.EncryptedDocumentException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; 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. * Created by kl on 2018/1/17.
@@ -22,62 +31,235 @@ import java.util.List;
@Service @Service
public class PdfFilePreviewImpl implements FilePreview { 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 FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; private final OtherFilePreviewImpl otherFilePreview;
private static final String PDF_PASSWORD_MSG = "password"; private final PdfToJpgService pdftojpgservice;
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) { 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.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.pdftojpgservice = pdftojpgservice;
this.officefilepreviewimpl = officefilepreviewimpl;
} }
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String pdfName = fileAttribute.getName(); //获取原始文件名 String pdfName = fileAttribute.getName(); //获取原始文件名
String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型 String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令 boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径 String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径
String originFilePath = fileAttribute.getOriginFilePath(); //原始文件路径 String originFilePath; //原始文件路径
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) { String cacheName = pdfName+officePreviewType;
//当文件不存在时,就去下载 String filePassword = fileAttribute.getFilePassword(); // 获取密码
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { 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); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
originFilePath = response.getContent(); originFilePath = response.getContent();
if (ConfigConstants.isCacheEnabled()) { // 检查文件是否需要密码,但不启动转换
// 加入缓存 if (filePassword == null || filePassword.trim().isEmpty()) {
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(originFilePath)); // 没有提供密码,先检查文件是否需要密码
if (checkIfPdfNeedsPassword(originFilePath, cacheName, pdfName)) {
model.addAttribute("needFilePassword", true);
model.addAttribute("fileName", pdfName);
model.addAttribute("cacheName", pdfName);
return EXEL_FILE_PREVIEW_PAGE;
} }
} }
List<String> imageUrls;
try { try {
imageUrls = fileHandlerService.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute); // 启动异步转换
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);
}
} 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) { } catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e); Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) { for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) { if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) { if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
model.addAttribute("needFilePassword", true); FileConvertStatusManager.convertSuccess(cacheName);
return EXEL_FILE_PREVIEW_PAGE; 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) { } catch (Exception e) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常请联系管理员"); 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("imgUrls", imageUrls);
model.addAttribute("currentUrl", imageUrls.get(0)); model.addAttribute("currentUrl", imageUrls.getFirst());
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) { if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) {
return OFFICE_PICTURE_FILE_PREVIEW_PAGE; return OFFICE_PICTURE_FILE_PREVIEW_PAGE;
} else { } else {
return PICTURE_FILE_PREVIEW_PAGE; return PICTURE_FILE_PREVIEW_PAGE;
} }
} else { } 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开头浏览器不能直接访问需下载到本地 // 不是http开头浏览器不能直接访问需下载到本地
if (url != null && !url.toLowerCase().startsWith("http")) { if (url != null && !url.toLowerCase().startsWith("http")) {
if (!fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
if (response.isFailure()) { if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
@@ -93,7 +275,7 @@ public class PdfFilePreviewImpl implements FilePreview {
} else { } else {
model.addAttribute("pdfUrl", url); model.addAttribute("pdfUrl", url);
} }
}
return PDF_FILE_PREVIEW_PAGE; return PDF_FILE_PREVIEW_PAGE;
} }
} }

View File

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

View File

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

View File

@@ -5,37 +5,60 @@ import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.utils.ConvertPicUtil; import cn.keking.service.TifToPdfService;
import cn.keking.utils.DownloadUtils; import cn.keking.utils.DownloadUtils;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils; import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** /**
* tiff 图片文件处理 * tiff 图片文件处理
*
* @author kl (http://kailing.pub) * @author kl (http://kailing.pub)
* @since 2021/2/8 * @since 2021/2/8
*/ */
@Service @Service
public class TiffFilePreviewImpl implements FilePreview { 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 FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; 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.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview; this.otherFilePreview = otherFilePreview;
this.tiftoservice = tiftoservice;
this.officefilepreviewimpl = officefilepreviewimpl;
} }
@Override @Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) { public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String fileName = fileAttribute.getName(); String fileName = fileAttribute.getName();
String tifPreviewType = ConfigConstants.getTifPreviewType(); String tifPreviewType = ConfigConstants.getTifPreviewType();
String cacheName = fileAttribute.getCacheName(); String cacheName = fileAttribute.getCacheName();
String outFilePath = fileAttribute.getOutFilePath(); 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 ("jpg".equalsIgnoreCase(tifPreviewType) || "pdf".equalsIgnoreCase(tifPreviewType)) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName); ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
@@ -43,66 +66,113 @@ public class TiffFilePreviewImpl implements FilePreview {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg()); return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
} }
String filePath = response.getContent(); String filePath = response.getContent();
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
try { try {
ConvertPicUtil.convertJpg2Pdf(filePath, outFilePath); // 启动异步转换
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) { } catch (Exception e) {
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) { logger.error("Failed to start TIF conversion: {}", filePath, e);
model.addAttribute("imgUrls", url); return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换异常请联系系统管理员!");
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));
} }
} else {
// 如果已有缓存,直接渲染预览
if ("pdf".equalsIgnoreCase(tifPreviewType)) {
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName));
return PDF_FILE_PREVIEW_PAGE; return PDF_FILE_PREVIEW_PAGE;
}else { } else if ("jpg".equalsIgnoreCase(tifPreviewType)) {
// 将tif转换为jpg返回转换后的文件路径、文件名的list List<String> imgCache = fileHandlerService.getImgCache(cacheName);
List<String> listPic2Jpg; if (imgCache == null || imgCache.isEmpty()) {
try { return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换缓存异常请联系系统管理员!");
listPic2Jpg = ConvertPicUtil.convertTif2Jpg(filePath, outFilePath,forceUpdatedCache); }
} catch (Exception e) { model.addAttribute("imgUrls", imgCache);
if (e.getMessage().contains("Bad endianness tag (not 0x4949 or 0x4d4d)") ) { model.addAttribute("currentUrl", imgCache.getFirst());
model.addAttribute("imgUrls", url);
model.addAttribute("currentUrl", url);
return PICTURE_FILE_PREVIEW_PAGE; return PICTURE_FILE_PREVIEW_PAGE;
}else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转JPG异常请联系系统管理员!" );
} }
} }
//是否保留源文件,转换失败保留源文件,转换成功删除源文件
if(!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
} }
// 处理普通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()) { if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.putImgCache(cacheName, listPic2Jpg); fileHandlerService.putImgCache(cacheName, listPic2Jpg);
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath)); fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
} }
model.addAttribute("imgUrls", listPic2Jpg); }
model.addAttribute("currentUrl", listPic2Jpg.get(0)); FileConvertStatusManager.convertSuccess(cacheName);
return PICTURE_FILE_PREVIEW_PAGE; 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);
} }
} }
if ("pdf".equalsIgnoreCase(tifPreviewType)) { });
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName));
return PDF_FILE_PREVIEW_PAGE; // 添加转换完成后的回调
conversionFuture.thenRunAsync(() -> {
try {
// 是否保留源文件(只在转换成功后才删除)
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
} }
else if ("jpg".equalsIgnoreCase(tifPreviewType)) { } catch (Exception e) {
model.addAttribute("imgUrls", fileHandlerService.getImgCache(cacheName)); logger.error("TIF转换后续处理失败: {}", filePath, e);
model.addAttribute("currentUrl", fileHandlerService.getImgCache(cacheName).get(0));
return PICTURE_FILE_PREVIEW_PAGE;
} }
}, 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开头浏览器不能直接访问需下载到本地 // 不是http开头浏览器不能直接访问需下载到本地
if (url != null && !url.toLowerCase().startsWith("http")) { if (url != null && !url.toLowerCase().startsWith("http")) {
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) { if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
@@ -124,4 +194,3 @@ public class TiffFilePreviewImpl implements FilePreview {
return TIFF_FILE_PREVIEW_PAGE; 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.config.ConfigConstants;
import cn.keking.model.FileAttribute; import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse; import cn.keking.model.ReturnResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.mola.galimatias.GalimatiasParseException; import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.Arrays; import java.nio.file.Files;
import java.util.Map; import java.nio.file.StandardCopyOption;
import java.util.UUID; import java.util.UUID;
import static cn.keking.utils.KkFileUtils.isFtpUrl; import static cn.keking.utils.KkFileUtils.*;
import static cn.keking.utils.KkFileUtils.isHttpUrl;
/** /**
* @author yudian-it * @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_USERNAME = "ftp.username";
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password"; 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_CONTROL_ENCODING = "ftp.control.encoding";
private static final RestTemplate restTemplate = new RestTemplate(); private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper();
/** /**
* @param fileAttribute fileAttribute * @param fileAttribute fileAttribute
@@ -50,17 +37,17 @@ public class DownloadUtils {
* @return 本地文件绝对路径 * @return 本地文件绝对路径
*/ */
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) { public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
// 忽略ssl证书
String urlStr = null; String urlStr = null;
try { try {
SslUtils.ignoreSsl(); urlStr = fileAttribute.getUrl();
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
} catch (Exception e) { } catch (Exception e) {
logger.error("忽略SSL证书异常:", e); logger.error("处理URL异常:", e);
} }
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", ""); ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute); String realPath = getRelFilePath(fileName, fileAttribute);
// 获取文件后缀用于校验
final String fileSuffix = fileAttribute.getSuffix();
// 判断是否非法地址 // 判断是否非法地址
if (KkFileUtils.isIllegalFileName(realPath)) { if (KkFileUtils.isIllegalFileName(realPath)) {
response.setCode(1); response.setCode(1);
@@ -90,36 +77,33 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) { if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) { if (isHttpUrl(url)) {
File realFile = new File(realPath); File realFile = new File(realPath);
factory.setConnectionRequestTimeout(2000); //设置超时时间 CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
factory.setConnectTimeout(10000); String finalUrlStr = urlStr;
factory.setReadTimeout(72000); HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build(); // 获取响应头中的Content-Type
factory.setHttpClient(httpClient); //加入重定向方法 String contentType = responseWrapper.getContentType();
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> { // 如果是Office/设计文件需要校验MIME类型
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); if (WebUtils.isMimeCheckRequired(fileSuffix)) {
String proxyAuthorization = fileAttribute.getKkProxyAuthorization(); if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
if(StringUtils.hasText(proxyAuthorization)){ logger.error("文件类型错误期望二进制文件但接收到文本类型url: {}, Content-Type: {}",
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class); finalUrlStr, contentType);
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value)); responseWrapper.setHasError(true);
return;
} }
}; }
try {
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> { // 保存文件
FileUtils.copyToFile(fileResponse.getBody(), realFile); FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
return null;
}); });
} catch (Exception e) {
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败:" + e);
return response;
}
} else if (isFtpUrl(url)) { } else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME); String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD); String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING); 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 { } else {
response.setCode(1); response.setCode(1);
response.setMsg("url不能识别url" + urlStr); response.setMsg("url不能识别url" + urlStr);
@@ -138,9 +122,68 @@ public class DownloadUtils {
response.setMsg(e.getMessage()); response.setMsg(e.getMessage());
} }
return response; 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; 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 cn.keking.config.ConfigConstants;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URL; import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Objects;
/** /**
* @auther: chenjh * @auther: chenjh
* @since: 2019/6/18 14:36 * @since: 2019/6/18 14:36
*/ */
public class FtpUtils { public class FtpUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class); 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 { /**
FTPClient ftpClient = new FTPClient(); * 从FTP服务器下载文件到本地
ftpClient.connect(host, port); */
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) { public static void download(String ftpUrl, String ftpport, String localFilePath,
ftpClient.login(username, password); 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());
} }
int reply = ftpClient.getReplyCode(); }
if (!FTPReply.isPositiveCompletion(reply)) { } 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(); 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.setControlEncoding(controlEncoding); ftpClient.setControlEncoding(controlEncoding);
ftpClient.connect(host, port);
if (!ftpClient.login(username, password)) {
throw new IOException("FTP登录失败用户名或密码错误");
}
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE); ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
return ftpClient; 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.io.File;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.*;
import java.util.List; import java.util.regex.Matcher;
import java.util.Objects; import java.util.regex.Pattern;
public class KkFileUtils { public class KkFileUtils {
@@ -19,17 +19,31 @@ public class KkFileUtils {
public static final String DEFAULT_FILE_ENCODING = "UTF-8"; public static final String DEFAULT_FILE_ENCODING = "UTF-8";
private static final List<String> illegalFileStrList = new ArrayList<>(); // 路径遍历关键字列表
private static final Set<String> illegalFileStrList;
static { static {
illegalFileStrList.add("../"); Set<String> set = new HashSet<>();
illegalFileStrList.add("./");
illegalFileStrList.add("..\\"); // 基本路径遍历
illegalFileStrList.add(".\\"); Collections.addAll(set, "../", "./", "..\\", ".\\", "\\..", "\\.", "..", "...", "....", ".....");
illegalFileStrList.add("\\..");
illegalFileStrList.add("\\."); // URL编码的路径遍历
illegalFileStrList.add(".."); Collections.addAll(set, "%2e%2e%2f", "%2e%2e/", "..%2f", "%2e%2e%5c", "%2e%2e\\", "..%5c",
illegalFileStrList.add("..."); "%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; 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 * @return 是否http
*/ */
public static boolean isHttpUrl(URL url) { 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); File file = new File(filePath);
return file.exists(); 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.2",
"/opt/libreoffice24.8", "/opt/libreoffice24.8",
"/opt/libreoffice25.2", "/opt/libreoffice25.2",
"/opt/libreoffice25.8",
"/opt/libreoffice26.2",
"/opt/libreoffice26.8",
"/usr/lib64/libreoffice", "/usr/lib64/libreoffice",
"/usr/lib/libreoffice", "/usr/lib/libreoffice",
"/usr/local/lib64/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; package cn.keking.utils;
import javax.net.ssl.*; import org.apache.hc.client5.http.config.RequestConfig;
import java.security.cert.CertificateException; 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; import java.security.cert.X509Certificate;
/** /**
* @author 鞠玉果 * @author 高雄
*/ */
public class SslUtils { public class SslUtils {
private static void trustAllHttpsCertificates() throws Exception { /**
TrustManager[] trustAllCerts = new TrustManager[1]; * 创建忽略SSL验证的HttpClient适用于HttpClient 5.6
TrustManager tm = new miTM(); */
trustAllCerts[0] = tm; public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
SSLContext sc = SSLContext.getInstance("SSL"); return configureHttpClientBuilder(HttpClients.custom(), true, true).build();
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 {
}
} }
/** /**
* 忽略HTTPS请求的SSL证书必须在openConnection之前调用 * 配置HttpClientBuilder支持SSL和重定向配置
* @param builder HttpClientBuilder
* @param ignoreSSL 是否忽略SSL验证
* @param enableRedirect 是否启用重定向
* @return 配置好的HttpClientBuilder
*/ */
public static void ignoreSsl() throws Exception { public static HttpClientBuilder configureHttpClientBuilder(HttpClientBuilder builder,
HostnameVerifier hv = (urlHostName, session) -> true; boolean ignoreSSL,
trustAllHttpsCertificates(); boolean enableRedirect) throws Exception {
HttpsURLConnection.setDefaultHostnameVerifier(hv); // 配置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 { public class UrlEncoderUtils {
private static BitSet dontNeedEncoding; private static final BitSet dontNeedEncoding;
static { static {
dontNeedEncoding = new BitSet(256); dontNeedEncoding = new BitSet(256);
@@ -19,7 +19,7 @@ public class UrlEncoderUtils {
dontNeedEncoding.set(i); dontNeedEncoding.set(i);
} }
dontNeedEncoding.set('+'); dontNeedEncoding.set('+');
/** /*
* 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢<br> * 这里会有误差,比如输入一个字符串 123+456,它到底是原文就是123+456还是123 456做了urlEncode后的内容呢<br>
* 其实问题是一样的比如遇到123%2B456,它到底是原文即使如此还是123+456 urlEncode后的呢 <br> * 其实问题是一样的比如遇到123%2B456,它到底是原文即使如此还是123+456 urlEncode后的呢 <br>
* 在这里我认为只要符合urlEncode规范的就当作已经urlEncode过了<br> * 在这里我认为只要符合urlEncode规范的就当作已经urlEncode过了<br>
@@ -36,13 +36,10 @@ public class UrlEncoderUtils {
* 判断str是否urlEncoder.encode过<br> * 判断str是否urlEncoder.encode过<br>
* 经常遇到这样的情况拿到一个URL,但是搞不清楚到底要不要encode.<Br> * 经常遇到这样的情况拿到一个URL,但是搞不清楚到底要不要encode.<Br>
* 不做encode吧担心出错做encode吧又怕重复了<Br> * 不做encode吧担心出错做encode吧又怕重复了<Br>
*
* @param str
* @return
*/ */
public static boolean hasUrlEncoded(String str) { public static boolean hasUrlEncoded(String str) {
/** /*
* 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' <br> * 支持JAVA的URLEncoder.encode出来的string做判断。 即: 将' '转成'+' <br>
* 0-9a-zA-Z保留 <br> * 0-9a-zA-Z保留 <br>
* '-''_''.''*'保留 <br> * '-''_''.''*'保留 <br>
@@ -51,7 +48,7 @@ public class UrlEncoderUtils {
boolean needEncode = false; boolean needEncode = false;
for (int i = 0; i < str.length(); i++) { for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i); char c = str.charAt(i);
if (dontNeedEncoding.get((int) c)) { if (dontNeedEncoding.get(c)) {
continue; continue;
} }
if (c == '%' && (i + 2) < str.length()) { if (c == '%' && (i + 2) < str.length()) {
@@ -72,9 +69,6 @@ public class UrlEncoderUtils {
/** /**
* 判断c是否是16进制的字符 * 判断c是否是16进制的字符
*
* @param c
* @return
*/ */
private static boolean isDigit16Char(char c) { private static boolean isDigit16Char(char c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'); return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F');

View File

@@ -1,10 +1,15 @@
package cn.keking.utils; package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import io.mola.galimatias.GalimatiasParseException; import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.HtmlUtils;
@@ -17,8 +22,7 @@ import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -30,6 +34,18 @@ public class WebUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class); private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
private static final String BASE64_MSG = "base64"; 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 * 获取标准的URL
* *
@@ -46,11 +62,10 @@ public class WebUtils {
* *
*/ */
public static String encodeFileName(String name) { public static String encodeFileName(String name) {
try { name = URLEncoder.encode(name, StandardCharsets.UTF_8)
name = URLEncoder.encode(name, "UTF-8").replaceAll("\\+", "%20"); .replaceAll("%2F", "/") // 恢复斜杠
} catch (UnsupportedEncodingException e) { .replaceAll("%5C", "/") // 恢复反斜杠
return null; .replaceAll("\\+", "%20"); // 空格处理
}
return name; return name;
} }
@@ -215,11 +230,7 @@ public class WebUtils {
if (fileNameEndIndex < fileNameStartIndex) { if (fileNameEndIndex < fileNameStartIndex) {
return url; return url;
} }
try { encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), StandardCharsets.UTF_8);
encodedFileName = URLEncoder.encode(noQueryUrl.substring(fileNameStartIndex, fileNameEndIndex), "UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
return url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex); return url.substring(0, fileNameStartIndex) + encodedFileName + url.substring(fileNameEndIndex);
} }
@@ -234,17 +245,18 @@ public class WebUtils {
String urls = request.getParameter("urls"); String urls = request.getParameter("urls");
String currentUrl = request.getParameter("currentUrl"); String currentUrl = request.getParameter("currentUrl");
String urlPath = request.getParameter("urlPath"); String urlPath = request.getParameter("urlPath");
String encryption = request.getParameter("encryption");
if (StringUtils.isNotBlank(url)) { if (StringUtils.isNotBlank(url)) {
return decodeUrl(url); return decodeUrl(url,encryption);
} }
if (StringUtils.isNotBlank(currentUrl)) { if (StringUtils.isNotBlank(currentUrl)) {
return decodeUrl(currentUrl); return decodeUrl(currentUrl,encryption);
} }
if (StringUtils.isNotBlank(urlPath)) { if (StringUtils.isNotBlank(urlPath)) {
return decodeUrl(urlPath); return decodeUrl(urlPath,encryption);
} }
if (StringUtils.isNotBlank(urls)) { if (StringUtils.isNotBlank(urls)) {
urls = decodeUrl(urls); urls = decodeUrl(urls,encryption);
String[] images = urls.split("\\|"); String[] images = urls.split("\\|");
return images[0]; return images[0];
} }
@@ -268,14 +280,21 @@ public class WebUtils {
* *
* aHR0cHM6Ly9maWxlLmtla2luZy5jbi9kZW1vL%2BS4reaWhy5wcHR4 -> https://file.keking.cn/demo/%E4%B8%AD%E6%96%87.pptx -> https://file.keking.cn/demo/中文.pptx * 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) { public static String decodeUrl(String source,String encryption) {
String url = decodeBase64String(source, StandardCharsets.UTF_8); String url;
if (! StringUtils.isNotBlank(url)){ if(ObjectUtils.isEmpty(encryption) || Objects.equals(ConfigConstants.getaesKey(), "false")){
return null; 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;
} }
}
/** /**
* 将 Base64 字符串使用指定字符集解码 * 将 Base64 字符串使用指定字符集解码
@@ -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 * 获取 url 的 host
* @param urlStr url * @param urlStr url
@@ -371,4 +414,165 @@ public class WebUtils {
} }
session.removeAttribute(key); 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; package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute; import cn.keking.model.FileAttribute;
import cn.keking.service.FileHandlerService; import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview; import cn.keking.service.FilePreview;
import cn.keking.service.FilePreviewFactory; import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService; import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl; import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.KkFileUtils; import cn.keking.utils.*;
import cn.keking.utils.WebUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.opensagres.xdocreport.core.io.IOUtils; import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping; 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.bind.annotation.ResponseBody;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@@ -35,9 +28,10 @@ import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE; 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 * @author yudian-it
@@ -45,16 +39,19 @@ import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
@Controller @Controller
public class OnlinePreviewController { public class OnlinePreviewController {
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
private final Logger logger = LoggerFactory.getLogger(OnlinePreviewController.class); 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 FilePreviewFactory previewFactory;
private final CacheService cacheService; private final CacheService cacheService;
private final FileHandlerService fileHandlerService; private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview; 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) { public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
this.previewFactory = filePreviewFactory; this.previewFactory = filePreviewFactory;
@@ -64,16 +61,31 @@ public class OnlinePreviewController {
} }
@GetMapping( "/onlinePreview") @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; String fileUrl;
try { try {
fileUrl = WebUtils.decodeUrl(url); fileUrl = WebUtils.decodeUrl(url, encryption);
} catch (Exception ex) { } catch (Exception ex) {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url"); String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return otherFilePreview.notSupportedFile(model, errorMsg); 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); model.addAttribute("file", fileAttribute);
FilePreview filePreview = previewFactory.get(fileAttribute); FilePreview filePreview = previewFactory.get(fileAttribute);
logger.info("预览文件url{}previewType{}", fileUrl, fileAttribute.getType()); logger.info("预览文件url{}previewType{}", fileUrl, fileAttribute.getType());
@@ -85,10 +97,22 @@ public class OnlinePreviewController {
} }
@GetMapping( "/picturesPreview") @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; String fileUrls;
try { try {
fileUrls = WebUtils.decodeUrl(urls); fileUrls = WebUtils.decodeUrl(urls, encryption);
// 防止XSS攻击 // 防止XSS攻击
fileUrls = KkFileUtils.htmlEscape(fileUrls); fileUrls = KkFileUtils.htmlEscape(fileUrls);
} catch (Exception ex) { } catch (Exception ex) {
@@ -119,51 +143,54 @@ public class OnlinePreviewController {
* @param response response * @param response response
*/ */
@GetMapping("/getCorsFile") @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; URL url;
try { try {
urlPath = WebUtils.decodeUrl(urlPath); urlPath = WebUtils.decodeUrl(urlPath, encryption);
url = WebUtils.normalizedURL(urlPath); url = WebUtils.normalizedURL(urlPath);
} catch (Exception ex) { } catch (Exception ex) {
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex); logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex);
return; return;
} }
assert urlPath != null; 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); logger.info("读取跨域文件异常可能存在非法访问urlPath{}", urlPath);
return; return;
} }
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
InputStream inputStream = null; InputStream inputStream = null;
logger.info("读取跨域pdf文件url{}", urlPath); logger.info("读取跨域文件url{}", urlPath);
if (!urlPath.toLowerCase().startsWith("ftp:")) { if (!isFtpUrl(url)) {
factory.setConnectionRequestTimeout(2000); CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
factory.setConnectTimeout(10000);
factory.setReadTimeout(72000); HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build(); } else {
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));
}
};
try { try {
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> { String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
IOUtils.copy(fileResponse.getBody(), response.getOutputStream()); String contentType = WebUtils.getContentTypeByFilename(filename);
return null; if (contentType != null) {
}); response.setContentType(contentType);
} catch (Exception e) {
System.out.println(e);
} }
}else{ String ftpUsername = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_USERNAME);
try { String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD);
if(urlPath.contains(".svg")) { String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING);
response.setContentType("image/svg+xml"); String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT);
} inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
inputStream = (url).openStream();
IOUtils.copy(inputStream, response.getOutputStream()); IOUtils.copy(inputStream, response.getOutputStream());
} catch (IOException e) { } catch (IOException e) {
logger.error("读取跨域文件异常url{}", urlPath); logger.error("读取跨域文件异常url{}", urlPath);
@@ -180,9 +207,32 @@ public class OnlinePreviewController {
*/ */
@GetMapping("/addTask") @GetMapping("/addTask")
@ResponseBody @ResponseBody
public String addQueueTask(String url) { public String addQueueTask(@RequestParam String url,
logger.info("添加转码队列url{}", url); @RequestParam(required = false) String key,
cacheService.addQueueTask(url); @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"; return "success";
} }
} }

View File

@@ -47,6 +47,14 @@ public class AttributeSetFilter implements Filter {
request.setAttribute("homePagination", ConfigConstants.getHomePagination()); request.setAttribute("homePagination", ConfigConstants.getHomePagination());
request.setAttribute("homePageSize", ConfigConstants.getHomePageSize()); request.setAttribute("homePageSize", ConfigConstants.getHomePageSize());
request.setAttribute("homeSearch", ConfigConstants.getHomeSearch()); 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; package cn.keking.web.filter;
import cn.keking.config.ConfigConstants; import cn.keking.config.ConfigConstants;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils; import cn.keking.utils.WebUtils;
import io.mola.galimatias.GalimatiasParseException; import io.mola.galimatias.GalimatiasParseException;
import org.jodconverter.core.util.OSUtils; import org.jodconverter.core.util.OSUtils;
@@ -11,6 +13,8 @@ import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import jakarta.servlet.*; import jakarta.servlet.*;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder; import java.net.URLDecoder;
@@ -56,18 +60,42 @@ public class TrustDirFilter implements Filter {
} }
private boolean allowPreview(String urlPath) { private boolean allowPreview(String urlPath) {
//判断URL是否合法 // 判断URL是否合法
if(!StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) { if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
return false ; return false;
} }
try { try {
URL url = WebUtils.normalizedURL(urlPath); URL url = WebUtils.normalizedURL(urlPath);
if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) { if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) {
String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name()); String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
if (OSUtils.IS_OS_WINDOWS) { // 将文件路径转换为File对象
filePath = filePath.replaceAll("/", "\\\\"); 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; return true;
} catch (IOException | GalimatiasParseException e) { } catch (IOException | GalimatiasParseException e) {
@@ -75,4 +103,26 @@ public class TrustDirFilter implements Filter {
return false; 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 { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = WebUtils.getSourceUrl(request); String url = WebUtils.getSourceUrl(request);
String host = WebUtils.getHost(url); String host = WebUtils.getHost(url);
if (isNotTrustHost(host)) { if (isNotTrustHost(host) || !WebUtils.isValidUrl(url)) {
String currentHost = host == null ? "UNKNOWN" : host; String currentHost = host == null ? "UNKNOWN" : host;
if (response instanceof HttpServletResponse httpServletResponse) { if (response instanceof HttpServletResponse httpServletResponse) {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
@@ -78,7 +78,6 @@ public class TrustHostFilter implements Filter {
&& matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) { && matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) {
return true; return true;
} }
// 如果配置了白名单,检查是否在白名单中 // 如果配置了白名单,检查是否在白名单中
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) { if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
// 支持通配符 * 表示允许所有主机 // 支持通配符 * 表示允许所有主机

View File

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

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