Compare commits

...

70 Commits

Author SHA1 Message Date
kl
40eac988cc Trigger docker archive workflow from temp branch 2026-04-14 10:30:32 +08:00
kl
2c0702874b Add manual docker archive release workflow 2026-04-14 10:30:00 +08:00
kl
171762d676 Merge pull request #740 from kekingcn/release/5.0.0
Release 5.0.0
2026-04-14 09:13:51 +08:00
kl
76e091900b Complete 5.0.0 release notes from v4.4.0 diff 2026-04-14 09:07:18 +08:00
kl
bfa4ceab90 Document 5.0.0 default config changes 2026-04-14 09:04:43 +08:00
kl
b18cfa797a Align release record page with 5.0.0 2026-04-14 09:02:29 +08:00
kl
8a117a41e8 Merge v5.0 notes into 5.0.0 release notes 2026-04-14 09:00:11 +08:00
kl
17ba41320e Prepare 5.0.0 release 2026-04-14 08:57:01 +08:00
kl
476c0bfefc Merge pull request #739 from kekingcn/paseo/ai-docs
Add AI-friendly repository guide
2026-04-14 08:13:52 +08:00
kl
1c6691d785 Clarify config defaults in AGENTS guide 2026-04-14 08:06:52 +08:00
kl
36ae290cb6 Add AI-friendly repository guide 2026-04-14 07:54:42 +08:00
kl
597715ce33 Merge pull request #738 from kekingcn/paseo/kkfileview
Refine archive preview and PDF defaults
2026-04-13 21:09:42 +08:00
kl
a8a08c1dcc Address PR review feedback 2026-04-13 21:02:59 +08:00
kl
7757729efd Refine archive preview and PDF defaults 2026-04-13 20:54:27 +08:00
kl
b246bfdac7 Merge pull request #737 from kekingcn/codex/fix-index-form-layout
fix: correct index preview form layout
2026-04-11 21:36:37 +08:00
chenkailing
d35393ba22 fix: tighten index form layout 2026-04-11 21:30:25 +08:00
chenkailing
c893dd7095 fix: keep preview parameters on one row 2026-04-11 21:12:06 +08:00
kl
9bdb18d833 Merge pull request #736 from kekingcn/codex/ci-auto-deploy-server-build
feat: add server-build auto deploy
2026-04-11 20:30:13 +08:00
chenkailing
58fc1af74f fix: address deploy review comments 2026-04-11 20:22:15 +08:00
chenkailing
1b3cf33bf0 feat: add server-build auto deploy 2026-04-11 19:36:19 +08:00
kl
c9005d0c04 Merge pull request #735 from kekingcn/codex/ci-auto-deploy-fix
fix: harden master auto deploy artifact delivery
2026-04-11 17:16:26 +08:00
chenkailing
37bda20d08 fix: harden master auto deploy artifact delivery 2026-04-11 17:08:31 +08:00
kl
1819861647 Merge pull request #734 from kekingcn/codex/ci-auto-deploy
[codex] add master auto deploy workflow
2026-04-11 16:27:37 +08:00
chenkailing
352b86b40d fix: address deploy review feedback 2026-04-11 16:14:01 +08:00
chenkailing
853ad0154f chore: drop generated pycache 2026-04-11 15:54:43 +08:00
chenkailing
c88bf04a0d feat: add master auto deploy workflow 2026-04-11 15:54:11 +08:00
kl
6a84e61ecb Merge pull request #733 from kekingcn/codex/redesign-demo-page-v1-polish
[codex] polish demo portal pages
2026-04-11 15:29:47 +08:00
chenkailing
dd6e369e6a feat: polish demo portal pages 2026-04-11 15:19:26 +08:00
chenkailing
bd20546b6d feat: redesign demo pages v1 2026-04-11 00:29:49 +08:00
kl
c41c14bf3c Merge pull request #729 from kekingcn/copilot/fix-pdf-preview-error
Restore mobile PDF preview compatibility for browsers without `Array.prototype.at`
2026-04-09 12:04:13 +08:00
copilot-swe-agent[bot]
3fbf0156e2 fix: refine pdfjs at compatibility polyfill
Agent-Logs-Url: https://github.com/kekingcn/kkFileView/sessions/287f0212-d368-4b59-ae70-0374fb3fe76f

Co-authored-by: klboke <18591662+klboke@users.noreply.github.com>
2026-04-09 02:50:17 +00:00
copilot-swe-agent[bot]
530304b832 test: use junit assertions for pdf viewer compatibility
Agent-Logs-Url: https://github.com/kekingcn/kkFileView/sessions/287f0212-d368-4b59-ae70-0374fb3fe76f

Co-authored-by: klboke <18591662+klboke@users.noreply.github.com>
2026-04-09 02:48:24 +00:00
copilot-swe-agent[bot]
5499150395 fix: add pdfjs at compatibility polyfill
Agent-Logs-Url: https://github.com/kekingcn/kkFileView/sessions/287f0212-d368-4b59-ae70-0374fb3fe76f

Co-authored-by: klboke <18591662+klboke@users.noreply.github.com>
2026-04-09 02:46:24 +00:00
kl
2dcc62f3da fix(actions): use @Copilot mention in issue auto comment 2026-04-09 10:45:31 +08:00
copilot-swe-agent[bot]
405a12ef07 Initial plan 2026-04-09 02:39:52 +00:00
kl
bb0734b3d3 Merge feat/e2e-expand-pr342-coverage into master 2026-04-07 16:32:12 +08:00
kl
a9e394950f test(e2e): expand preview coverage for pdf mp4 cad 2026-04-07 16:31:55 +08:00
kl
be6023701c Merge PR342 into master with trust-host conflict resolution 2026-04-07 15:59:20 +08:00
kl
02bcd35779 verify: 回退到原始 OFD 修复版本进行验证 (#724)
使用原始选择器 #content svg { overflow: hidden !important; }
验证该版本是否存在问题
2026-03-11 21:02:55 +08:00
kl
b6dd8129ea refactor: 优化 OFD 表格溢出修复选择器 - 仅覆盖有 inline style 的 SVG
采纳 Copilot 的建议,改用更精准的选择器 #content svg[style="overflow:visible"]。
这样可以只覆盖有 inline style overflow:visible 的 SVG 容器,避免影响其他
已有 overflow:hidden 的元素。保留完整的注释说明问题根因和解决方案。

Ref: Copilot suggestion on PR #723
2026-03-11 20:37:01 +08:00
kl
db5cd68a1e fix: OFD 表格竖线溢出修复 (#723)
cnofd 库渲染 OFD 表格时,为每个元素创建独立 SVG 容器并设置
inline style overflow:visible。当表格中间竖线的 path 元素
y 坐标超过 SVG 容器高度时,线条会溢出到表格底部边框之外。

修复方案:对 #content 下的 SVG 元素强制 overflow:hidden,
使用 !important 覆盖 inline style,精确裁剪超出部分。

Closes #xxx
2026-03-11 20:04:00 +08:00
kl
d1c310ab63 test(e2e): expand archive format coverage (tar/tgz/7z/rar) (#720)
* test(e2e): expand archive coverage to tar/tgz/7z/rar

* test: address copilot archive fixture review feedback

* test(e2e): add rar smoke coverage and align archive deps

* test(e2e): make tgz fixture gzip header deterministic

* test(e2e): keep legacy zip temp dir ignored

* test(e2e): address remaining copilot review comments

* test(e2e): make 7z fixture generation deterministic and strict

* test(e2e): fail fast when sample.rar fixture is missing
2026-03-09 12:44:48 +08:00
kl
b10e14899d test(e2e): unify E2E CI command and align preview URL encoding (#719)
* test(e2e): follow-up fixes for post-merge copilot review feedback

* test(e2e): guard E2E_MAX_PREVIEW_MS against sub-second values

* test(e2e): align preview URL encoding and docs
2026-03-04 17:39:41 +08:00
kl
eee3a2ed38 test(e2e): follow-up fixes after post-merge copilot findings (#718)
* test(e2e): follow-up fixes for post-merge copilot review feedback

* test(e2e): guard E2E_MAX_PREVIEW_MS against sub-second values
2026-03-04 15:45:04 +08:00
kl
68d4d23a4b test(e2e): phase-3 add nightly full run and perf smoke checks (#717)
* test(e2e): phase-3 add nightly workflow and perf smoke suite

* test(e2e): address copilot review for phase-3 fixture and readiness flow
2026-03-04 15:06:15 +08:00
kl
bb457924cd test(e2e): phase-2 add Office and zip smoke automation (#714)
* test(e2e): phase-2 add office and zip smoke coverage

* test(e2e): address copilot review for fixture stability and CI python setup

* test(e2e): fix preflight fixture scope and path handling

* test(e2e): harden fixture preflight and remove duplicate generation

* test(e2e): remove redundant zip install and cleanup temp zip dir

* test(e2e): ensure zip dependency and unify python command in docs

* docs(e2e): align README with npm gen scripts and python3 usage
2026-03-04 14:34:32 +08:00
高雄
9d72706c2d 5.0版本 发布 美化首页 修复压缩包 秘钥问题 修复分页问题 2026-01-26 10:58:50 +08:00
高雄
0aa8de27d4 5.0版本 发布 新增pptm 新增heif 美化heif 模板 美化tif 美化md 优化http方法 优化首页目录读取方法 2026-01-23 15:50:24 +08:00
高雄
b08f4657e5 5.0版本 发布 重构下载方法 修复pdf 占用demo文件夹 2026-01-22 17:17:37 +08:00
高雄
1e04822532 5.0版本 发布 优化下载方法 修复office 重复转换方法错误 2026-01-22 15:41:54 +08:00
高雄
4e5e9b7cba 5.0版本 发布 新增 cadviewer转换方法 2026-01-22 11:53:14 +08:00
高雄
a2fe4658c4 5.0版本 发布 新增 cadviewer转换方法 2026-01-22 11:35:09 +08:00
高雄
e43f10138f 5.0版本 发布 新增 cadviewer转换方法 2026-01-22 11:28:25 +08:00
高雄
7dc0469b30 新增 页码定位 美化前端 其他功能调整等 2026-01-19 17:46:32 +08:00
高雄
904af89190 优化多线程转换方法 添加异步转换提示 2026-01-15 15:43:37 +08:00
高雄
2425bea9b6 优化多线程转换方法 添加异步转换提示 2026-01-15 15:43:13 +08:00
高雄
b5579ae890 tif 优化多线程转换方法 2026-01-09 17:36:37 +08:00
高雄
595f3f55f2 tif 优化多线程转换方法 2026-01-09 17:36:26 +08:00
高雄
c96b7ebd1e tif 优化多线程转换方法 2026-01-09 17:36:11 +08:00
高雄
9a3ec88390 cad pdf转换模块提取独立 优化多线程转换方法 2026-01-09 15:44:02 +08:00
高雄
ea35da6694 更新 3d解析组件 更新pdfjs解析组件 2026-01-06 16:07:29 +08:00
高雄
00a8e094db 更新 3d解析组件 2025-12-31 16:40:19 +08:00
高雄
58d0b24b16 更新 优化ofd 2025-12-31 16:39:08 +08:00
高雄
b20637652a 优化 视频转换模块 2025-12-29 16:40:00 +08:00
高雄
e1e146cb5f 更新 tif组件 修复因jdk 升级tif 转换失败问题 2025-12-29 15:22:14 +08:00
高雄
e52f374252 更新 libreoffice 25 和26 版本识别 2025-12-29 11:51:34 +08:00
高雄
daab5963bc 更新 redis 方法 支持群集 2025-12-29 10:55:17 +08:00
高雄
38a9c142e2 修复编码转换问题 2025-12-29 10:20:59 +08:00
高雄
4afe1caa33 修复压缩包 路径错误问题 2025-12-29 09:42:21 +08:00
高雄
2dd008532a 更新 pom组件版本 新增安全秘钥接口 新增接口关闭 新增encryption接入方法 新增basic接入方法 新增User-Agent接入方法 支持ftp多用户 支持basic多用户 修复file接入协议错误 修复ssl伪证书错误 美化RUL报错提醒 2025-12-25 17:04:18 +08:00
1422 changed files with 579866 additions and 264167 deletions

117
.github/scripts/deploy_windows_winrm.py vendored Normal file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
import base64
import os
import pathlib
import sys
import uuid
import winrm
def require_env(name: str) -> str:
value = os.getenv(name, "").strip()
if not value:
raise SystemExit(f"Missing required environment variable: {name}")
return value
def optional_env(name: str, default: str) -> str:
value = os.getenv(name, "").strip()
return value if value else default
def ps_quote(value: str) -> str:
return value.replace("'", "''")
def main() -> int:
host = require_env("KK_DEPLOY_HOST")
port = optional_env("KK_DEPLOY_PORT", "5985")
username = require_env("KK_DEPLOY_USERNAME")
password = require_env("KK_DEPLOY_PASSWORD")
env_pairs = {
"KK_DEPLOY_ROOT": optional_env("KK_DEPLOY_ROOT", r"C:\kkFileView-5.0"),
"KK_DEPLOY_HEALTH_URL": optional_env("KK_DEPLOY_HEALTH_URL", "http://127.0.0.1:8012/"),
"KK_DEPLOY_REPO_URL": optional_env("KK_DEPLOY_REPO_URL", "https://github.com/kekingcn/kkFileView.git"),
"KK_DEPLOY_BRANCH": optional_env("KK_DEPLOY_BRANCH", "master"),
"KK_DEPLOY_SOURCE_ROOT": optional_env("KK_DEPLOY_SOURCE_ROOT", r"C:\kkFileView-source"),
"KK_DEPLOY_JAVA_HOME": optional_env("KK_DEPLOY_JAVA_HOME", r"C:\Program Files\jdk-21.0.2"),
"KK_DEPLOY_GIT_EXE": optional_env("KK_DEPLOY_GIT_EXE", r"C:\kkFileView-tools\git\cmd\git.exe"),
"KK_DEPLOY_MVN_CMD": optional_env("KK_DEPLOY_MVN_CMD", r"C:\kkFileView-tools\maven\bin\mvn.cmd"),
"KK_DEPLOY_MAVEN_SETTINGS": optional_env("KK_DEPLOY_MAVEN_SETTINGS", ""),
"KK_DEPLOY_DRY_RUN": optional_env("KK_DEPLOY_DRY_RUN", "false").lower(),
}
script_path = pathlib.Path(__file__).with_name("remote_windows_deploy.ps1")
script_body = script_path.read_text(encoding="utf-8")
payload = script_body.encode("utf-8-sig")
payload_b64 = base64.b64encode(payload).decode("ascii")
endpoint = f"http://{host}:{port}/wsman"
session = winrm.Session(endpoint, auth=(username, password), transport="ntlm")
suffix = uuid.uuid4().hex
remote_b64_path = fr"C:\Windows\Temp\kkfileview_deploy_{suffix}.b64"
remote_ps1_path = fr"C:\Windows\Temp\kkfileview_deploy_{suffix}.ps1"
prep = session.run_ps(
f"""
$ErrorActionPreference = 'Stop'
if (Test-Path '{ps_quote(remote_b64_path)}') {{ Remove-Item '{ps_quote(remote_b64_path)}' -Force }}
if (Test-Path '{ps_quote(remote_ps1_path)}') {{ Remove-Item '{ps_quote(remote_ps1_path)}' -Force }}
New-Item -ItemType File -Path '{ps_quote(remote_b64_path)}' -Force | Out-Null
"""
)
if prep.status_code != 0:
sys.stderr.write(prep.std_err.decode("utf-8", errors="ignore"))
return prep.status_code
chunk_size = 1200
for start in range(0, len(payload_b64), chunk_size):
chunk = payload_b64[start : start + chunk_size]
append = session.run_ps(
f"Add-Content -LiteralPath '{ps_quote(remote_b64_path)}' -Value '{chunk}'"
)
if append.status_code != 0:
sys.stderr.write(append.std_err.decode("utf-8", errors="ignore"))
return append.status_code
result = session.run_ps(
f"""
$ErrorActionPreference = 'Stop'
$raw = Get-Content -LiteralPath '{ps_quote(remote_b64_path)}' -Raw
[System.IO.File]::WriteAllBytes('{ps_quote(remote_ps1_path)}', [Convert]::FromBase64String($raw))
try {{
"""
+ "\n".join(
f" $env:{key} = '{ps_quote(value)}'" for key, value in env_pairs.items()
)
+ f"""
powershell -NoProfile -ExecutionPolicy Bypass -File '{ps_quote(remote_ps1_path)}' `
$code = $LASTEXITCODE
}} finally {{
"""
+ "\n".join(
f" Remove-Item Env:{key} -ErrorAction SilentlyContinue" for key in env_pairs
)
+ f"""
Remove-Item '{ps_quote(remote_b64_path)}' -Force -ErrorAction SilentlyContinue
Remove-Item '{ps_quote(remote_ps1_path)}' -Force -ErrorAction SilentlyContinue
}}
exit $code
"""
)
stdout = result.std_out.decode("utf-8", errors="ignore").strip()
stderr = result.std_err.decode("utf-8", errors="ignore").strip()
if stdout:
print(stdout)
if stderr:
print(stderr, file=sys.stderr)
return result.status_code
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,327 @@
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Write-Step {
param([string]$Message)
Write-Host "==> $Message"
}
function Get-RequiredEnv {
param([string]$Name)
$Value = [Environment]::GetEnvironmentVariable($Name)
if ([string]::IsNullOrWhiteSpace($Value)) {
throw "Missing required environment variable: $Name"
}
return $Value
}
function Get-OptionalEnv {
param(
[string]$Name,
[string]$DefaultValue
)
$Value = [Environment]::GetEnvironmentVariable($Name)
if ([string]::IsNullOrWhiteSpace($Value)) {
return $DefaultValue
}
return $Value
}
$DeployRoot = Get-OptionalEnv 'KK_DEPLOY_ROOT' 'C:\kkFileView-5.0'
$HealthUrl = Get-OptionalEnv 'KK_DEPLOY_HEALTH_URL' 'http://127.0.0.1:8012/'
$RepoUrl = Get-OptionalEnv 'KK_DEPLOY_REPO_URL' 'https://github.com/kekingcn/kkFileView.git'
$Branch = Get-OptionalEnv 'KK_DEPLOY_BRANCH' 'master'
$SourceRoot = Get-OptionalEnv 'KK_DEPLOY_SOURCE_ROOT' 'C:\kkFileView-source'
$JavaHome = Get-OptionalEnv 'KK_DEPLOY_JAVA_HOME' 'C:\Program Files\jdk-21.0.2'
$GitExe = Get-OptionalEnv 'KK_DEPLOY_GIT_EXE' 'C:\kkFileView-tools\git\cmd\git.exe'
$MvnCmd = Get-OptionalEnv 'KK_DEPLOY_MVN_CMD' 'C:\kkFileView-tools\maven\bin\mvn.cmd'
$MavenSettings = Get-OptionalEnv 'KK_DEPLOY_MAVEN_SETTINGS' ''
$DryRun = Get-OptionalEnv 'KK_DEPLOY_DRY_RUN' 'false'
$BinDir = Join-Path $DeployRoot 'bin'
$StartupScript = Join-Path $BinDir 'startup.bat'
$ReleaseDir = Join-Path $DeployRoot 'releases'
$DeployTmp = Join-Path $DeployRoot 'deploy-tmp'
$BuildOutputDir = Join-Path (Join-Path $SourceRoot 'server') 'target'
if (-not (Test-Path $DeployRoot)) {
throw "Deploy root not found: $DeployRoot"
}
if (-not (Test-Path $BinDir)) {
throw "Bin directory not found: $BinDir"
}
if (-not (Test-Path $StartupScript)) {
throw "Startup script not found: $StartupScript"
}
$CurrentJar = Get-ChildItem $BinDir -Filter 'kkFileView-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $CurrentJar) {
throw "No kkFileView jar found in $BinDir"
}
$JavaExe = Join-Path $JavaHome 'bin\java.exe'
if (-not (Test-Path $JavaExe)) {
throw "JDK 21 java executable not found: $JavaExe"
}
if (-not (Test-Path $GitExe)) {
throw "Git executable not found: $GitExe"
}
if (-not (Test-Path $MvnCmd)) {
throw "Maven executable not found: $MvnCmd"
}
if (-not [string]::IsNullOrWhiteSpace($MavenSettings) -and -not (Test-Path $MavenSettings)) {
throw "Maven settings file not found: $MavenSettings"
}
$JarName = $CurrentJar.Name
$JarPath = $CurrentJar.FullName
Write-Step "Deploy root: $DeployRoot"
Write-Step "Current jar: $JarPath"
Write-Step "Startup script: $StartupScript"
Write-Step "Health url: $HealthUrl"
Write-Step "Source root: $SourceRoot"
Write-Step "Branch: $Branch"
Write-Step "Git exe: $GitExe"
Write-Step "Maven cmd: $MvnCmd"
Write-Step "Java home: $JavaHome"
if (-not [string]::IsNullOrWhiteSpace($MavenSettings)) {
Write-Step "Maven settings: $MavenSettings"
}
function Invoke-External {
param(
[string]$FilePath,
[string[]]$Arguments,
[string]$WorkingDirectory = $null
)
$previous = $null
if ($WorkingDirectory) {
$previous = Get-Location
Set-Location $WorkingDirectory
}
try {
& $FilePath @Arguments
if ($LASTEXITCODE -ne 0) {
throw "Command failed ($LASTEXITCODE): $FilePath $($Arguments -join ' ')"
}
} finally {
if ($previous) {
Set-Location $previous
}
}
}
function Assert-SafeSourceRoot {
param([string]$PathToCheck)
$FullPath = [System.IO.Path]::GetFullPath($PathToCheck)
$RootPath = [System.IO.Path]::GetPathRoot($FullPath)
if ($FullPath.TrimEnd('\') -eq $RootPath.TrimEnd('\')) {
throw "Refusing to use drive root as source root: $FullPath"
}
$DangerousLeafNames = @(
'Windows',
'Users',
'Program Files',
'Program Files (x86)',
'ProgramData'
)
$LeafName = Split-Path -Leaf $FullPath.TrimEnd('\')
if ($DangerousLeafNames -contains $LeafName) {
throw "Refusing to use a high-risk source root path: $FullPath"
}
}
$env:JAVA_HOME = $JavaHome
$env:Path = (Join-Path $JavaHome 'bin') + ';' + (Split-Path -Parent $GitExe) + ';' + (Split-Path -Parent $MvnCmd) + ';' + $env:Path
Write-Step 'Validating Git executable'
Invoke-External -FilePath $GitExe -Arguments @('--version')
Write-Step 'Validating Maven executable'
$MavenVersionArgs = @('-version')
if (-not [string]::IsNullOrWhiteSpace($MavenSettings)) {
$MavenVersionArgs = @('-s', $MavenSettings, '-version')
}
Invoke-External -FilePath $MvnCmd -Arguments $MavenVersionArgs
if ($DryRun -eq 'true') {
Write-Step "Dry run enabled, remote validation finished"
return
}
New-Item -ItemType Directory -Force -Path $ReleaseDir | Out-Null
New-Item -ItemType Directory -Force -Path $DeployTmp | Out-Null
function Sync-Repository {
Assert-SafeSourceRoot -PathToCheck $SourceRoot
if (-not (Test-Path (Join-Path $SourceRoot '.git'))) {
if (Test-Path $SourceRoot) {
Remove-Item $SourceRoot -Recurse -Force
}
$parent = Split-Path -Parent $SourceRoot
if ($parent) {
New-Item -ItemType Directory -Force -Path $parent | Out-Null
}
Write-Step "Cloning repository from $RepoUrl"
Invoke-External -FilePath $GitExe -Arguments @('clone', '--depth', '1', '--branch', $Branch, '--single-branch', $RepoUrl, $SourceRoot)
return
}
Write-Step "Fetching latest branch state from origin/$Branch"
Invoke-External -FilePath $GitExe -Arguments @('remote', 'set-url', 'origin', $RepoUrl) -WorkingDirectory $SourceRoot
Invoke-External -FilePath $GitExe -Arguments @('fetch', '--prune', '--depth', '1', 'origin', $Branch) -WorkingDirectory $SourceRoot
Invoke-External -FilePath $GitExe -Arguments @('checkout', '-B', $Branch, "origin/$Branch") -WorkingDirectory $SourceRoot
Invoke-External -FilePath $GitExe -Arguments @('reset', '--hard', "origin/$Branch") -WorkingDirectory $SourceRoot
Invoke-External -FilePath $GitExe -Arguments @('clean', '-fd') -WorkingDirectory $SourceRoot
}
function Build-KkFileView {
Write-Step 'Building kkFileView from source'
$BuildArgs = @('-B', 'clean', 'package', '-Dmaven.test.skip=true', '--file', 'pom.xml')
if (-not [string]::IsNullOrWhiteSpace($MavenSettings)) {
$BuildArgs = @('-s', $MavenSettings) + $BuildArgs
}
Invoke-External -FilePath $MvnCmd -Arguments $BuildArgs -WorkingDirectory $SourceRoot
}
Sync-Repository
Build-KkFileView
$DownloadedJars = Get-ChildItem $BuildOutputDir -Filter 'kkFileView-*.jar' -File
if (-not $DownloadedJars) {
throw "No kkFileView jar found in build output: $BuildOutputDir"
}
if ($DownloadedJars.Count -ne 1) {
throw "Expected exactly one kkFileView jar in build output, found $($DownloadedJars.Count)"
}
$DownloadedJar = $DownloadedJars[0]
$Timestamp = Get-Date -Format 'yyyyMMddHHmmss'
$BackupJar = Join-Path $ReleaseDir ("{0}.{1}.bak" -f $JarName, $Timestamp)
function Stop-KkFileView {
foreach ($Process in @(Get-KkFileViewJavaProcesses) + @(Get-KkFileViewLauncherProcesses)) {
Write-Step "Stopping process $($Process.ProcessId)"
Stop-Process -Id $Process.ProcessId -Force -ErrorAction SilentlyContinue
}
}
function Get-KkFileViewJavaProcesses {
$JarPattern = [regex]::Escape($JarName)
return Get-CimInstance Win32_Process | Where-Object {
$_.Name -match '^java(\.exe)?$' -and $_.CommandLine -and $_.CommandLine -match $JarPattern
}
}
function Get-KkFileViewLauncherProcesses {
$StartupPattern = [regex]::Escape([System.IO.Path]::GetFileName($StartupScript))
return Get-CimInstance Win32_Process | Where-Object {
$_.Name -ieq 'cmd.exe' -and $_.CommandLine -and $_.CommandLine -match $StartupPattern
}
}
function Wait-KkFileViewStopped {
param([int]$TimeoutSeconds = 30)
for ($i = 0; $i -lt $TimeoutSeconds; $i++) {
$JavaProcesses = @(Get-KkFileViewJavaProcesses)
$CmdProcesses = @(Get-KkFileViewLauncherProcesses)
if ((@($JavaProcesses).Count + @($CmdProcesses).Count) -eq 0) {
return $true
}
Start-Sleep -Seconds 1
}
return $false
}
function Start-KkFileView {
Write-Step "Starting kkFileView"
$CreateResult = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
CommandLine = ('cmd.exe /c ""' + $StartupScript + '""')
CurrentDirectory = $BinDir
}
if ($CreateResult.ReturnValue -ne 0) {
throw "Failed to start kkFileView launcher, Win32_Process.Create returned $($CreateResult.ReturnValue)"
}
Write-Step "Launcher process created with pid $($CreateResult.ProcessId)"
}
function Wait-Health {
param([string]$Url)
$SuccessfulChecks = 0
for ($i = 0; $i -lt 24; $i++) {
Start-Sleep -Seconds 5
try {
$Response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 5
if ($Response.StatusCode -eq 200 -and @(Get-KkFileViewJavaProcesses).Count -gt 0) {
$SuccessfulChecks++
} else {
$SuccessfulChecks = 0
}
if ($SuccessfulChecks -ge 3) {
return $true
}
} catch {
$SuccessfulChecks = 0
Start-Sleep -Milliseconds 200
}
}
return $false
}
Write-Step "Backing up current jar to $BackupJar"
Copy-Item $JarPath $BackupJar -Force
Stop-KkFileView
if (-not (Wait-KkFileViewStopped)) {
throw "Timed out waiting for the previous kkFileView process to exit"
}
Write-Step "Replacing jar with artifact output"
Copy-Item $DownloadedJar.FullName $JarPath -Force
Start-KkFileView
if (-not (Wait-Health -Url $HealthUrl)) {
Write-Step "Health check failed, rolling back"
Stop-KkFileView
if (-not (Wait-KkFileViewStopped)) {
throw "Timed out waiting for the failed kkFileView process to exit during rollback"
}
Copy-Item $BackupJar $JarPath -Force
Start-KkFileView
if (-not (Wait-Health -Url $HealthUrl)) {
throw "Deployment failed and rollback health check also failed"
}
throw "Deployment failed, rollback completed successfully"
}
Write-Step "Deployment completed successfully"

View File

@@ -22,7 +22,7 @@ jobs:
const owner = context.repo.owner;
const repo = context.repo.repo;
const body = `@copilot 请自动分诊并直接给出可执行建议无需人工先介入\n\n- 先判断类型Bug / Performance / Security / Question / Feature\n- 检查 Issue 信息是否完整版本部署方式复现步骤日志\n- 若信息不完整请直接按模板列出缺失项并引导补充\n- 若信息较完整请给出下一步排查建议与最小复现建议\n- 若判断为已知问题或已修复请给出对应版本/修复方向\n\nIssue #${issue.number}\n标题${issue.title}\n链接${issue.html_url}\n\n---\n\n补充说明 / Support Notice:\n- GitHub Issues 会持续跟进处理 / We will continue to follow up through GitHub Issues.\n- 如为线上紧急问题可通过知识星球渠道加速处理 / For urgent production issues, you can use our Knowledge Planet channel for faster processing:\n https://wx.zsxq.com/group/48844125114258`;
const body = `@Copilot 请自动分诊并直接给出可执行建议无需人工先介入\n\n- 先判断类型Bug / Performance / Security / Question / Feature\n- 检查 Issue 信息是否完整版本部署方式复现步骤日志\n- 若信息不完整请直接按模板列出缺失项并引导补充\n- 若信息较完整请给出下一步排查建议与最小复现建议\n- 若判断为已知问题或已修复请给出对应版本/修复方向\n\nIssue #${issue.number}\n标题${issue.title}\n链接${issue.html_url}\n\n---\n\n补充说明 / Support Notice:\n- GitHub Issues 会持续跟进处理 / We will continue to follow up through GitHub Issues.\n- 如为线上紧急问题可通过知识星球渠道加速处理 / For urgent production issues, you can use our Knowledge Planet channel for faster processing:\n https://wx.zsxq.com/group/48844125114258`;
await github.rest.issues.createComment({
owner,

View File

@@ -0,0 +1,91 @@
name: Manual Release Docker Packages
on:
push:
branches:
- ops/release-v5.0.0-docker
workflow_dispatch:
jobs:
build-docker-archives:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build server package
run: mvn -B -pl server -DskipTests package
- name: Prepare release Dockerfile
run: |
cat > Dockerfile.release <<'EOF'
FROM ubuntu:24.04
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources && \
sed -i 's@//security.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources && \
sed -i 's@//ports.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources && \
apt-get update && \
export DEBIAN_FRONTEND=noninteractive && \
apt-get install -y --no-install-recommends openjdk-21-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui && \
echo 'Asia/Shanghai' > /etc/timezone && \
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 && \
locale-gen zh_CN.UTF-8 && \
apt-get install -y --no-install-recommends ttf-mscorefonts-installer && \
apt-get install -y --no-install-recommends ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
COPY docker/kkfileview-base/fonts/ /usr/share/fonts/chinese/
RUN cd /usr/share/fonts/chinese && \
mkfontscale && \
mkfontdir && \
fc-cache -fv
ENV LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8
ADD server/target/kkFileView-*.tar.gz /opt/
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.0/bin
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.0/config/application.properties","-jar","/opt/kkFileView-5.0.0/bin/kkFileView-5.0.0.jar"]
EOF
- name: Build amd64 docker archive
run: |
mkdir -p dist
docker buildx build \
--platform linux/amd64 \
--provenance=false \
--output type=docker,dest=dist/kkFileView-5.0.0-docker_x64.tar \
-f Dockerfile.release \
.
- name: Build arm64 docker archive
run: |
docker buildx build \
--platform linux/arm64 \
--provenance=false \
--output type=docker,dest=dist/kkFileView-5.0.0-docker_aarch64.tar \
-f Dockerfile.release \
.
- name: Upload docker archives
uses: actions/upload-artifact@v4
with:
name: kkfileview-docker-release
path: dist/kkFileView-5.0.0-docker_*.tar
retention-days: 7

View File

@@ -0,0 +1,52 @@
name: Master Auto Deploy
on:
push:
branches: [ master ]
workflow_dispatch:
concurrency:
group: master-auto-deploy-production
cancel-in-progress: false
permissions:
contents: read
jobs:
deploy-windows:
runs-on: ubuntu-22.04
env:
KK_DEPLOY_HOST: ${{ secrets.KK_DEPLOY_HOST }}
KK_DEPLOY_PORT: ${{ secrets.KK_DEPLOY_PORT }}
KK_DEPLOY_USERNAME: ${{ secrets.KK_DEPLOY_USERNAME }}
KK_DEPLOY_PASSWORD: ${{ secrets.KK_DEPLOY_PASSWORD }}
KK_DEPLOY_ROOT: ${{ secrets.KK_DEPLOY_ROOT }}
KK_DEPLOY_HEALTH_URL: ${{ secrets.KK_DEPLOY_HEALTH_URL }}
KK_DEPLOY_REPO_URL: ${{ vars.KK_DEPLOY_REPO_URL }}
KK_DEPLOY_BRANCH: ${{ vars.KK_DEPLOY_BRANCH }}
KK_DEPLOY_SOURCE_ROOT: ${{ vars.KK_DEPLOY_SOURCE_ROOT }}
KK_DEPLOY_JAVA_HOME: ${{ vars.KK_DEPLOY_JAVA_HOME }}
KK_DEPLOY_GIT_EXE: ${{ vars.KK_DEPLOY_GIT_EXE }}
KK_DEPLOY_MVN_CMD: ${{ vars.KK_DEPLOY_MVN_CMD }}
KK_DEPLOY_MAVEN_SETTINGS: ${{ vars.KK_DEPLOY_MAVEN_SETTINGS }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install WinRM dependencies
run: pip install pywinrm
- name: Validate deploy secrets
run: |
test -n "$KK_DEPLOY_HOST" || (echo "Missing secret: KK_DEPLOY_HOST" && exit 1)
test -n "$KK_DEPLOY_USERNAME" || (echo "Missing secret: KK_DEPLOY_USERNAME" && exit 1)
test -n "$KK_DEPLOY_PASSWORD" || (echo "Missing secret: KK_DEPLOY_PASSWORD" && exit 1)
- name: Deploy to Windows server
run: python .github/scripts/deploy_windows_winrm.py

118
.github/workflows/nightly-e2e.yml vendored Normal file
View File

@@ -0,0 +1,118 @@
name: Nightly E2E Full
on:
schedule:
- cron: '30 18 * * *' # 02:30 Asia/Shanghai
workflow_dispatch:
permissions:
contents: read
jobs:
e2e-nightly:
runs-on: ubuntu-latest
timeout-minutes: 50
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests/e2e/package-lock.json
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install LibreOffice + archive tools
run: |
sudo apt-get update
sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
python -m pip install --upgrade pip
pip install -r tests/e2e/requirements.txt
- name: Build kkFileView
run: mvn -q -pl server -DskipTests package
- name: Install E2E deps
working-directory: tests/e2e
run: |
npm ci
npx playwright install --with-deps chromium
- name: Start fixture server
run: |
cd tests/e2e/fixtures
python3 -m http.server 18080 > /tmp/fixture-server.log 2>&1 &
- name: Start kkFileView
run: |
JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1)
nohup env KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH" > /tmp/kkfileview.log 2>&1 &
- name: Wait for services
run: |
fixture_ready=false
for i in {1..60}; do
if curl -fsS http://127.0.0.1:18080/sample.txt >/dev/null; then
fixture_ready=true
break
fi
sleep 1
done
if [ "$fixture_ready" != "true" ]; then
echo "Error: fixture server did not become ready within 60 seconds." >&2
exit 1
fi
kkfileview_ready=false
for i in {1..120}; do
if curl -fsS http://127.0.0.1:8012/ >/dev/null; then
kkfileview_ready=true
break
fi
sleep 1
done
if [ "$kkfileview_ready" != "true" ]; then
echo "Error: kkFileView service did not become ready within 120 seconds." >&2
exit 1
fi
- name: Run nightly E2E suites
working-directory: tests/e2e
env:
KK_BASE_URL: http://127.0.0.1:8012
FIXTURE_BASE_URL: http://127.0.0.1:18080
E2E_MAX_PREVIEW_MS: 20000
run: npm run test:ci
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: nightly-playwright-report
path: tests/e2e/playwright-report
- name: Upload service logs
if: always()
uses: actions/upload-artifact@v4
with:
name: nightly-e2e-service-logs
path: |
/tmp/kkfileview.log
/tmp/fixture-server.log

View File

@@ -31,10 +31,20 @@ jobs:
cache: 'npm'
cache-dependency-path: tests/e2e/package-lock.json
- name: Install LibreOffice
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install LibreOffice + archive tools
run: |
sudo apt-get update
sudo apt-get install -y libreoffice
sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
python -m pip install --upgrade pip
pip install -r tests/e2e/requirements.txt
- name: Build kkFileView
run: mvn -q -pl server -DskipTests package
@@ -45,9 +55,6 @@ jobs:
npm install
npx playwright install --with-deps chromium
- name: Generate fixtures
run: node tests/e2e/scripts/generate-fixtures.mjs
- name: Start fixture server
run: |
cd tests/e2e/fixtures

1
.gitignore vendored
View File

@@ -26,6 +26,7 @@ nbdist/
### VS Code ###
.vscode/
.DS_Store
.artifacts/
server/src/main/cache/
server/src/main/file/

230
AGENTS.md Normal file
View File

@@ -0,0 +1,230 @@
# AGENTS.md
This document is for coding agents and automation tools working in this repository.
## Project Overview
- Project: `kkFileView`
- Stack: Spring Boot + Freemarker + Redis/Redisson (optional) + JODConverter + front-end preview pages
- Main module: `server`
- Default local URL: `http://127.0.0.1:8012/`
- Production demo: `https://file.kkview.cn/`
This repository is a document preview service. Most user-facing work falls into one of these areas:
1. preview routing and file-type dispatch
2. conversion pipelines for Office / PDF / CAD / archives / images
3. Freemarker preview templates under `server/src/main/resources/web`
4. CI, E2E fixtures, and production deployment automation
## Repository Layout
- `server/`
Main application code, templates, config, packaged artifacts
- `server/src/main/java/cn/keking/`
Core Java application code
- `server/src/main/resources/web/`
Freemarker preview templates
- `server/src/main/resources/static/`
Front-end static assets used by preview pages
- `server/src/main/config/`
Main runtime config files
- `server/src/main/bin/`
Local startup/dev scripts
- `tests/e2e/`
Playwright-based end-to-end tests and fixtures
- `.github/workflows/`
CI and deployment workflows
- `.github/scripts/`
Windows production deployment scripts over WinRM
## Key Entry Points
- App entry:
- `server/src/main/java/cn/keking/ServerMain.java`
- Preview controller:
- `server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java`
- File attribute parsing / request handling:
- `server/src/main/java/cn/keking/service/FileHandlerService.java`
- Office preview flow:
- `server/src/main/java/cn/keking/service/impl/OfficeFilePreviewImpl.java`
- PDF preview flow:
- `server/src/main/java/cn/keking/service/impl/PdfFilePreviewImpl.java`
- Archive extraction:
- `server/src/main/java/cn/keking/service/CompressFileReader.java`
## Important Templates
- `server/src/main/resources/web/compress.ftl`
Archive directory/tree preview page
- `server/src/main/resources/web/pdf.ftl`
PDF preview container page
- `server/src/main/resources/web/picture.ftl`
Single image preview page
- `server/src/main/resources/web/officePicture.ftl`
Office/PDF image-mode preview page
- `server/src/main/resources/web/officeweb.ftl`
Front-end xlsx/html preview page
When debugging UX issues, inspect the exact template selected by the preview flow first. Do not assume two similar preview pages share the same CSS or behavior.
## Local Development
### Recommended dev mode
Use:
```bash
./server/src/main/bin/dev.sh
```
This runs Spring Boot with resource hot reload using:
- `spring-boot:run`
- `-Dspring-boot.run.addResources=true`
- `server/src/main/config/application.properties`
For front-end template or CSS/JS edits, prefer `dev.sh` over rebuilding jars repeatedly.
### Jar build
```bash
mvn -q -pl server -DskipTests package
```
### Main test command used in CI
```bash
mvn -B package -Dmaven.test.skip=true --file pom.xml
```
## Configuration Notes
Primary runtime config used by the scripts and defaults committed in this repository:
- `server/src/main/config/application.properties`
Optional environment-specific config:
- `server/src/main/config/test.properties`
Be careful: the repository defaults point at `application.properties`. If a deployment environment explicitly starts the app with `test.properties`, treat that as an environment-specific override rather than the repository default. Always verify the actual startup command before assuming which config file is active.
Examples of config that commonly affects behavior:
- `office.preview.type`
- `office.preview.switch.disabled`
- `trust.host`
- `not.trust.host`
- `file.upload.disable`
## Preview Behavior Notes
- Office files can render in `pdf` mode or `image` mode.
- PDF preview uses `pdf.ftl`.
- Single images use `picture.ftl`.
- Office image-mode previews use `officePicture.ftl`.
- Archive previews are not simple file lists; they can load nested previews via the archive UI in `compress.ftl`.
When changing preview defaults, verify both:
1. server-side default config
2. front-end mode-switch links/buttons
## Archive Preview Notes
Archive preview is a sensitive area because it combines:
- directory tree generation
- extraction to disk
- nested preview URL construction
- inline iframe loading
If an archive-contained Office file gets stuck on loading:
1. verify the extracted file on disk is not corrupted
2. verify conversion output exists
3. verify the preview template points to the correct generated artifact
4. verify the running Office manager / LibreOffice process is healthy
Do not assume loading forever is a front-end issue.
## Testing
### Targeted Java tests
Example targeted test:
```bash
mvn -q -pl server -Dtest=PdfViewerCompatibilityTests test
```
### E2E tests
See:
- `tests/e2e/README.md`
PR E2E currently covers:
- common preview smoke tests
- Office smoke tests
- archive smoke tests
- basic security and performance checks
## CI / Deployment
### CI
- `maven.yml`
- builds on `push` to `master`
- builds on PRs targeting `master`
- `pr-e2e-mvp.yml`
- runs E2E on PRs to `master`
### Production deployment
- `master-auto-deploy.yml`
- triggers on push to `master`
- deploys to Windows over WinRM
Deployment script:
- `.github/scripts/remote_windows_deploy.ps1`
Important operational detail:
- the committed `bin/startup.bat` in this repo points at `..\config\application.properties`
- if production uses a different config file, treat that as an out-of-repo server override rather than a repository default
If a production config change does not take effect, inspect the actual startup command or deployed `startup.bat` on the server first and verify which config file path it is using.
## Working Conventions For Agents
- Prefer minimal, targeted changes over wide refactors.
- Inspect the active preview template before editing CSS.
- Verify whether behavior is controlled by config, back-end routing, or front-end template logic before changing code.
- For production/debug tasks, distinguish clearly between:
- repository source defaults
- deployed server config
- runtime process arguments
- When changing defaults, mention whether the change affects:
- local dev only
- repository default config
- deployed server config
- existing query-param overrides
## Suggested Validation Checklist
For preview-related changes, validate as many of these as apply:
1. target URL returns `200`
2. selected template is the expected one
3. generated intermediate artifacts exist when required
4. target UI element or style change is actually present in rendered HTML
5. targeted Java test passes
6. relevant E2E path is still compatible
## Non-Goals
This file is not a replacement for user-facing product documentation. Keep it focused on helping coding agents navigate the codebase and make correct changes faster.

View File

@@ -1,4 +1,4 @@
FROM keking/kkfileview-base:4.4.0
FROM keking/kkfileview-base:5.0.0
ADD server/target/kkFileView-*.tar.gz /opt/
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-4.4.0/bin
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-4.4.0/config/application.properties","-jar","/opt/kkFileView-4.4.0/bin/kkFileView-4.4.0.jar"]
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.0/bin
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.0/config/application.properties","-jar","/opt/kkFileView-5.0.0/bin/kkFileView-5.0.0.jar"]

View File

@@ -1,6 +1,6 @@
# kkFileView
文档在线预览项目解决方案项目使用流行的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 办公文档
3. 支持 odt, ods, ots, odp, otp, six, ott, fodt, fods 等OpenOfficeLibreOffice 办公文档
4. 支持 vsd, vsdx Visio 流程图文件
@@ -9,18 +9,18 @@
7. 支持 pdf ,ofd, rtf 等文档
8. 支持 xmind 软件模型文件
9. 支持 bpmn 工作流文件
10. 支持 eml 邮件文件
10. 支持 eml, msg 邮件文件
11. 支持 epub 图书文档
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 模型文件
14. 支持 txt, xml(渲染), xbrl(渲染), md(渲染), java, php, py, js, css 等所有纯文本
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 图信息模型文件
18. 支持 tga 图像格式文件
19. 支持 svg 矢量图像格式文件
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 等医疗数位影像预览
23. 支持 drawio 绘图预览
@@ -149,6 +149,59 @@ pdf预览模式预览效果如下
### 历史更新记录
#### > 2026年04月14日v5.0.0 版本发布
#### 优化内容
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能
2. 图片解析优化 - 改进图片处理机制
3. tif 解析优化 - 增强TIF格式支持
4. svg 解析优化 - 优化SVG矢量图渲染
5. json 解析优化 - 改进JSON文件处理
6. ftp多客户端接入优化 - 提升FTP服务兼容性
7. 首页目录访问优化 - 采用post服务端分页机制
8. marked 解析优化 - 改进Markdown渲染
9. 压缩包预览页重构为单工作区布局支持目录折叠与右侧内嵌预览
10. 优化压缩包内文件类型标识以及单图预览页的展示样式
11. 补充面向工程自动化与编码代理的仓库说明文档
12. 重构演示门户页面包括首页接入说明版本记录与赞助页
#### 新增功能
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自签证书接入问题 - 修复自签名证书兼容性
5. 修复压缩包内 Office 文件在重复解压后被追加写坏导致一直卡在加载中的问题
6. Office 默认预览改为 PDF 模式 PDF 预览默认打开缩略图侧栏
7. 启动脚本改为自动发现当前发布包中的 jar移除过时的硬编码 jar 名称
8. 更新 Docker 与发布辅助文档使其与 5.0.0 发布线保持一致
9. 修复 OFD 表格竖线溢出导致的渲染异常
10. 修复 PDF.js 兼容性补丁避免兼容环境下的预览报错
#### 更新内容
1. JDK版本要求 - 强制要求JDK 21及以上版本
2. pdf前端解析更新 - 升级PDF前端渲染组件
3. odf前端解析更新 - 升级ODF文档前端渲染
4. 3D模型前端解析更新 - 升级3D模型查看器
5. pdf后端异步转换优化 - 实现多线程异步转换
6. tif后端异步转换优化 - 实现多线程异步转换
7. 视频后端异步转换优化 - 实现多线程异步转换
8. CAD后端异步转换优化 - 实现多线程异步转换
9. 默认预览配置策略调整 - Office 预览默认切换为 PDF 模式默认隐藏图片/PDF 模式切换按钮 PDF 预览默认展开缩略图侧栏若升级后仍需保持旧的图片优先体验请显式设置 `office.preview.type=image` `office.preview.switch.disabled=false`
10. 信任域名配置匹配策略扩展 - `trust.host` 及相关规则现已支持通配符和 CIDR 匹配升级后如果你依赖域名/IP 模式匹配需要重新检查白名单和黑名单的实际生效范围
#### > 2025年01月16日v4.4.0 版本发布
### 新增功能
@@ -427,4 +480,3 @@ dcm医疗数位影像 引用于 [dcmjs](https://github.com/dcmjs-org/dcmjs )开
- 本项目诞生于[凯京集团]在取得公司高层同意后以 Apache 协议开源出来反哺社区在此特别感谢凯京集团以及集团领导[@唐老大](https://github.com/tangshd)的支持、@端木详笑的贡献。
- 本项目已脱离公司由[KK开源社区]维护发展壮大感谢所有给 kkFileView Issue Pr 开发者
- 本项目引入的第三方组件已在 '关于引用' 列表列出感谢这些项目 kkFileView 更出色

186
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:
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`.
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`.
@@ -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`.
8. Supports software model files like `xmind`.
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
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.
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.
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`.
18. Supports image format files such as `tga`.
19. Supports vector image format files such as `svg`.
@@ -63,6 +63,186 @@ 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/`.
## Change History
### Version 5.0.0 (April 14, 2026)
#### Improvements
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
9. Redesigned archive preview into a single workspace with a collapsible tree and inline file preview
10. Improved archive preview file-type badges and single-image preview styling
11. Added an agent-focused repository guide for engineering automation and maintenance
12. Refreshed the demo portal pages, including the index, integration guide, release record, and sponsor pages
#### 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
5. Fixed archive-contained Office files that could stay stuck on loading because repeated extraction appended to existing files
6. Default Office preview now prefers PDF mode, and PDF preview opens with the thumbnail sidebar visible by default
7. Updated startup scripts to discover the packaged jar dynamically instead of relying on stale hard-coded jar names
8. Updated Docker and release helper docs to align with the 5.0.0 release line
9. Fixed OFD table border overflow rendering issues
10. Refined the PDF.js compatibility polyfill to avoid preview errors in compatibility environments
#### 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
9. Default preview configuration strategy adjusted - Office preview now defaults to PDF mode, the mode switch is hidden by default, and PDF preview opens with the thumbnail sidebar visible. If you need the previous image-first behavior after upgrade, explicitly set `office.preview.type=image` and `office.preview.switch.disabled=false`.
10. Trust host configuration matching expanded - `trust.host` and related rules now support wildcard and CIDR matching, which may broaden or narrow effective allow/deny behavior after upgrade depending on your patterns
### 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
> December 14, 2022, version 4.1.0 released:

57
doc/ci-auto-deploy.md Normal file
View File

@@ -0,0 +1,57 @@
# kkFileView master 自动部署
当前线上 Windows 服务器的实际部署信息如下
- 部署根目录`C:\kkFileView-5.0`
- 运行 jar`C:\kkFileView-5.0\bin\kkFileView-<当前项目版本>.jar`
- 启动脚本`C:\kkFileView-5.0\bin\startup.bat`
- 运行配置`C:\kkFileView-5.0\config\test.properties`
- 健康检查地址`http://127.0.0.1:8012/`
当前自动部署链路采用服务器拉最新源码并本机编译的方式
1. 通过 WinRM 连接 Windows 服务器
2. 在服务器上的源码目录执行 `git fetch/reset/clean`同步到 `origin/$KK_DEPLOY_BRANCH`默认 `master`
3. 使用服务器上的 JDK 21 Maven 执行 `mvn clean package -Dmaven.test.skip=true`
4. 备份线上 jar替换为新构建产物
5. 使用现有 `startup.bat` 重启并做健康检查
6. 如果健康检查失败则自动回滚旧 jar 并重新拉起
## 需要配置的 GitHub Secrets
- `KK_DEPLOY_HOST`
- `KK_DEPLOY_USERNAME`
- `KK_DEPLOY_PASSWORD`
以下部署参数当前由 workflow GitHub Secrets 读取如果未单独配置则使用脚本默认值
- `KK_DEPLOY_PORT=5985`
- `KK_DEPLOY_ROOT=C:\kkFileView-5.0`
- `KK_DEPLOY_HEALTH_URL=http://127.0.0.1:8012/`
下面这些非敏感参数可以通过 workflow env GitHub Variables 覆盖未配置时会使用默认值
- `KK_DEPLOY_REPO_URL=https://github.com/kekingcn/kkFileView.git`
- `KK_DEPLOY_BRANCH=master`
- `KK_DEPLOY_SOURCE_ROOT=C:\kkFileView-source`
- `KK_DEPLOY_JAVA_HOME=C:\Program Files\jdk-21.0.2`
- `KK_DEPLOY_GIT_EXE=C:\kkFileView-tools\git\cmd\git.exe`
- `KK_DEPLOY_MVN_CMD=C:\kkFileView-tools\maven\bin\mvn.cmd`
- `KK_DEPLOY_MAVEN_SETTINGS=`
如果服务器到 GitHub 的拉取速度不稳定也可以把 `KK_DEPLOY_REPO_URL` 改成你自己的 Git 镜像地址
如果服务器访问 Maven Central 不稳定也可以通过 `KK_DEPLOY_MAVEN_SETTINGS` 指向自定义 `settings.xml`切换到就近镜像仓库
## 服务器前置环境
服务器需要具备以下工具
- Git for Windows推荐安装在 `C:\kkFileView-tools\git`
- Apache Maven 3.9.x推荐安装在 `C:\kkFileView-tools\maven`
- JDK 21当前线上已存在`C:\Program Files\jdk-21.0.2`
## Workflow
新增 workflow`.github/workflows/master-auto-deploy.yml`
- 触发条件`push` `master`或手动 `workflow_dispatch`
- 部署方式WinRM + 服务器源码同步 + 服务器本机 Maven 编译 + jar 替换/回滚

View File

@@ -0,0 +1,44 @@
# E2E 完善清单基于 PR342 回归经验
## 背景
本次手工回归已经验证了以下关键链路
- TXT
- XLSX
- ZIP
- PDF
- DOCX
- MP4
- CAD / DXF
但当前 GitHub CI 自动化 E2E 仅覆盖了其中一部分且大多只断言 HTTP 200没有校验最终预览效果
## 本次落地目标
### 1. 补齐缺失的关键链路
- [x] PDF 预览 smoke
- [x] MP4 预览 smoke
- [x] CAD / DXF 预览 smoke
### 2. 升级断言方式
- [x] 不再只看 `status === 200`
- [x] 增加标题/页面关键字断言确认命中了正确预览模板
- [x] PDF / DOCX / CAD 增加等待页 -> 最终页面的轮询兼容
### 3. 补齐 CI 所需 fixture
- [x] `sample.pdf` 进入 required fixture 清单
- [x] `sample.mp4` 进入 required fixture 清单
- [x] `text.dxf` 进入 required fixture 清单
- [x] MP4 DXF fixture 作为仓库内静态样例纳入 CI
### 4. 后续可继续增强本次未全部落地
- [ ] PDF / Office / CAD 增加截图型 nightly artifact
- [ ] `/picturesPreview` 增加独立 smoke
- [ ] OFD 增加稳定 fixture smoke case
- [ ] 为媒体预览增加更多格式 wav / mp3 / mov
- [ ] CAD 增加第二份标准样例避免单样例偏差
- [ ] 将当前 HTML 测试报告模板收敛成 nightly 自动产物
## 预期收益
- CI 覆盖这次 PR342 真正验证过的关键主链路
- 避免未来出现CI 绿了 PDF / MP4 / CAD 实际挂了的情况
- E2E 更接近用户真实感知而不是仅验证接口可达

View File

@@ -7,10 +7,10 @@
然后使用 kkfileview-base 作为基础镜像进行构建加快 kkfileview docker 镜像构建与发布
执行如下命令即可构建基础镜像
> 这里镜像 tag 4.4.0 为例本项目所维护的 Dockerfile 文件考虑了跨平台兼容性 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
> 这里镜像 tag 5.0.0 为例本项目所维护的 Dockerfile 文件考虑了跨平台兼容性 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
```shell
docker build --tag keking/kkfileview-base:4.4.0 .
docker build --tag keking/kkfileview-base:5.0.0 .
```
@@ -46,5 +46,5 @@ docker build --tag keking/kkfileview-base:4.4.0 .
现在就可以愉快地开始构建了构建命令示例:
```shell
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:5.0.0 --push .
```

View File

@@ -8,10 +8,10 @@ Then, use kkfileview-base as the base image to build and speed up the kkfileview
To build the base image, run the following command:
> In this example, the image tag is 4.4.0. The Dockerfile maintained in this project considers cross-platform compatibility. If you need an arm64 architecture image, run the same build command on an arm64 architecture machine.
> In this example, the image tag is 5.0.0. The Dockerfile maintained in this project considers cross-platform compatibility. If you need an arm64 architecture image, run the same build command on an arm64 architecture machine.
```shell
docker build --tag keking/kkfileview-base:4.4.0 .
docker build --tag keking/kkfileview-base:5.0.0 .
```
@@ -49,5 +49,5 @@ Assuming the current machine is amd64 (x86_64) architecture, you'll need to enab
Now you can enjoy the building. Heres an example build command:
```shell
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:5.0.0 --push .
```

90
pom.xml
View File

@@ -6,54 +6,75 @@
<groupId>cn.keking</groupId>
<artifactId>kkFileView-parent</artifactId>
<version>4.4.0</version>
<version>5.0.0</version>
<properties>
<!-- ========== Java 和编译配置 ========== -->
<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.release>${java.version}</maven.compiler.release>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<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>
<modules>
<module>server</module>
</modules>
<!-- ========== 项目信息 ========== -->
<name>kkFileView-parent</name>
<description>专注文件在线预览服务</description>
<url>https://github.com/kekingcn/kkFileView</url>
@@ -89,5 +110,4 @@
<system>github</system>
<url>https://github.com/kekingcn/kkFileView/issues</url>
</issueManagement>
</project>

View File

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

16
server/src/main/bin/dev.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
set -e
ROOT_DIR=$(cd "$(dirname "$0")/../../../.." || exit 1; pwd)
SERVER_DIR="$ROOT_DIR/server"
if [ -n "$JAVA_HOME" ]; then
export PATH="$JAVA_HOME/bin:$PATH"
fi
cd "$SERVER_DIR" || exit 1
mvn spring-boot:run \
-Dspring-boot.run.addResources=true \
-Dspring-boot.run.jvmArguments="-Dfile.encoding=UTF-8 -Dspring.config.location=$SERVER_DIR/src/main/config/application.properties"

View File

@@ -1,10 +1,20 @@
@echo off
set "KKFILEVIEW_BIN_FOLDER=%cd%"
cd "%KKFILEVIEW_BIN_FOLDER%"
set "JAR_NAME="
for %%F in (kkFileView-*.jar) do (
set "JAR_NAME=%%~nxF"
goto :jar_found
)
echo Error: kkFileView jar not found in %KKFILEVIEW_BIN_FOLDER%
exit /b 1
:jar_found
echo Using KKFILEVIEW_BIN_FOLDER %KKFILEVIEW_BIN_FOLDER%
echo Using JAR_NAME %JAR_NAME%
echo Starting kkFileView...
echo Please check log file in ../log/kkFileView.log for more information
echo You can get help in our official home site: https://kkview.cn
echo If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ
echo If this project is helpful to you, please star it on https://gitee.com/kekingcn/file-online-preview/stargazers
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0.jar -> ..\log\kkFileView.log
java -Dspring.config.location=..\config\application.properties -jar "%JAR_NAME%" > ..\log\kkFileView.log 2>&1

View File

@@ -49,9 +49,16 @@ else
fi
fi
JAR_PATH=$(ls kkFileView-*.jar 2>/dev/null | head -n 1)
if [ -z "${JAR_PATH}" ]; then
echo "kkFileView jar not found in ${KKFILEVIEW_BIN_FOLDER}"
exit 1
fi
## 启动kkFileView
echo "Starting kkFileView..."
nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -jar kkFileView-4.4.0.jar > ../log/kkFileView.log 2>&1 &
echo "Using jar ${JAR_PATH}"
nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -jar "${JAR_PATH}" > ../log/kkFileView.log 2>&1 &
echo "Please execute ./showlog.sh to check log for more information"
echo "You can get help in our official home site: https://kkview.cn"
echo "If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ"

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -59,26 +59,31 @@ public class CompressFileReader {
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (!item.isFolder()) {
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
ExtractOperationResult result = item.extractSlow(data -> {
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), true))) {
out.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
return data.length;
}, filePassword);
if (result != ExtractOperationResult.OK) {
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
if (result1.equals(result)) {
throw new Exception("Password");
}else {
throw new Exception("Failed to extract RAR file.");
Files.deleteIfExists(filePathInsideArchive);
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), false))) {
ExtractOperationResult result = item.extractSlow(data -> {
try {
out.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
return data.length;
}, filePassword);
if (result != ExtractOperationResult.OK) {
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
if (result1.equals(result)) {
throw new Exception("Password");
} else {
throw new Exception("Failed to extract RAR file.");
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), "UTF-8"));
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), StandardCharsets.UTF_8).replaceAll("%2F", "/"));
}
}
}
@@ -110,4 +115,4 @@ public class CompressFileReader {
}
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,24 +6,25 @@ import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.service.Mediatomp4Service;
import cn.keking.utils.DownloadUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import cn.keking.utils.FileConvertStatusManager;
import cn.keking.utils.KkFileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author : kl
* @authorboke : kailing.pub
* @create : 2018-03-25 上午11:58
* @description:
* @description: 异步视频文件预览实现
**/
@Service
public class MediaFilePreviewImpl implements FilePreview {
@@ -31,11 +32,20 @@ public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private static final String mp4 = "mp4";
private final Mediatomp4Service mediatomp4Service;
private final OfficeFilePreviewImpl officefilepreviewimpl;
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
// 用于处理回调的线程池
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
public MediaFilePreviewImpl(FileHandlerService fileHandlerService,
OtherFilePreviewImpl otherFilePreview,
Mediatomp4Service mediatomp4Service,
OfficeFilePreviewImpl officefilepreviewimpl) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
this.mediatomp4Service = mediatomp4Service;
this.officefilepreviewimpl = officefilepreviewimpl;
}
@Override
@@ -46,122 +56,140 @@ public class MediaFilePreviewImpl implements FilePreview {
String outFilePath = fileAttribute.getOutFilePath();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
FileType type = fileAttribute.getType();
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
// 检查是否是需要转换的视频格式
boolean mediaTypes = false;
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES;
for (String temp : mediaTypesConvert) {
if (suffix.equals(temp)) {
if (suffix.equalsIgnoreCase(temp)) {
mediaTypes = true;
break;
}
}
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
// 查询转换状态
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
if (statusResult != null) {
return statusResult;
}
// 非HTTP协议或需要转换的文件
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) {
// 检查缓存
File outputFile = new File(outFilePath);
if (outputFile.exists() && !forceUpdatedCache && ConfigConstants.isCacheEnabled()) {
String relativePath = fileHandlerService.getRelativePath(outFilePath);
if (fileHandlerService.listConvertedMedias().containsKey(cacheName)) {
model.addAttribute("mediaUrl", relativePath);
return MEDIA_FILE_PREVIEW_PAGE;
}
}
// 下载文件
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
if (mediaTypes) {
// 检查文件大小限制
if (isFileSizeExceeded(filePath)) {
return otherFilePreview.notSupportedFile(model, fileAttribute,
"视频文件大小超过" + ConfigConstants.getMediaConvertMaxSize() + "MB限制禁止转换");
}
String filePath = response.getContent();
String convertedUrl = null;
try {
if (mediaTypes) {
convertedUrl = convertToMp4(filePath, outFilePath, fileAttribute);
} else {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
}
// 启动异步转换,并添加回调处理
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
int refreshSchedule = ConfigConstants.getTime();
// 返回等待页面
model.addAttribute("fileName", fileName);
model.addAttribute("time", refreshSchedule);
model.addAttribute("message", "视频文件正在转换中,请稍候...");
return WAITING_FILE_PREVIEW_PAGE;
} catch (Exception e) {
logger.error("Failed to convert media file: {}", filePath, e);
}
if (convertedUrl == null) {
logger.error("Failed to start video conversion: {}", filePath, e);
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
}
if (ConfigConstants.isCacheEnabled()) {
// 加入缓存
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
}
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
} else {
model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName));
// 不需要转换的文件,直接返回
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
return MEDIA_FILE_PREVIEW_PAGE;
}
return MEDIA_FILE_PREVIEW_PAGE;
}
if (type.equals(FileType.MEDIA)) { // 支持输出 只限默认格式
// HTTP协议的媒体文件直接播放
if (type.equals(FileType.MEDIA)) {
model.addAttribute("mediaUrl", url);
return MEDIA_FILE_PREVIEW_PAGE;
}
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
}
/**
* 检查视频文件转换是否已开启,以及当前文件是否需要转换
*
* @return
* 启动异步转换,并在转换完成后处理后续操作
*/
private void startAsyncConversion(String filePath, String outFilePath,
String cacheName, FileAttribute fileAttribute) {
// 启动异步转换
CompletableFuture<Boolean> conversionFuture = mediatomp4Service.convertToMp4Async(
filePath,
outFilePath,
cacheName,
fileAttribute
);
// 添加转换完成后的回调
conversionFuture.whenCompleteAsync((success, throwable) -> {
if (success != null && success) {
try {
// 1. 是否保留源文件(只在转换成功后才删除)
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
KkFileUtils.deleteFileByPath(filePath);
}
// 2. 加入视频缓存(只在转换成功后才添加)
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedMedias(cacheName,
fileHandlerService.getRelativePath(outFilePath));
}
} catch (Exception e) {
logger.error("视频转换后续处理失败: {}", filePath, e);
}
} else {
// 转换失败,保留源文件供排查问题
logger.error("视频转换失败,保留源文件: {}", filePath);
if (throwable != null) {
logger.error("视频转换失败原因: ", throwable);
}
}
}, callbackExecutor);
}
/**
* 检查文件大小是否超过限制
*/
private boolean isFileSizeExceeded(String filePath) {
try {
File inputFile = new File(filePath);
if (inputFile.exists()) {
long fileSizeMB = inputFile.length() / (1024 * 1024);
int maxSizeMB = ConfigConstants.getMediaConvertMaxSize();
if (fileSizeMB > maxSizeMB) {
logger.warn("视频文件大小超过限制: {}MB > {}MB", fileSizeMB, maxSizeMB);
return true;
}
}
} catch (Exception e) {
logger.error("检查文件大小时出错: {}", filePath, e);
}
return false;
}
/**
* 检查是否需要转换
*/
private boolean checkNeedConvert(boolean mediaTypes) {
//1.检查开关是否开启
// 检查转换开关是否开启
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
return mediaTypes;
}
return false;
}
private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception {
FFmpegFrameGrabber frameGrabber = FFmpegFrameGrabber.createDefault(filePath);
Frame captured_frame;
FFmpegFrameRecorder recorder = null;
try {
File desFile = new File(outFilePath);
//判断一下防止重复转换
if (desFile.exists()) {
return outFilePath;
}
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
String folder = outFilePath.substring(0, index);
File path = new File(folder);
//目录不存在 创建新的目录
if (!path.exists()) {
path.mkdirs();
}
}
frameGrabber.start();
recorder = new FFmpegFrameRecorder(outFilePath, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
// recorder.setImageHeight(640);
// recorder.setImageWidth(480);
recorder.setFormat(mp4);
recorder.setFrameRate(frameGrabber.getFrameRate());
recorder.setSampleRate(frameGrabber.getSampleRate());
//视频编码属性配置 H.264 H.265 MPEG
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频比特率,单位:b
recorder.setVideoBitrate(frameGrabber.getVideoBitrate());
recorder.setAspectRatio(frameGrabber.getAspectRatio());
// 设置音频通用编码格式
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
//设置音频比特率,单位:b (比特率越高,清晰度/音质越好,当然文件也就越大 128000 = 182kb)
recorder.setAudioBitrate(frameGrabber.getAudioBitrate());
recorder.setAudioOptions(frameGrabber.getAudioOptions());
recorder.setAudioChannels(frameGrabber.getAudioChannels());
recorder.start();
while (true) {
captured_frame = frameGrabber.grabFrame();
if (captured_frame == null) {
System.out.println("转码完成:" + filePath);
break;
}
recorder.record(captured_frame);
}
} catch (Exception e) {
logger.error("Failed to convert video file to mp4: {}", filePath, e);
return null;
} finally {
if (recorder != null) { //关闭
recorder.stop();
recorder.close();
}
frameGrabber.stop();
frameGrabber.close();
}
return outFilePath;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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