mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-28 11:06:43 +00:00
Compare commits
70 Commits
copilot/su
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
634babfba4 | ||
|
|
e7fe1afe19 | ||
|
|
171762d676 | ||
|
|
76e091900b | ||
|
|
bfa4ceab90 | ||
|
|
b18cfa797a | ||
|
|
8a117a41e8 | ||
|
|
17ba41320e | ||
|
|
476c0bfefc | ||
|
|
1c6691d785 | ||
|
|
36ae290cb6 | ||
|
|
597715ce33 | ||
|
|
a8a08c1dcc | ||
|
|
7757729efd | ||
|
|
b246bfdac7 | ||
|
|
d35393ba22 | ||
|
|
c893dd7095 | ||
|
|
9bdb18d833 | ||
|
|
58fc1af74f | ||
|
|
1b3cf33bf0 | ||
|
|
c9005d0c04 | ||
|
|
37bda20d08 | ||
|
|
1819861647 | ||
|
|
352b86b40d | ||
|
|
853ad0154f | ||
|
|
c88bf04a0d | ||
|
|
6a84e61ecb | ||
|
|
dd6e369e6a | ||
|
|
bd20546b6d | ||
|
|
c41c14bf3c | ||
|
|
3fbf0156e2 | ||
|
|
530304b832 | ||
|
|
5499150395 | ||
|
|
2dcc62f3da | ||
|
|
405a12ef07 | ||
|
|
bb0734b3d3 | ||
|
|
a9e394950f | ||
|
|
be6023701c | ||
|
|
02bcd35779 | ||
|
|
b6dd8129ea | ||
|
|
db5cd68a1e | ||
|
|
d1c310ab63 | ||
|
|
b10e14899d | ||
|
|
eee3a2ed38 | ||
|
|
68d4d23a4b | ||
|
|
bb457924cd | ||
|
|
9d72706c2d | ||
|
|
0aa8de27d4 | ||
|
|
b08f4657e5 | ||
|
|
1e04822532 | ||
|
|
4e5e9b7cba | ||
|
|
a2fe4658c4 | ||
|
|
e43f10138f | ||
|
|
7dc0469b30 | ||
|
|
904af89190 | ||
|
|
2425bea9b6 | ||
|
|
b5579ae890 | ||
|
|
595f3f55f2 | ||
|
|
c96b7ebd1e | ||
|
|
9a3ec88390 | ||
|
|
ea35da6694 | ||
|
|
00a8e094db | ||
|
|
58d0b24b16 | ||
|
|
b20637652a | ||
|
|
e1e146cb5f | ||
|
|
e52f374252 | ||
|
|
daab5963bc | ||
|
|
38a9c142e2 | ||
|
|
4afe1caa33 | ||
|
|
2dd008532a |
117
.github/scripts/deploy_windows_winrm.py
vendored
Normal file
117
.github/scripts/deploy_windows_winrm.py
vendored
Normal 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())
|
||||||
327
.github/scripts/remote_windows_deploy.ps1
vendored
Normal file
327
.github/scripts/remote_windows_deploy.ps1
vendored
Normal 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"
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
const repo = context.repo.repo;
|
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({
|
await github.rest.issues.createComment({
|
||||||
owner,
|
owner,
|
||||||
|
|||||||
52
.github/workflows/master-auto-deploy.yml
vendored
Normal file
52
.github/workflows/master-auto-deploy.yml
vendored
Normal 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
118
.github/workflows/nightly-e2e.yml
vendored
Normal 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
|
||||||
17
.github/workflows/pr-e2e-mvp.yml
vendored
17
.github/workflows/pr-e2e-mvp.yml
vendored
@@ -31,10 +31,20 @@ jobs:
|
|||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: tests/e2e/package-lock.json
|
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: |
|
run: |
|
||||||
sudo apt-get update
|
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
|
- name: Build kkFileView
|
||||||
run: mvn -q -pl server -DskipTests package
|
run: mvn -q -pl server -DskipTests package
|
||||||
@@ -45,9 +55,6 @@ jobs:
|
|||||||
npm install
|
npm install
|
||||||
npx playwright install --with-deps chromium
|
npx playwright install --with-deps chromium
|
||||||
|
|
||||||
- name: Generate fixtures
|
|
||||||
run: node tests/e2e/scripts/generate-fixtures.mjs
|
|
||||||
|
|
||||||
- name: Start fixture server
|
- name: Start fixture server
|
||||||
run: |
|
run: |
|
||||||
cd tests/e2e/fixtures
|
cd tests/e2e/fixtures
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ nbdist/
|
|||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.artifacts/
|
||||||
|
|
||||||
server/src/main/cache/
|
server/src/main/cache/
|
||||||
server/src/main/file/
|
server/src/main/file/
|
||||||
|
|||||||
230
AGENTS.md
Normal file
230
AGENTS.md
Normal 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.
|
||||||
@@ -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/
|
ADD server/target/kkFileView-*.tar.gz /opt/
|
||||||
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-4.4.0/bin
|
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.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"]
|
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"]
|
||||||
|
|||||||
62
README.cn.md
62
README.cn.md
@@ -1,6 +1,6 @@
|
|||||||
# kkFileView
|
# kkFileView
|
||||||
文档在线预览项目解决方案,项目使用流行的spring boot搭建,易上手和部署。万能的文件预览开源项目,基本支持主流文档格式预览,如:
|
文档在线预览项目解决方案,项目使用流行的spring boot搭建,易上手和部署。万能的文件预览开源项目,基本支持主流文档格式预览,如:
|
||||||
1. 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla ,pages 等 Office 办公文档
|
1. 支持 doc, docx, xls, xlsx, xlsm, ppt, pptx, csv, tsv, dotm, xlt, xltm, dot, dotx,xlam, xla ,pages ,pptm 等 Office 办公文档
|
||||||
2. 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档
|
2. 支持 wps, dps, et, ett, wpt 等国产 WPS Office 办公文档
|
||||||
3. 支持 odt, ods, ots, odp, otp, six, ott, fodt, fods 等OpenOffice、LibreOffice 办公文档
|
3. 支持 odt, ods, ots, odp, otp, six, ott, fodt, fods 等OpenOffice、LibreOffice 办公文档
|
||||||
4. 支持 vsd, vsdx 等 Visio 流程图文件
|
4. 支持 vsd, vsdx 等 Visio 流程图文件
|
||||||
@@ -9,18 +9,18 @@
|
|||||||
7. 支持 pdf ,ofd, rtf 等文档
|
7. 支持 pdf ,ofd, rtf 等文档
|
||||||
8. 支持 xmind 软件模型文件
|
8. 支持 xmind 软件模型文件
|
||||||
9. 支持 bpmn 工作流文件
|
9. 支持 bpmn 工作流文件
|
||||||
10. 支持 eml 邮件文件
|
10. 支持 eml, msg 邮件文件
|
||||||
11. 支持 epub 图书文档
|
11. 支持 epub 图书文档
|
||||||
12. 支持 obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim 等 3D 模型文件
|
12. 支持 obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim 等 3D 模型文件
|
||||||
13. 支持 dwg, dxf, dwf, iges , igs, dwt, dng, ifc, dwfx, stl, cf2, plt 等 CAD 模型文件
|
13. 支持 dwg, dxf, dwf, iges , igs, dwt, dng, ifc, dwfx, stl, cf2, plt 等 CAD 模型文件
|
||||||
14. 支持 txt, xml(渲染), xbrl(渲染), md(渲染), java, php, py, js, css 等所有纯文本
|
14. 支持 txt, xml(渲染), xbrl(渲染), md(渲染), java, php, py, js, css 等所有纯文本
|
||||||
15. 支持 zip, rar, jar, tar, gzip, 7z 等压缩包
|
15. 支持 zip, rar, jar, tar, gzip, 7z 等压缩包
|
||||||
16. 支持 jpg, jpeg, png, gif, bmp, ico, jfif, webp 等图片预览(翻转,缩放,镜像)
|
16. 支持 jpg, jpeg, png, gif, bmp, ico, jfif, webp ,heic ,heif等图片预览(翻转,缩放,镜像)
|
||||||
17. 支持 tif, tiff 图信息模型文件
|
17. 支持 tif, tiff 图信息模型文件
|
||||||
18. 支持 tga 图像格式文件
|
18. 支持 tga 图像格式文件
|
||||||
19. 支持 svg 矢量图像格式文件
|
19. 支持 svg 矢量图像格式文件
|
||||||
20. 支持 mp3,wav,mp4,flv 等音视频格式文件
|
20. 支持 mp3,wav,mp4,flv 等音视频格式文件
|
||||||
21. 支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts,swf 等视频格式转码预览
|
21. 支持 avi,mov,rm,webm,ts,rm,mkv,mpeg,ogg,mpg,rmvb,wmv,3gp,ts 等视频格式转码预览
|
||||||
22. 支持 dcm 等医疗数位影像预览
|
22. 支持 dcm 等医疗数位影像预览
|
||||||
23. 支持 drawio 绘图预览
|
23. 支持 drawio 绘图预览
|
||||||
|
|
||||||
@@ -149,6 +149,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 版本发布 :
|
#### > 2025年01月16日,v4.4.0 版本发布 :
|
||||||
|
|
||||||
### 新增功能
|
### 新增功能
|
||||||
@@ -427,4 +480,3 @@ dcm医疗数位影像 引用于 [dcmjs](https://github.com/dcmjs-org/dcmjs )开
|
|||||||
- 本项目诞生于[凯京集团],在取得公司高层同意后以 Apache 协议开源出来反哺社区,在此特别感谢凯京集团,以及集团领导[@唐老大](https://github.com/tangshd)的支持、@端木详笑的贡献。
|
- 本项目诞生于[凯京集团],在取得公司高层同意后以 Apache 协议开源出来反哺社区,在此特别感谢凯京集团,以及集团领导[@唐老大](https://github.com/tangshd)的支持、@端木详笑的贡献。
|
||||||
- 本项目已脱离公司由[KK开源社区]维护发展壮大,感谢所有给 kkFileView 提 Issue 、Pr 开发者
|
- 本项目已脱离公司由[KK开源社区]维护发展壮大,感谢所有给 kkFileView 提 Issue 、Pr 开发者
|
||||||
- 本项目引入的第三方组件已在 '关于引用' 列表列出,感谢这些项目,让 kkFileView 更出色
|
- 本项目引入的第三方组件已在 '关于引用' 列表列出,感谢这些项目,让 kkFileView 更出色
|
||||||
|
|
||||||
|
|||||||
186
README.md
186
README.md
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
Document online preview project solution, built using the popular Spring Boot framework for easy setup and deployment. This versatile open source project provides basic support for a wide range of document formats, including:
|
Document online preview project solution, built using the popular Spring Boot framework for easy setup and deployment. This versatile open source project provides basic support for a wide range of document formats, including:
|
||||||
|
|
||||||
1. Supports Office documents such as `doc`, `docx`, `xls`, `xlsx`, `xlsm`, `ppt`, `pptx`, `csv`, `tsv`, , `dotm`, `xlt`, `xltm`, `dot`, `xlam`, `dotx`, `xla,` ,`pages` etc.
|
1. Supports Office documents such as `doc`, `docx`, `xls`, `xlsx`, `xlsm`, `ppt`, `pptx`, `csv`, `tsv`, , `dotm`, `xlt`, `xltm`, `dot`, `xlam`, `dotx`, `xla,` ,`pages` ,`pptm` etc.
|
||||||
2. Supports domestic WPS Office documents such as `wps`, `dps`, `et` , `ett`, ` wpt`.
|
2. Supports domestic WPS Office documents such as `wps`, `dps`, `et` , `ett`, ` wpt`.
|
||||||
3. Supports OpenOffice, LibreOffice office documents such as `odt`, `ods`, `ots`, `odp`, `otp`, `six`, `ott`, `fodt` and `fods`.
|
3. Supports OpenOffice, LibreOffice office documents such as `odt`, `ods`, `ots`, `odp`, `otp`, `six`, `ott`, `fodt` and `fods`.
|
||||||
4. Supports Visio flowchart files such as `vsd`, `vsdx`.
|
4. Supports Visio flowchart files such as `vsd`, `vsdx`.
|
||||||
@@ -13,13 +13,13 @@ Document online preview project solution, built using the popular Spring Boot fr
|
|||||||
7. Supports document formats like `pdf`, `ofd`, and `rtf`.
|
7. Supports document formats like `pdf`, `ofd`, and `rtf`.
|
||||||
8. Supports software model files like `xmind`.
|
8. Supports software model files like `xmind`.
|
||||||
9. Support for `bpmn` workflow files.
|
9. Support for `bpmn` workflow files.
|
||||||
10. Support for `eml` mail files
|
10. Support for `eml` , `msg` mail files
|
||||||
11. Support for `epub` book documents
|
11. Support for `epub` book documents
|
||||||
12. Supports 3D model files like `obj`, `3ds`, `stl`, `ply`, `gltf`, `glb`, `off`, `3dm`, `fbx`, `dae`, `wrl`, `3mf`, `ifc`, `brep`, `step`, `iges`, `fcstd`, `bim`, etc.
|
12. Supports 3D model files like `obj`, `3ds`, `stl`, `ply`, `gltf`, `glb`, `off`, `3dm`, `fbx`, `dae`, `wrl`, `3mf`, `ifc`, `brep`, `step`, `iges`, `fcstd`, `bim`, etc.
|
||||||
13. Supports CAD model files such as `dwg`, `dxf`, `dwf` `iges` ,` igs`, `dwt` , `dng` , `ifc` , `dwfx` , `stl` , `cf2` , `plt`, etc.
|
13. Supports CAD model files such as `dwg`, `dxf`, `dwf` `iges` ,` igs`, `dwt` , `dng` , `ifc` , `dwfx` , `stl` , `cf2` , `plt`, etc.
|
||||||
14. Supports all plain text files such as `txt`, `xml` (rendering), `md` (rendering), `java`, `php`, `py`, `js`, `css`, etc.
|
14. Supports all plain text files such as `txt`, `xml` (rendering), `md` (rendering), `java`, `php`, `py`, `js`, `css`, etc.
|
||||||
15. Supports compressed packages such as `zip`, `rar`, `jar`, `tar`, `gzip`, `7z`, etc.
|
15. Supports compressed packages such as `zip`, `rar`, `jar`, `tar`, `gzip`, `7z`, etc.
|
||||||
16. Supports image previewing (flip, zoom, mirror) of `jpg`, `jpeg`, `png`, `gif`, `bmp`, `ico`, `jfif`, `webp`, etc.
|
16. Supports image previewing (flip, zoom, mirror) of `jpg`, `jpeg`, `png`, `gif`, `bmp`, `ico`, `jfif`, `webp`, `heic`, ,`heif` etc.
|
||||||
17. Supports image information model files such as `tif` and `tiff`.
|
17. Supports image information model files such as `tif` and `tiff`.
|
||||||
18. Supports image format files such as `tga`.
|
18. Supports image format files such as `tga`.
|
||||||
19. Supports vector image format files such as `svg`.
|
19. Supports vector image format files such as `svg`.
|
||||||
@@ -63,6 +63,186 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
|||||||
|
|
||||||
2. second step:Run the main method of `/server/src/main/java/cn/keking/ServerMain.java`. After starting,visit `http://localhost:8012/`.
|
2. second step:Run 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
|
### Changelog
|
||||||
> December 14, 2022, version 4.1.0 released:
|
> December 14, 2022, version 4.1.0 released:
|
||||||
|
|
||||||
|
|||||||
57
doc/ci-auto-deploy.md
Normal file
57
doc/ci-auto-deploy.md
Normal 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 替换/回滚
|
||||||
44
doc/e2e-improvement-checklist.md
Normal file
44
doc/e2e-improvement-checklist.md
Normal 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 更接近用户真实感知,而不是仅验证接口可达
|
||||||
@@ -7,10 +7,10 @@
|
|||||||
然后使用 kkfileview-base 作为基础镜像进行构建,加快 kkfileview docker 镜像构建与发布。
|
然后使用 kkfileview-base 作为基础镜像进行构建,加快 kkfileview docker 镜像构建与发布。
|
||||||
|
|
||||||
执行如下命令即可构建基础镜像:
|
执行如下命令即可构建基础镜像:
|
||||||
> 这里镜像 tag 以 4.4.0 为例,本项目所维护的 Dockerfile 文件考虑了跨平台兼容性。 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
|
> 这里镜像 tag 以 5.0.0 为例,本项目所维护的 Dockerfile 文件考虑了跨平台兼容性。 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
|
||||||
|
|
||||||
```shell
|
```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
|
```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 .
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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:
|
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
|
```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. Here’s an example build command:
|
Now you can enjoy the building. Here’s an example build command:
|
||||||
|
|
||||||
```shell
|
```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
90
pom.xml
@@ -6,54 +6,75 @@
|
|||||||
|
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<version>4.4.0</version>
|
<version>5.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<!-- ========== Java 和编译配置 ========== -->
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<jodconverter.version>4.4.6</jodconverter.version>
|
|
||||||
<spring.boot.version>3.5.6</spring.boot.version>
|
|
||||||
<poi.version>5.2.2</poi.version>
|
|
||||||
<xdocreport.version>1.0.6</xdocreport.version>
|
|
||||||
<xstream.version>1.4.20</xstream.version>
|
|
||||||
<junrar.version>7.5.5</junrar.version>
|
|
||||||
<redisson.version>3.22.0</redisson.version>
|
|
||||||
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
|
|
||||||
<jchardet.version>1.0</jchardet.version>
|
|
||||||
<antlr.version>2.7.7</antlr.version>
|
|
||||||
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
|
|
||||||
<rocksdb.version>5.17.2</rocksdb.version>
|
|
||||||
<pdfbox.version>3.0.2</pdfbox.version>
|
|
||||||
<jai-imageio.version>1.4.0</jai-imageio.version>
|
|
||||||
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
|
|
||||||
<galimatias.version>0.2.1</galimatias.version>
|
|
||||||
<bytedeco.version>1.5.2</bytedeco.version>
|
|
||||||
<opencv.version>4.1.2-1.5.2</opencv.version>
|
|
||||||
<openblas.version>0.3.6-1.5.1</openblas.version>
|
|
||||||
<ffmpeg.version>4.2.1-1.5.2</ffmpeg.version>
|
|
||||||
<itextpdf.version>5.5.13.3</itextpdf.version>
|
|
||||||
<httpclient.version>3.1</httpclient.version>
|
|
||||||
<aspose-cad.version>23.9</aspose-cad.version>
|
|
||||||
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
|
|
||||||
<juniversalchardet.version>1.0.3</juniversalchardet.version>
|
|
||||||
<httpcomponents.version>4.5.14</httpcomponents.version>
|
|
||||||
|
|
||||||
<commons-cli.version>1.5.0</commons-cli.version>
|
|
||||||
<commons-net.version>3.9.0</commons-net.version>
|
|
||||||
<commons-lang3.version>3.13.0</commons-lang3.version>
|
|
||||||
|
|
||||||
|
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
|
|
||||||
|
<!-- ========== Spring 框架 ========== -->
|
||||||
|
<spring.boot.version>3.5.6</spring.boot.version>
|
||||||
|
|
||||||
|
<!-- ========== 文档转换和Office处理 ========== -->
|
||||||
|
<jodconverter.version>4.4.11</jodconverter.version>
|
||||||
|
<poi.version>5.2.5</poi.version>
|
||||||
|
<xdocreport.version>1.0.6</xdocreport.version>
|
||||||
|
<aspose-cad.version>25.10</aspose-cad.version>
|
||||||
|
|
||||||
|
<!-- ========== PDF 处理 ========== -->
|
||||||
|
<pdfbox.version>3.0.6</pdfbox.version>
|
||||||
|
|
||||||
|
<!-- ========== 图像处理 ========== -->
|
||||||
|
<jai-imageio.version>1.4.0</jai-imageio.version>
|
||||||
|
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
|
||||||
|
<commons-imaging.version>1.0.0-alpha6</commons-imaging.version>
|
||||||
|
|
||||||
|
<!-- ========== 视频处理 (JavaCV) ========== -->
|
||||||
|
<bytedeco.version>1.5.12</bytedeco.version>
|
||||||
|
<opencv.version>4.11.0-1.5.12</opencv.version>
|
||||||
|
<openblas.version>0.3.30-1.5.12</openblas.version>
|
||||||
|
<ffmpeg.version>7.1.1-1.5.12</ffmpeg.version>
|
||||||
|
|
||||||
|
<!-- ========== 压缩文件处理 ========== -->
|
||||||
|
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
|
||||||
|
<junrar.version>7.5.5</junrar.version>
|
||||||
|
|
||||||
|
<!-- ========== 缓存和存储 ========== -->
|
||||||
|
<redisson.version>4.0.0</redisson.version>
|
||||||
|
<rocksdb.version>5.17.2</rocksdb.version>
|
||||||
|
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
|
||||||
|
|
||||||
|
<!-- ========== 网络通信 ========== -->
|
||||||
|
<httpcomponents.version>4.5.16</httpcomponents.version>
|
||||||
|
<commons-net.version>3.12.0</commons-net.version>
|
||||||
|
|
||||||
|
<!-- ========== Apache Commons 工具库 ========== -->
|
||||||
|
<commons-lang3.version>3.20.0</commons-lang3.version>
|
||||||
|
<commons-cli.version>1.11.0</commons-cli.version>
|
||||||
|
|
||||||
|
<!-- ========== 编码和字符处理 ========== -->
|
||||||
|
<juniversalchardet.version>1.0.3</juniversalchardet.version>
|
||||||
|
<jchardet.version>1.0</jchardet.version>
|
||||||
|
|
||||||
|
<!-- ========== 安全相关 ========== -->
|
||||||
|
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
|
||||||
|
|
||||||
|
<!-- ========== 其他工具库 ========== -->
|
||||||
|
<xstream.version>1.4.21</xstream.version>
|
||||||
|
<antlr.version>2.7.7</antlr.version>
|
||||||
|
<galimatias.version>0.2.1</galimatias.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>server</module>
|
<module>server</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
|
<!-- ========== 项目信息 ========== -->
|
||||||
<name>kkFileView-parent</name>
|
<name>kkFileView-parent</name>
|
||||||
<description>专注文件在线预览服务</description>
|
<description>专注文件在线预览服务</description>
|
||||||
<url>https://github.com/kekingcn/kkFileView</url>
|
<url>https://github.com/kekingcn/kkFileView</url>
|
||||||
@@ -89,5 +110,4 @@
|
|||||||
<system>github</system>
|
<system>github</system>
|
||||||
<url>https://github.com/kekingcn/kkFileView/issues</url>
|
<url>https://github.com/kekingcn/kkFileView/issues</url>
|
||||||
</issueManagement>
|
</issueManagement>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
361
server/pom.xml
361
server/pom.xml
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<version>4.4.0</version>
|
<version>5.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>kkFileView</artifactId>
|
<artifactId>kkFileView</artifactId>
|
||||||
@@ -24,28 +24,26 @@
|
|||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
|
||||||
<id>aspose-maven-repository</id>
|
<!-- Aspose 仓库,且只启用 releases -->
|
||||||
<url>https://repository.aspose.com/repo</url>
|
<repository>
|
||||||
<snapshots>
|
<id>aspose-maven-repository</id>
|
||||||
<enabled>false</enabled>
|
<url>https://repository.aspose.com/repo</url>
|
||||||
</snapshots>
|
<releases>
|
||||||
</repository>
|
<enabled>true</enabled>
|
||||||
</repositories>
|
</releases>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>false</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<!-- ========== Spring Boot 框架依赖 ========== -->
|
||||||
<groupId>org.jodconverter</groupId>
|
|
||||||
<artifactId>jodconverter-local</artifactId>
|
|
||||||
<version>${jodconverter.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- web start -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||||
@@ -54,9 +52,21 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- web end -->
|
<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>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
<artifactId>poi</artifactId>
|
<artifactId>poi</artifactId>
|
||||||
@@ -93,85 +103,13 @@
|
|||||||
<artifactId>fr.opensagres.xdocreport.document</artifactId>
|
<artifactId>fr.opensagres.xdocreport.document</artifactId>
|
||||||
<version>${xdocreport.version}</version>
|
<version>${xdocreport.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- poi start -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
<groupId>com.aspose</groupId>
|
||||||
<artifactId>httpclient5</artifactId>
|
<artifactId>aspose-cad</artifactId>
|
||||||
|
<version>${aspose-cad.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 对 rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
|
<!-- ========== PDF处理 ========== -->
|
||||||
<dependency>
|
|
||||||
<groupId>net.sf.sevenzipjbinding</groupId>
|
|
||||||
<artifactId>sevenzipjbinding</artifactId>
|
|
||||||
<version>${sevenzipjbinding.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.sf.sevenzipjbinding</groupId>
|
|
||||||
<artifactId>sevenzipjbinding-all-platforms</artifactId>
|
|
||||||
<version>${sevenzipjbinding.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.commons</groupId>
|
|
||||||
<artifactId>commons-lang3</artifactId>
|
|
||||||
<version>${commons-lang3.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.redisson</groupId>
|
|
||||||
<artifactId>redisson</artifactId>
|
|
||||||
<version>${redisson.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 编码检测-JUniversalCharDet-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.googlecode.juniversalchardet</groupId>
|
|
||||||
<artifactId>juniversalchardet</artifactId>
|
|
||||||
<version>${juniversalchardet.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 解压(rar)-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.junrar</groupId>
|
|
||||||
<artifactId>junrar</artifactId>
|
|
||||||
<version>${junrar.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.sourceforge.jchardet</groupId>
|
|
||||||
<artifactId>jchardet</artifactId>
|
|
||||||
<version>${jchardet.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>antlr</groupId>
|
|
||||||
<artifactId>antlr</artifactId>
|
|
||||||
<version>${antlr.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>commons-cli</groupId>
|
|
||||||
<artifactId>commons-cli</artifactId>
|
|
||||||
<version>${commons-cli.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- FTP -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>commons-net</groupId>
|
|
||||||
<artifactId>commons-net</artifactId>
|
|
||||||
<version>${commons-net.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.thoughtworks.xstream</groupId>
|
|
||||||
<artifactId>xstream</artifactId>
|
|
||||||
<version>${xstream.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
|
|
||||||
<artifactId>concurrentlinkedhashmap-lru</artifactId>
|
|
||||||
<version>${concurrentlinkedhashmap.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.rocksdb</groupId>
|
|
||||||
<artifactId>rocksdbjni</artifactId>
|
|
||||||
<version>${rocksdb.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.pdfbox</groupId>
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
<artifactId>pdfbox</artifactId>
|
<artifactId>pdfbox</artifactId>
|
||||||
@@ -188,6 +126,25 @@
|
|||||||
<artifactId>pdfbox-tools</artifactId>
|
<artifactId>pdfbox-tools</artifactId>
|
||||||
<version>${pdfbox.version}</version>
|
<version>${pdfbox.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 压缩文件处理 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sf.sevenzipjbinding</groupId>
|
||||||
|
<artifactId>sevenzipjbinding</artifactId>
|
||||||
|
<version>${sevenzipjbinding.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sf.sevenzipjbinding</groupId>
|
||||||
|
<artifactId>sevenzipjbinding-all-platforms</artifactId>
|
||||||
|
<version>${sevenzipjbinding.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.junrar</groupId>
|
||||||
|
<artifactId>junrar</artifactId>
|
||||||
|
<version>${junrar.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 图像处理 ========== -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.jai-imageio</groupId>
|
<groupId>com.github.jai-imageio</groupId>
|
||||||
<artifactId>jai-imageio-jpeg2000</artifactId>
|
<artifactId>jai-imageio-jpeg2000</artifactId>
|
||||||
@@ -203,79 +160,12 @@
|
|||||||
<artifactId>jbig2-imageio</artifactId>
|
<artifactId>jbig2-imageio</artifactId>
|
||||||
<version>${jbig2-imageio.version}</version>
|
<version>${jbig2-imageio.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.aspose</groupId>
|
|
||||||
<artifactId>aspose-cad</artifactId>
|
|
||||||
<version>${aspose-cad.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- 密钥算法 -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.bouncycastle</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>bcprov-jdk15on</artifactId>
|
<artifactId>commons-imaging</artifactId>
|
||||||
<version>${bcprov-jdk15on.version}</version>
|
<version>${commons-imaging.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- url 规范化 -->
|
<!-- JAI 系统依赖 -->
|
||||||
<dependency>
|
|
||||||
<groupId>io.mola.galimatias</groupId>
|
|
||||||
<artifactId>galimatias</artifactId>
|
|
||||||
<version>${galimatias.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 以下是bytedeco 基于opencv ffmpeg封装的javacv,用于视频处理 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>javacv</artifactId>
|
|
||||||
<version>${bytedeco.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>javacpp</artifactId>
|
|
||||||
<version>${bytedeco.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 此版本中主要兼容linux和windows系统,如需兼容其他系统平台,请引入对应依赖即可 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>opencv</artifactId>
|
|
||||||
<version>${opencv.version}</version>
|
|
||||||
<classifier>linux-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>opencv</artifactId>
|
|
||||||
<version>${opencv.version}</version>
|
|
||||||
<classifier>windows-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>openblas</artifactId>
|
|
||||||
<version>${openblas.version}</version>
|
|
||||||
<classifier>linux-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>openblas</artifactId>
|
|
||||||
<version>${openblas.version}</version>
|
|
||||||
<classifier>windows-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>ffmpeg</artifactId>
|
|
||||||
<version>${ffmpeg.version}</version>
|
|
||||||
<classifier>linux-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.bytedeco</groupId>
|
|
||||||
<artifactId>ffmpeg</artifactId>
|
|
||||||
<version>${ffmpeg.version}</version>
|
|
||||||
<classifier>windows-x86_64</classifier>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.itextpdf</groupId>
|
|
||||||
<artifactId>itextpdf</artifactId>
|
|
||||||
<version>${itextpdf.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.media</groupId>
|
<groupId>javax.media</groupId>
|
||||||
<artifactId>jai_core</artifactId>
|
<artifactId>jai_core</artifactId>
|
||||||
@@ -291,25 +181,134 @@
|
|||||||
<systemPath>${pom.basedir}/lib/jai_codec-1.1.3.jar</systemPath>
|
<systemPath>${pom.basedir}/lib/jai_codec-1.1.3.jar</systemPath>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- test dependency - start -->
|
<!-- ========== 视频处理 (JavaCV) ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacv</artifactId>
|
||||||
|
<version>${bytedeco.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>javacpp</artifactId>
|
||||||
|
<version>${bytedeco.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv</artifactId>
|
||||||
|
<version>${opencv.version}</version>
|
||||||
|
<classifier>linux-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>opencv</artifactId>
|
||||||
|
<version>${opencv.version}</version>
|
||||||
|
<classifier>windows-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas</artifactId>
|
||||||
|
<version>${openblas.version}</version>
|
||||||
|
<classifier>linux-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>openblas</artifactId>
|
||||||
|
<version>${openblas.version}</version>
|
||||||
|
<classifier>windows-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>${ffmpeg.version}</version>
|
||||||
|
<classifier>linux-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bytedeco</groupId>
|
||||||
|
<artifactId>ffmpeg</artifactId>
|
||||||
|
<version>${ffmpeg.version}</version>
|
||||||
|
<classifier>windows-x86_64</classifier>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 网络通信 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
|
<artifactId>httpclient5</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-net</groupId>
|
||||||
|
<artifactId>commons-net</artifactId>
|
||||||
|
<version>${commons-net.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 缓存和存储 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.redisson</groupId>
|
||||||
|
<artifactId>redisson</artifactId>
|
||||||
|
<version>${redisson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.rocksdb</groupId>
|
||||||
|
<artifactId>rocksdbjni</artifactId>
|
||||||
|
<version>${rocksdb.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.concurrentlinkedhashmap</groupId>
|
||||||
|
<artifactId>concurrentlinkedhashmap-lru</artifactId>
|
||||||
|
<version>${concurrentlinkedhashmap.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 编码检测和字符处理 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.googlecode.juniversalchardet</groupId>
|
||||||
|
<artifactId>juniversalchardet</artifactId>
|
||||||
|
<version>${juniversalchardet.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sourceforge.jchardet</groupId>
|
||||||
|
<artifactId>jchardet</artifactId>
|
||||||
|
<version>${jchardet.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== Apache Commons 工具库 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-lang3</artifactId>
|
||||||
|
<version>${commons-lang3.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-cli</groupId>
|
||||||
|
<artifactId>commons-cli</artifactId>
|
||||||
|
<version>${commons-cli.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 其他工具库 ========== -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>antlr</groupId>
|
||||||
|
<artifactId>antlr</artifactId>
|
||||||
|
<version>${antlr.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.thoughtworks.xstream</groupId>
|
||||||
|
<artifactId>xstream</artifactId>
|
||||||
|
<version>${xstream.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
<version>${bcprov-jdk15on.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.mola.galimatias</groupId>
|
||||||
|
<artifactId>galimatias</artifactId>
|
||||||
|
<version>${galimatias.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- ========== 测试依赖 ========== -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>commons-httpclient</groupId>
|
|
||||||
<artifactId>commons-httpclient</artifactId>
|
|
||||||
<version>${httpclient.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<artifactId>commons-logging</artifactId>
|
|
||||||
<groupId>commons-logging</groupId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
<!-- test dependency - end -->
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -373,4 +372,4 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
16
server/src/main/bin/dev.sh
Executable file
16
server/src/main/bin/dev.sh
Executable 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"
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
@echo off
|
@echo off
|
||||||
set "KKFILEVIEW_BIN_FOLDER=%cd%"
|
set "KKFILEVIEW_BIN_FOLDER=%cd%"
|
||||||
cd "%KKFILEVIEW_BIN_FOLDER%"
|
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 KKFILEVIEW_BIN_FOLDER %KKFILEVIEW_BIN_FOLDER%
|
||||||
|
echo Using JAR_NAME %JAR_NAME%
|
||||||
echo Starting kkFileView...
|
echo Starting kkFileView...
|
||||||
echo Please check log file in ../log/kkFileView.log for more information
|
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 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 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
|
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
|
||||||
|
|||||||
@@ -49,9 +49,16 @@ else
|
|||||||
fi
|
fi
|
||||||
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
|
## 启动kkFileView
|
||||||
echo "Starting 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 "Please execute ./showlog.sh to check log for more information"
|
||||||
echo "You can get help in our official home site: https://kkview.cn"
|
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 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
478
server/src/main/config/test.properties
Normal file
478
server/src/main/config/test.properties
Normal 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)的前端解析方式,默认为web(Web端解析)
|
||||||
|
# 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 默认集成到系统,但是特别吃服务器性能 (支持转换格式为 svg、pdf)
|
||||||
|
# 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.txt(Windows)或 file:/opt/1.txt(Linux)
|
||||||
|
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数据库索引,默认为0(0-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-8,Windows一般为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
@@ -6,213 +6,282 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
/**
|
|
||||||
* @auther: chenjh
|
|
||||||
* @time: 2019/4/10 16:16
|
|
||||||
* @description 每隔1s读取并更新一次配置文件
|
|
||||||
*/
|
|
||||||
@Component
|
@Component
|
||||||
public class ConfigRefreshComponent {
|
public class ConfigRefreshComponent {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRefreshComponent.class);
|
||||||
|
private static final long DEBOUNCE_DELAY_SECONDS = 5;
|
||||||
|
|
||||||
|
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
private final ExecutorService watchServiceExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
private final Object lock = new Object();
|
||||||
|
|
||||||
|
private ScheduledFuture<?> scheduledReloadTask;
|
||||||
|
private WatchService watchService;
|
||||||
|
private volatile boolean running = true;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void refresh() {
|
void init() {
|
||||||
Thread configRefreshThread = new Thread(new ConfigRefreshThread());
|
loadConfig();
|
||||||
configRefreshThread.start();
|
watchServiceExecutor.submit(this::watchConfigFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ConfigRefreshThread implements Runnable {
|
@PreDestroy
|
||||||
@Override
|
void destroy() {
|
||||||
public void run() {
|
running = false;
|
||||||
|
watchServiceExecutor.shutdownNow();
|
||||||
|
scheduler.shutdownNow();
|
||||||
|
if (watchService != null) {
|
||||||
|
try {
|
||||||
|
watchService.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("关闭 WatchService 时发生异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void watchConfigFile() {
|
||||||
|
try {
|
||||||
|
String configFilePath = ConfigUtils.getCustomizedConfigPath();
|
||||||
|
Path configPath = Paths.get(configFilePath);
|
||||||
|
Path configDir = configPath.getParent();
|
||||||
|
if (configDir == null) {
|
||||||
|
LOGGER.error("配置文件路径无效: {}", configFilePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
watchService = FileSystems.getDefault().newWatchService();
|
||||||
|
configDir.register(watchService,
|
||||||
|
StandardWatchEventKinds.ENTRY_MODIFY,
|
||||||
|
StandardWatchEventKinds.ENTRY_CREATE,
|
||||||
|
StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
LOGGER.info("开始监听配置文件: {}", configFilePath);
|
||||||
|
|
||||||
|
while (running && !Thread.currentThread().isInterrupted()) {
|
||||||
|
try {
|
||||||
|
WatchKey key = watchService.take();
|
||||||
|
for (WatchEvent<?> event : key.pollEvents()) {
|
||||||
|
WatchEvent.Kind<?> kind = event.kind();
|
||||||
|
Path changedPath = (Path) event.context();
|
||||||
|
if (changedPath.equals(configPath.getFileName())) {
|
||||||
|
handleConfigChange(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!key.reset()) {
|
||||||
|
LOGGER.warn("WatchKey 无法重置");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("初始化配置文件监听失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleConfigChange(WatchEvent.Kind<?> kind) {
|
||||||
|
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
|
||||||
|
running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind == StandardWatchEventKinds.ENTRY_MODIFY ||
|
||||||
|
kind == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (scheduledReloadTask != null && !scheduledReloadTask.isDone()) {
|
||||||
|
scheduledReloadTask.cancel(false);
|
||||||
|
}
|
||||||
|
scheduledReloadTask = scheduler.schedule(() -> {
|
||||||
|
try {
|
||||||
|
loadConfig();
|
||||||
|
LOGGER.info("配置文件已重新加载");
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("重新加载配置失败", e);
|
||||||
|
}
|
||||||
|
}, DEBOUNCE_DELAY_SECONDS, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
synchronized (lock) {
|
||||||
try {
|
try {
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
String text;
|
|
||||||
String media;
|
|
||||||
boolean cacheEnabled;
|
|
||||||
String[] textArray;
|
|
||||||
String[] mediaArray;
|
|
||||||
String officePreviewType;
|
|
||||||
String officePreviewSwitchDisabled;
|
|
||||||
String ftpUsername;
|
|
||||||
String ftpPassword;
|
|
||||||
String ftpControlEncoding;
|
|
||||||
String configFilePath = ConfigUtils.getCustomizedConfigPath();
|
String configFilePath = ConfigUtils.getCustomizedConfigPath();
|
||||||
String baseUrl;
|
Path configPath = Paths.get(configFilePath);
|
||||||
String trustHost;
|
if (!Files.exists(configPath)) {
|
||||||
String notTrustHost;
|
LOGGER.warn("配置文件不存在: {}", configFilePath);
|
||||||
String pdfPresentationModeDisable;
|
return;
|
||||||
String pdfOpenFileDisable;
|
}
|
||||||
String pdfPrintDisable;
|
|
||||||
String pdfDownloadDisable;
|
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(configFilePath))) {
|
||||||
String pdfBookmarkDisable;
|
|
||||||
String pdfDisableEditing;
|
|
||||||
boolean fileUploadDisable;
|
|
||||||
String tifPreviewType;
|
|
||||||
String prohibit;
|
|
||||||
String[] prohibitArray;
|
|
||||||
String beian;
|
|
||||||
String size;
|
|
||||||
String password;
|
|
||||||
int pdf2JpgDpi;
|
|
||||||
String officeTypeWeb;
|
|
||||||
String cadPreviewType;
|
|
||||||
boolean deleteSourceFile;
|
|
||||||
boolean deleteCaptcha;
|
|
||||||
String officPageRange;
|
|
||||||
String officWatermark;
|
|
||||||
String officQuality;
|
|
||||||
String officMaxImageResolution;
|
|
||||||
boolean officExportBookmarks;
|
|
||||||
boolean officeExportNotes;
|
|
||||||
boolean officeDocumentOpenPasswords;
|
|
||||||
String cadTimeout;
|
|
||||||
int cadThread;
|
|
||||||
String homePageNumber;
|
|
||||||
String homePagination;
|
|
||||||
String homePageSize;
|
|
||||||
String homeSearch;
|
|
||||||
int pdfTimeout;
|
|
||||||
int pdfTimeout80;
|
|
||||||
int pdfTimeout200;
|
|
||||||
int pdfThread;
|
|
||||||
while (true) {
|
|
||||||
FileReader fileReader = new FileReader(configFilePath);
|
|
||||||
BufferedReader bufferedReader = new BufferedReader(fileReader);
|
|
||||||
properties.load(bufferedReader);
|
properties.load(bufferedReader);
|
||||||
ConfigUtils.restorePropertiesFromEnvFormat(properties);
|
ConfigUtils.restorePropertiesFromEnvFormat(properties);
|
||||||
cacheEnabled = Boolean.parseBoolean(properties.getProperty("cache.enabled", ConfigConstants.DEFAULT_CACHE_ENABLED));
|
updateConfigConstants(properties);
|
||||||
text = properties.getProperty("simText", ConfigConstants.DEFAULT_TXT_TYPE);
|
|
||||||
media = properties.getProperty("media", ConfigConstants.DEFAULT_MEDIA_TYPE);
|
|
||||||
officePreviewType = properties.getProperty("office.preview.type", ConfigConstants.DEFAULT_OFFICE_PREVIEW_TYPE);
|
|
||||||
officePreviewSwitchDisabled = properties.getProperty("office.preview.switch.disabled", ConfigConstants.DEFAULT_OFFICE_PREVIEW_SWITCH_DISABLED);
|
|
||||||
ftpUsername = properties.getProperty("ftp.username", ConfigConstants.DEFAULT_FTP_USERNAME);
|
|
||||||
ftpPassword = properties.getProperty("ftp.password", ConfigConstants.DEFAULT_FTP_PASSWORD);
|
|
||||||
ftpControlEncoding = properties.getProperty("ftp.control.encoding", ConfigConstants.DEFAULT_FTP_CONTROL_ENCODING);
|
|
||||||
textArray = text.split(",");
|
|
||||||
mediaArray = media.split(",");
|
|
||||||
baseUrl = properties.getProperty("base.url", ConfigConstants.DEFAULT_VALUE);
|
|
||||||
trustHost = properties.getProperty("trust.host", ConfigConstants.DEFAULT_VALUE);
|
|
||||||
notTrustHost = properties.getProperty("not.trust.host", ConfigConstants.DEFAULT_VALUE);
|
|
||||||
pdfPresentationModeDisable = properties.getProperty("pdf.presentationMode.disable", ConfigConstants.DEFAULT_PDF_PRESENTATION_MODE_DISABLE);
|
|
||||||
pdfOpenFileDisable = properties.getProperty("pdf.openFile.disable", ConfigConstants.DEFAULT_PDF_OPEN_FILE_DISABLE);
|
|
||||||
pdfPrintDisable = properties.getProperty("pdf.print.disable", ConfigConstants.DEFAULT_PDF_PRINT_DISABLE);
|
|
||||||
pdfDownloadDisable = properties.getProperty("pdf.download.disable", ConfigConstants.DEFAULT_PDF_DOWNLOAD_DISABLE);
|
|
||||||
pdfBookmarkDisable = properties.getProperty("pdf.bookmark.disable", ConfigConstants.DEFAULT_PDF_BOOKMARK_DISABLE);
|
|
||||||
pdfDisableEditing = properties.getProperty("pdf.disable.editing", ConfigConstants.DEFAULT_PDF_DISABLE_EDITING);
|
|
||||||
fileUploadDisable = Boolean.parseBoolean(properties.getProperty("file.upload.disable", ConfigConstants.DEFAULT_FILE_UPLOAD_DISABLE));
|
|
||||||
tifPreviewType = properties.getProperty("tif.preview.type", ConfigConstants.DEFAULT_TIF_PREVIEW_TYPE);
|
|
||||||
cadPreviewType = properties.getProperty("cad.preview.type", ConfigConstants.DEFAULT_CAD_PREVIEW_TYPE);
|
|
||||||
size = properties.getProperty("spring.servlet.multipart.max-file-size", ConfigConstants.DEFAULT_SIZE);
|
|
||||||
beian = properties.getProperty("beian", ConfigConstants.DEFAULT_BEIAN);
|
|
||||||
prohibit = properties.getProperty("prohibit", ConfigConstants.DEFAULT_PROHIBIT);
|
|
||||||
password = properties.getProperty("delete.password", ConfigConstants.DEFAULT_PASSWORD);
|
|
||||||
pdf2JpgDpi = Integer.parseInt(properties.getProperty("pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI));
|
|
||||||
officeTypeWeb = properties.getProperty("office.type.web", ConfigConstants.DEFAULT_OFFICE_TYPE_WEB);
|
|
||||||
deleteSourceFile = Boolean.parseBoolean(properties.getProperty("delete.source.file", ConfigConstants.DEFAULT_DELETE_SOURCE_FILE));
|
|
||||||
deleteCaptcha = Boolean.parseBoolean(properties.getProperty("delete.captcha", ConfigConstants.DEFAULT_DELETE_CAPTCHA));
|
|
||||||
officPageRange = properties.getProperty("office.pagerange", ConfigConstants.DEFAULT_OFFICE_PAQERANQE);
|
|
||||||
officWatermark = properties.getProperty("office.watermark", ConfigConstants.DEFAULT_OFFICE_WATERMARK);
|
|
||||||
officQuality = properties.getProperty("office.quality", ConfigConstants.DEFAULT_OFFICE_QUALITY);
|
|
||||||
officMaxImageResolution = properties.getProperty("office.maximageresolution", ConfigConstants.DEFAULT_OFFICE_MAXIMAQERESOLUTION);
|
|
||||||
officExportBookmarks = Boolean.parseBoolean(properties.getProperty("office.exportbookmarks", ConfigConstants.DEFAULT_OFFICE_EXPORTBOOKMARKS));
|
|
||||||
officeExportNotes = Boolean.parseBoolean(properties.getProperty("office.exportnotes", ConfigConstants.DEFAULT_OFFICE_EXPORTNOTES));
|
|
||||||
officeDocumentOpenPasswords = Boolean.parseBoolean(properties.getProperty("office.documentopenpasswords", ConfigConstants.DEFAULT_OFFICE_EOCUMENTOPENPASSWORDS));
|
|
||||||
cadTimeout = properties.getProperty("cad.timeout", ConfigConstants.DEFAULT_CAD_TIMEOUT);
|
|
||||||
homePageNumber = properties.getProperty("home.pagenumber", ConfigConstants.DEFAULT_HOME_PAGENUMBER);
|
|
||||||
homePagination = properties.getProperty("home.pagination", ConfigConstants.DEFAULT_HOME_PAGINATION);
|
|
||||||
homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
|
|
||||||
homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
|
|
||||||
cadThread = Integer.parseInt(properties.getProperty("cad.thread", ConfigConstants.DEFAULT_CAD_THREAD));
|
|
||||||
pdfTimeout = Integer.parseInt(properties.getProperty("pdf.timeout", ConfigConstants.DEFAULT_PDF_TIMEOUT));
|
|
||||||
pdfTimeout80 = Integer.parseInt(properties.getProperty("pdf.timeout80", ConfigConstants.DEFAULT_PDF_TIMEOUT80));
|
|
||||||
pdfTimeout200 = Integer.parseInt(properties.getProperty("pdf.timeout200", ConfigConstants.DEFAULT_PDF_TIMEOUT200));
|
|
||||||
pdfThread = Integer.parseInt(properties.getProperty("pdf.thread", ConfigConstants.DEFAULT_PDF_THREAD));
|
|
||||||
prohibitArray = prohibit.split(",");
|
|
||||||
|
|
||||||
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
|
|
||||||
ConfigConstants.setSimTextValue(textArray);
|
|
||||||
ConfigConstants.setMediaValue(mediaArray);
|
|
||||||
ConfigConstants.setOfficePreviewTypeValue(officePreviewType);
|
|
||||||
ConfigConstants.setFtpUsernameValue(ftpUsername);
|
|
||||||
ConfigConstants.setFtpPasswordValue(ftpPassword);
|
|
||||||
ConfigConstants.setFtpControlEncodingValue(ftpControlEncoding);
|
|
||||||
ConfigConstants.setBaseUrlValue(baseUrl);
|
|
||||||
ConfigConstants.setTrustHostValue(trustHost);
|
|
||||||
ConfigConstants.setNotTrustHostValue(notTrustHost);
|
|
||||||
ConfigConstants.setOfficePreviewSwitchDisabledValue(officePreviewSwitchDisabled);
|
|
||||||
ConfigConstants.setPdfPresentationModeDisableValue(pdfPresentationModeDisable);
|
|
||||||
ConfigConstants.setPdfOpenFileDisableValue(pdfOpenFileDisable);
|
|
||||||
ConfigConstants.setPdfPrintDisableValue(pdfPrintDisable);
|
|
||||||
ConfigConstants.setPdfDownloadDisableValue(pdfDownloadDisable);
|
|
||||||
ConfigConstants.setPdfBookmarkDisableValue(pdfBookmarkDisable);
|
|
||||||
ConfigConstants.setPdfDisableEditingValue(pdfDisableEditing);
|
|
||||||
ConfigConstants.setFileUploadDisableValue(fileUploadDisable);
|
|
||||||
ConfigConstants.setTifPreviewTypeValue(tifPreviewType);
|
|
||||||
ConfigConstants.setCadPreviewTypeValue(cadPreviewType);
|
|
||||||
ConfigConstants.setBeianValue(beian);
|
|
||||||
ConfigConstants.setSizeValue(size);
|
|
||||||
ConfigConstants.setProhibitValue(prohibitArray);
|
|
||||||
ConfigConstants.setPasswordValue(password);
|
|
||||||
ConfigConstants.setPdf2JpgDpiValue(pdf2JpgDpi);
|
|
||||||
ConfigConstants.setOfficeTypeWebValue(officeTypeWeb);
|
|
||||||
ConfigConstants.setOfficePageRangeValue(officPageRange);
|
|
||||||
ConfigConstants.setOfficeWatermarkValue(officWatermark);
|
|
||||||
ConfigConstants.setOfficeQualityValue(officQuality);
|
|
||||||
ConfigConstants.setOfficeMaxImageResolutionValue(officMaxImageResolution);
|
|
||||||
ConfigConstants.setOfficeExportBookmarksValue(officExportBookmarks);
|
|
||||||
ConfigConstants.setOfficeExportNotesValue(officeExportNotes);
|
|
||||||
ConfigConstants.setOfficeDocumentOpenPasswordsValue(officeDocumentOpenPasswords);
|
|
||||||
ConfigConstants.setDeleteSourceFileValue(deleteSourceFile);
|
|
||||||
ConfigConstants.setDeleteCaptchaValue(deleteCaptcha);
|
|
||||||
ConfigConstants.setCadTimeoutValue(cadTimeout);
|
|
||||||
ConfigConstants.setCadThreadValue(cadThread);
|
|
||||||
ConfigConstants.setHomePageNumberValue(homePageNumber);
|
|
||||||
ConfigConstants.setHomePaginationValue(homePagination);
|
|
||||||
ConfigConstants.setHomePageSizeValue(homePageSize);
|
|
||||||
ConfigConstants.setHomeSearchValue(homeSearch);
|
|
||||||
ConfigConstants.setPdfTimeoutValue(pdfTimeout);
|
|
||||||
ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
|
|
||||||
ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
|
|
||||||
ConfigConstants.setPdfThreadValue(pdfThread);
|
|
||||||
setWatermarkConfig(properties);
|
setWatermarkConfig(properties);
|
||||||
bufferedReader.close();
|
LOGGER.info("配置文件重新加载完成");
|
||||||
fileReader.close();
|
|
||||||
TimeUnit.SECONDS.sleep(1);
|
|
||||||
}
|
}
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.error("读取配置文件异常", e);
|
LOGGER.error("读取配置文件异常", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
package cn.keking.config;
|
package cn.keking.config;
|
||||||
|
|
||||||
import io.netty.channel.nio.NioEventLoopGroup;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.redisson.Redisson;
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
import org.redisson.client.codec.Codec;
|
import org.redisson.client.codec.Codec;
|
||||||
import org.redisson.config.Config;
|
import org.redisson.config.Config;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||||
@@ -11,42 +12,80 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by kl on 2017/09/26.
|
* Redisson 客户端配置(完善版)
|
||||||
* redisson 客户端配置
|
* 支持 single / cluster / master-slave / sentinel 四种模式,配置完整,统一参数。
|
||||||
*/
|
*/
|
||||||
@ConditionalOnExpression("'${cache.type:default}'.equals('redis')")
|
@ConditionalOnExpression("'${cache.type:default}'.equals('redis')")
|
||||||
@ConfigurationProperties(prefix = "spring.redisson")
|
@ConfigurationProperties(prefix = "spring.redisson")
|
||||||
@Configuration
|
@Configuration
|
||||||
public class RedissonConfig {
|
public class RedissonConfig {
|
||||||
|
|
||||||
|
// ========================== 连接配置 ==========================
|
||||||
private String address;
|
private String address;
|
||||||
|
private String password;
|
||||||
|
private String clientName;
|
||||||
|
private int database = 0;
|
||||||
|
private String mode = "single";
|
||||||
|
private String masterName = "kkfile";
|
||||||
|
|
||||||
|
// ========================== 超时配置 ==========================
|
||||||
|
private int idleConnectionTimeout = 10000;
|
||||||
|
private int connectTimeout = 10000;
|
||||||
|
private int timeout = 3000;
|
||||||
|
|
||||||
|
// ========================== 重试配置 ==========================
|
||||||
|
private int retryAttempts = 3;
|
||||||
|
private int retryInterval = 1500;
|
||||||
|
|
||||||
|
// ========================== 连接池配置 ==========================
|
||||||
private int connectionMinimumIdleSize = 10;
|
private int connectionMinimumIdleSize = 10;
|
||||||
private int idleConnectionTimeout=10000;
|
private int connectionPoolSize = 64;
|
||||||
private int pingTimeout=1000;
|
private int subscriptionsPerConnection = 5;
|
||||||
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 subscriptionConnectionMinimumIdleSize = 1;
|
||||||
private int subscriptionConnectionPoolSize = 50;
|
private int subscriptionConnectionPoolSize = 50;
|
||||||
private int connectionPoolSize = 64;
|
|
||||||
private int database = 0;
|
// ========================== 集群专用配置 ==========================
|
||||||
private boolean dnsMonitoring = false;
|
private int scanInterval = 2000;
|
||||||
|
|
||||||
|
// ========================== 其他配置 ==========================
|
||||||
private int dnsMonitoringInterval = 5000;
|
private int dnsMonitoringInterval = 5000;
|
||||||
|
private int threads; // 默认为0,表示使用 CPU 核数 * 2
|
||||||
private int thread; //当前处理核数量 * 2
|
private String codec = "org.redisson.codec.JsonJacksonCodec";
|
||||||
|
|
||||||
private String codec="org.redisson.codec.JsonJacksonCodec";
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
Config config() throws Exception {
|
public RedissonClient redissonClient() {
|
||||||
Config config = new Config();
|
Config config = new Config();
|
||||||
config.useSingleServer().setAddress(address)
|
|
||||||
|
// 密码处理:空字符串转为 null
|
||||||
|
String pwd = StringUtils.isBlank(password) ? null : password;
|
||||||
|
|
||||||
|
// 根据模式构建配置
|
||||||
|
switch (mode.toLowerCase()) {
|
||||||
|
case "cluster":
|
||||||
|
configureClusterMode(config, pwd);
|
||||||
|
break;
|
||||||
|
case "master-slave":
|
||||||
|
configureMasterSlaveMode(config, pwd);
|
||||||
|
break;
|
||||||
|
case "sentinel":
|
||||||
|
configureSentinelMode(config, pwd);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
configureSingleMode(config, pwd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 公共配置:编码器、线程数
|
||||||
|
applyCommonConfig(config);
|
||||||
|
return Redisson.create(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================== 配置方法 ==========================
|
||||||
|
|
||||||
|
private void configureSingleMode(Config config, String pwd) {
|
||||||
|
String normalizedAddress = normalizeAddress(address);
|
||||||
|
config.useSingleServer()
|
||||||
|
.setAddress(normalizedAddress)
|
||||||
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
||||||
.setConnectionPoolSize(connectionPoolSize)
|
.setConnectionPoolSize(connectionPoolSize)
|
||||||
.setDatabase(database)
|
.setDatabase(database)
|
||||||
@@ -60,179 +99,184 @@ public class RedissonConfig {
|
|||||||
.setTimeout(timeout)
|
.setTimeout(timeout)
|
||||||
.setConnectTimeout(connectTimeout)
|
.setConnectTimeout(connectTimeout)
|
||||||
.setIdleConnectionTimeout(idleConnectionTimeout)
|
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||||
.setPassword(StringUtils.trimToNull(password));
|
.setPassword(pwd);
|
||||||
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
|
|
||||||
config.setCodec(codec);
|
|
||||||
config.setThreads(thread);
|
|
||||||
config.setEventLoopGroup(new NioEventLoopGroup());
|
|
||||||
return config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getThread() {
|
private void configureClusterMode(Config config, String pwd) {
|
||||||
return thread;
|
String[] nodeAddresses = normalizeAddresses(address.split(","));
|
||||||
|
config.useClusterServers()
|
||||||
|
.setScanInterval(scanInterval)
|
||||||
|
.addNodeAddress(nodeAddresses)
|
||||||
|
.setPassword(pwd)
|
||||||
|
.setRetryAttempts(retryAttempts)
|
||||||
|
.setRetryInterval(retryInterval)
|
||||||
|
.setTimeout(timeout)
|
||||||
|
.setConnectTimeout(connectTimeout)
|
||||||
|
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||||
|
.setMasterConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSlaveConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
||||||
|
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
||||||
|
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
||||||
|
.setClientName(clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setThread(int thread) {
|
private void configureMasterSlaveMode(Config config, String pwd) {
|
||||||
this.thread = thread;
|
String[] addresses = address.split(",");
|
||||||
|
validateMasterSlaveAddresses(addresses);
|
||||||
|
String[] normalizedAddresses = normalizeAddresses(addresses);
|
||||||
|
String masterAddress = normalizedAddresses[0];
|
||||||
|
String[] slaveAddresses = new String[normalizedAddresses.length - 1];
|
||||||
|
System.arraycopy(normalizedAddresses, 1, slaveAddresses, 0, slaveAddresses.length);
|
||||||
|
|
||||||
|
config.useMasterSlaveServers()
|
||||||
|
.setDatabase(database)
|
||||||
|
.setPassword(pwd)
|
||||||
|
.setMasterAddress(masterAddress)
|
||||||
|
.addSlaveAddress(slaveAddresses)
|
||||||
|
.setRetryAttempts(retryAttempts)
|
||||||
|
.setRetryInterval(retryInterval)
|
||||||
|
.setTimeout(timeout)
|
||||||
|
.setConnectTimeout(connectTimeout)
|
||||||
|
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||||
|
.setMasterConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSlaveConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
||||||
|
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
||||||
|
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
||||||
|
.setClientName(clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAddress() {
|
private void configureSentinelMode(Config config, String pwd) {
|
||||||
return address;
|
String[] sentinelAddresses = normalizeAddresses(address.split(","));
|
||||||
|
config.useSentinelServers()
|
||||||
|
.setDatabase(database)
|
||||||
|
.setPassword(pwd)
|
||||||
|
.setMasterName(masterName)
|
||||||
|
.addSentinelAddress(sentinelAddresses)
|
||||||
|
.setRetryAttempts(retryAttempts)
|
||||||
|
.setRetryInterval(retryInterval)
|
||||||
|
.setTimeout(timeout)
|
||||||
|
.setConnectTimeout(connectTimeout)
|
||||||
|
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||||
|
.setMasterConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSlaveConnectionPoolSize(connectionPoolSize)
|
||||||
|
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
||||||
|
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
||||||
|
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
||||||
|
.setClientName(clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAddress(String address) {
|
private void applyCommonConfig(Config config) {
|
||||||
this.address = address;
|
// 设置编码器
|
||||||
|
if (StringUtils.isNotBlank(codec)) {
|
||||||
|
try {
|
||||||
|
Class<?> codecClass = ClassUtils.forName(codec, ClassUtils.getDefaultClassLoader());
|
||||||
|
Codec codecInstance = (Codec) codecClass.getDeclaredConstructor().newInstance();
|
||||||
|
config.setCodec(codecInstance);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("Failed to create Redisson codec: " + codec, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置线程数(大于0时生效,否则Redisson使用默认值:CPU核数*2)
|
||||||
|
if (threads > 0) {
|
||||||
|
config.setThreads(threads);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getIdleConnectionTimeout() {
|
// ========================== 辅助方法 ==========================
|
||||||
return idleConnectionTimeout;
|
|
||||||
|
/**
|
||||||
|
* 自动补齐 Redis 地址协议前缀(redis:// 或 rediss://)
|
||||||
|
*/
|
||||||
|
private String normalizeAddress(String addr) {
|
||||||
|
if (addr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
addr = addr.trim();
|
||||||
|
if (!addr.startsWith("redis://") && !addr.startsWith("rediss://")) {
|
||||||
|
addr = "redis://" + addr;
|
||||||
|
}
|
||||||
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIdleConnectionTimeout(int idleConnectionTimeout) {
|
private String[] normalizeAddresses(String[] addresses) {
|
||||||
this.idleConnectionTimeout = idleConnectionTimeout;
|
String[] normalized = new String[addresses.length];
|
||||||
|
for (int i = 0; i < addresses.length; i++) {
|
||||||
|
normalized[i] = normalizeAddress(addresses[i]);
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPingTimeout() {
|
private void validateMasterSlaveAddresses(String[] addresses) {
|
||||||
return pingTimeout;
|
if (addresses.length < 2) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Master-slave mode requires at least 2 addresses: master and at least one slave. " +
|
||||||
|
"Current addresses: " + String.join(",", addresses));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPingTimeout(int pingTimeout) {
|
// ========================== Getter / Setter(供 Spring 绑定配置) ==========================
|
||||||
this.pingTimeout = pingTimeout;
|
// 以下所有字段都需要提供 getter/setter,示例中只列出关键字段,实际使用时请补全所有字段。
|
||||||
}
|
// 建议使用 Lombok @Data 或 IDE 自动生成。这里只展示部分,避免篇幅过长。
|
||||||
|
|
||||||
public int getConnectTimeout() {
|
public String getAddress() { return address; }
|
||||||
return connectTimeout;
|
public void setAddress(String address) { this.address = address; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setConnectTimeout(int connectTimeout) {
|
public String getPassword() { return password; }
|
||||||
this.connectTimeout = connectTimeout;
|
public void setPassword(String password) { this.password = password; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getTimeout() {
|
public String getClientName() { return clientName; }
|
||||||
return timeout;
|
public void setClientName(String clientName) { this.clientName = clientName; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimeout(int timeout) {
|
public int getDatabase() { return database; }
|
||||||
this.timeout = timeout;
|
public void setDatabase(int database) { this.database = database; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getRetryAttempts() {
|
public String getMode() { return mode; }
|
||||||
return retryAttempts;
|
public void setMode(String mode) { this.mode = mode; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setRetryAttempts(int retryAttempts) {
|
public String getMasterName() { return masterName; }
|
||||||
this.retryAttempts = retryAttempts;
|
public void setMasterName(String masterName) { this.masterName = masterName; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getRetryInterval() {
|
public int getIdleConnectionTimeout() { return idleConnectionTimeout; }
|
||||||
return retryInterval;
|
public void setIdleConnectionTimeout(int idleConnectionTimeout) { this.idleConnectionTimeout = idleConnectionTimeout; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setRetryInterval(int retryInterval) {
|
public int getConnectTimeout() { return connectTimeout; }
|
||||||
this.retryInterval = retryInterval;
|
public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getReconnectionTimeout() {
|
public int getTimeout() { return timeout; }
|
||||||
return reconnectionTimeout;
|
public void setTimeout(int timeout) { this.timeout = timeout; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setReconnectionTimeout(int reconnectionTimeout) {
|
public int getRetryAttempts() { return retryAttempts; }
|
||||||
this.reconnectionTimeout = reconnectionTimeout;
|
public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getFailedAttempts() {
|
public int getRetryInterval() { return retryInterval; }
|
||||||
return failedAttempts;
|
public void setRetryInterval(int retryInterval) { this.retryInterval = retryInterval; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setFailedAttempts(int failedAttempts) {
|
public int getConnectionMinimumIdleSize() { return connectionMinimumIdleSize; }
|
||||||
this.failedAttempts = failedAttempts;
|
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) { this.connectionMinimumIdleSize = connectionMinimumIdleSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getPassword() {
|
public int getConnectionPoolSize() { return connectionPoolSize; }
|
||||||
return password;
|
public void setConnectionPoolSize(int connectionPoolSize) { this.connectionPoolSize = connectionPoolSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setPassword(String password) {
|
public int getSubscriptionsPerConnection() { return subscriptionsPerConnection; }
|
||||||
this.password = password;
|
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) { this.subscriptionsPerConnection = subscriptionsPerConnection; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getSubscriptionsPerConnection() {
|
public int getSubscriptionConnectionMinimumIdleSize() { return subscriptionConnectionMinimumIdleSize; }
|
||||||
return subscriptionsPerConnection;
|
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) { this.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) {
|
public int getSubscriptionConnectionPoolSize() { return subscriptionConnectionPoolSize; }
|
||||||
this.subscriptionsPerConnection = subscriptionsPerConnection;
|
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) { this.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getClientName() {
|
public int getScanInterval() { return scanInterval; }
|
||||||
return clientName;
|
public void setScanInterval(int scanInterval) { this.scanInterval = scanInterval; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setClientName(String clientName) {
|
public int getDnsMonitoringInterval() { return dnsMonitoringInterval; }
|
||||||
this.clientName = clientName;
|
public void setDnsMonitoringInterval(int dnsMonitoringInterval) { this.dnsMonitoringInterval = dnsMonitoringInterval; }
|
||||||
}
|
|
||||||
|
|
||||||
public int getSubscriptionConnectionMinimumIdleSize() {
|
public int getThreads() { return threads; }
|
||||||
return subscriptionConnectionMinimumIdleSize;
|
public void setThreads(int threads) { this.threads = threads; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) {
|
public String getCodec() { return codec; }
|
||||||
this.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize;
|
public void setCodec(String codec) { this.codec = codec; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSubscriptionConnectionPoolSize() {
|
|
||||||
return subscriptionConnectionPoolSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) {
|
|
||||||
this.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getConnectionMinimumIdleSize() {
|
|
||||||
return connectionMinimumIdleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
|
|
||||||
this.connectionMinimumIdleSize = connectionMinimumIdleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getConnectionPoolSize() {
|
|
||||||
return connectionPoolSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConnectionPoolSize(int connectionPoolSize) {
|
|
||||||
this.connectionPoolSize = connectionPoolSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDatabase() {
|
|
||||||
return database;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDatabase(int database) {
|
|
||||||
this.database = database;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isDnsMonitoring() {
|
|
||||||
return dnsMonitoring;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDnsMonitoring(boolean dnsMonitoring) {
|
|
||||||
this.dnsMonitoring = dnsMonitoring;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDnsMonitoringInterval() {
|
|
||||||
return dnsMonitoringInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDnsMonitoringInterval(int dnsMonitoringInterval) {
|
|
||||||
this.dnsMonitoringInterval = dnsMonitoringInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCodec() {
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCodec(String codec) {
|
|
||||||
this.codec = codec;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -33,13 +33,15 @@ public enum FileType {
|
|||||||
EPUB("epubFilePreviewImpl"),
|
EPUB("epubFilePreviewImpl"),
|
||||||
BPMN("bpmnFilePreviewImpl"),
|
BPMN("bpmnFilePreviewImpl"),
|
||||||
DCM("dcmFilePreviewImpl"),
|
DCM("dcmFilePreviewImpl"),
|
||||||
|
MSG("msgFilePreviewImpl"),
|
||||||
DRAWIO("drawioFilePreviewImpl");
|
DRAWIO("drawioFilePreviewImpl");
|
||||||
|
|
||||||
private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla","pages", "eps"};
|
private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla","pages", "eps", "pptm"};
|
||||||
private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp"};
|
private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp", "heic", "avif", "heif"};
|
||||||
private static final String[] ARCHIVE_TYPES = {"rar", "zip", "jar", "7-zip", "tar", "gzip", "7z"};
|
private static final String[] ARCHIVE_TYPES = {"rar", "zip", "jar", "7-zip", "tar", "gzip", "7z"};
|
||||||
private static final String[] ONLINE3D_TYPES = {"obj", "3ds", "stl", "ply", "off", "3dm", "fbx", "dae", "wrl", "3mf", "ifc","glb","o3dv","gltf","stp","bim","fcstd","step","iges","brep"};
|
private static final String[] ONLINE3D_TYPES = {"obj", "3ds", "stl", "ply", "off", "3dm", "fbx", "dae", "wrl", "3mf", "ifc","glb","o3dv","gltf","stp","bim","fcstd","step","iges","brep"};
|
||||||
private static final String[] EML_TYPES = {"eml"};
|
private static final String[] EML_TYPES = {"eml"};
|
||||||
|
private static final String[] MSG_TYPES = {"msg"};
|
||||||
private static final String[] XMIND_TYPES = {"xmind"};
|
private static final String[] XMIND_TYPES = {"xmind"};
|
||||||
private static final String[] EPUB_TYPES = {"epub"};
|
private static final String[] EPUB_TYPES = {"epub"};
|
||||||
private static final String[] DCM_TYPES = {"dcm"};
|
private static final String[] DCM_TYPES = {"dcm"};
|
||||||
@@ -96,6 +98,9 @@ public enum FileType {
|
|||||||
for (String eml : EML_TYPES) {
|
for (String eml : EML_TYPES) {
|
||||||
FILE_TYPE_MAPPER.put(eml, FileType.EML);
|
FILE_TYPE_MAPPER.put(eml, FileType.EML);
|
||||||
}
|
}
|
||||||
|
for (String msg : MSG_TYPES) {
|
||||||
|
FILE_TYPE_MAPPER.put(msg, FileType.MSG);
|
||||||
|
}
|
||||||
for (String xmind : XMIND_TYPES) {
|
for (String xmind : XMIND_TYPES) {
|
||||||
FILE_TYPE_MAPPER.put(xmind, FileType.XMIND);
|
FILE_TYPE_MAPPER.put(xmind, FileType.XMIND);
|
||||||
}
|
}
|
||||||
|
|||||||
509
server/src/main/java/cn/keking/service/CadToPdfService.java
Normal file
509
server/src/main/java/cn/keking/service/CadToPdfService.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -59,26 +59,31 @@ public class CompressFileReader {
|
|||||||
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
|
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
|
||||||
if (!item.isFolder()) {
|
if (!item.isFolder()) {
|
||||||
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
|
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
|
||||||
ExtractOperationResult result = item.extractSlow(data -> {
|
Files.deleteIfExists(filePathInsideArchive);
|
||||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), true))) {
|
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), false))) {
|
||||||
out.write(data);
|
ExtractOperationResult result = item.extractSlow(data -> {
|
||||||
} catch (IOException e) {
|
try {
|
||||||
throw new RuntimeException(e);
|
out.write(data);
|
||||||
}
|
} catch (IOException e) {
|
||||||
return data.length;
|
throw new RuntimeException(e);
|
||||||
}, filePassword);
|
}
|
||||||
if (result != ExtractOperationResult.OK) {
|
return data.length;
|
||||||
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
|
}, filePassword);
|
||||||
if (result1.equals(result)) {
|
if (result != ExtractOperationResult.OK) {
|
||||||
throw new Exception("Password");
|
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
|
||||||
}else {
|
if (result1.equals(result)) {
|
||||||
throw new Exception("Failed to extract RAR file.");
|
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());
|
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
|
||||||
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码
|
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码
|
||||||
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), "UTF-8"));
|
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), StandardCharsets.UTF_8).replaceAll("%2F", "/"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,4 +115,4 @@ public class CompressFileReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,35 +4,18 @@ import cn.keking.config.ConfigConstants;
|
|||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
import cn.keking.model.FileType;
|
import cn.keking.model.FileType;
|
||||||
import cn.keking.service.cache.CacheService;
|
import cn.keking.service.cache.CacheService;
|
||||||
import cn.keking.service.cache.NotResourceCache;
|
import cn.keking.utils.*;
|
||||||
import cn.keking.utils.EncodingDetects;
|
|
||||||
import cn.keking.utils.KkFileUtils;
|
|
||||||
import cn.keking.utils.UrlEncoderUtils;
|
|
||||||
import cn.keking.utils.WebUtils;
|
|
||||||
import cn.keking.web.filter.BaseUrlFilter;
|
import cn.keking.web.filter.BaseUrlFilter;
|
||||||
import com.aspose.cad.*;
|
|
||||||
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
|
|
||||||
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
|
|
||||||
import com.aspose.cad.imageoptions.*;
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
||||||
import org.apache.pdfbox.Loader;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
|
||||||
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
|
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
@@ -41,7 +24,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.*;
|
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,10 +32,10 @@ import java.util.stream.IntStream;
|
|||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@DependsOn(ConfigConstants.BEAN_NAME)
|
@DependsOn(ConfigConstants.BEAN_NAME)
|
||||||
public class FileHandlerService implements InitializingBean {
|
public class FileHandlerService {
|
||||||
|
|
||||||
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
|
private static final String PDF2JPG_IMAGE_FORMAT = ".jpg";
|
||||||
private static final String PDF_PASSWORD_MSG = "password";
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(FileHandlerService.class);
|
private final Logger logger = LoggerFactory.getLogger(FileHandlerService.class);
|
||||||
private final String fileDir = ConfigConstants.getFileDir();
|
private final String fileDir = ConfigConstants.getFileDir();
|
||||||
private final CacheService cacheService;
|
private final CacheService cacheService;
|
||||||
@@ -147,15 +129,6 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
cacheService.putImgCache(fileKey, imgs);
|
cacheService.putImgCache(fileKey, imgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* cad定义线程池
|
|
||||||
*/
|
|
||||||
private ExecutorService pool = null;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterPropertiesSet() throws Exception {
|
|
||||||
pool = Executors.newFixedThreadPool(ConfigConstants.getCadThread());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对转换后的文件进行操作(改变编码方式)
|
* 对转换后的文件进行操作(改变编码方式)
|
||||||
@@ -195,18 +168,20 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
* @param index 图片索引
|
* @param index 图片索引
|
||||||
* @return 图片访问地址
|
* @return 图片访问地址
|
||||||
*/
|
*/
|
||||||
private String getPdf2jpgUrl(String pdfFilePath, int index) {
|
public String getPdf2jpgUrl(String pdfFilePath, int index) {
|
||||||
String baseUrl = BaseUrlFilter.getBaseUrl();
|
String baseUrl = BaseUrlFilter.getBaseUrl();
|
||||||
pdfFilePath = pdfFilePath.replace(fileDir, "");
|
pdfFilePath = pdfFilePath.replace(fileDir, "");
|
||||||
String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4);
|
String pdfFolder = pdfFilePath.substring(0, pdfFilePath.length() - 4);
|
||||||
String urlPrefix;
|
// 对整个路径进行编码,包括特殊字符
|
||||||
try {
|
String encodedPath = URLEncoder.encode(pdfFolder, StandardCharsets.UTF_8);
|
||||||
urlPrefix = baseUrl + URLEncoder.encode(pdfFolder, uriEncoding).replaceAll("\\+", "%20");
|
encodedPath = encodedPath
|
||||||
} catch (UnsupportedEncodingException e) {
|
.replaceAll("%2F", "/") // 恢复斜杠
|
||||||
logger.error("UnsupportedEncodingException", e);
|
.replaceAll("%5C", "/") // 恢复反斜杠
|
||||||
urlPrefix = baseUrl + pdfFolder;
|
.replaceAll("\\+", "%20"); // 空格处理
|
||||||
}
|
// 构建URL:使用_作为分隔符(这是kkFileView压缩包预览的常见格式)
|
||||||
return urlPrefix + "/" + index + PDF2JPG_IMAGE_FORMAT;
|
String url = baseUrl + encodedPath + "/" + index + PDF2JPG_IMAGE_FORMAT;
|
||||||
|
return url;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -215,225 +190,20 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
* @param pdfFilePath pdf文件路径
|
* @param pdfFilePath pdf文件路径
|
||||||
* @return 图片访问集合
|
* @return 图片访问集合
|
||||||
*/
|
*/
|
||||||
private List<String> loadPdf2jpgCache(String pdfFilePath) {
|
public List<String> loadPdf2jpgCache(String pdfFilePath) { // 移除 static 修饰符
|
||||||
List<String> imageUrls = new ArrayList<>();
|
List<String> imageUrls = new ArrayList<>();
|
||||||
Integer imageCount = this.getPdf2jpgCache(pdfFilePath);
|
Integer imageCount = this.getPdf2jpgCache(pdfFilePath); // 使用 this. 调用
|
||||||
if (Objects.isNull(imageCount)) {
|
if (Objects.isNull(imageCount)) {
|
||||||
return imageUrls;
|
return imageUrls;
|
||||||
}
|
}
|
||||||
IntStream.range(0, imageCount).forEach(i -> {
|
IntStream.range(0, imageCount).forEach(i -> {
|
||||||
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i);
|
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, i); // 使用 this. 调用
|
||||||
imageUrls.add(imageUrl);
|
imageUrls.add(imageUrl);
|
||||||
});
|
});
|
||||||
return imageUrls;
|
return imageUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* pdf文件转换成jpg图片集
|
|
||||||
* fileNameFilePath pdf文件路径
|
|
||||||
* pdfFilePath pdf输出文件路径
|
|
||||||
* pdfName pdf文件名称
|
|
||||||
* loadPdf2jpgCache 图片访问集合
|
|
||||||
*/
|
|
||||||
public List<String> pdf2jpg(String fileNameFilePath, String pdfFilePath, String pdfName, FileAttribute fileAttribute) throws Exception {
|
|
||||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
|
||||||
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
|
|
||||||
String filePassword = fileAttribute.getFilePassword();
|
|
||||||
PDDocument doc;
|
|
||||||
final String[] pdfPassword = {null};
|
|
||||||
final int[] pageCount = new int[1];
|
|
||||||
if (!forceUpdatedCache) {
|
|
||||||
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
|
|
||||||
if (!CollectionUtils.isEmpty(cacheResult)) {
|
|
||||||
return cacheResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<String> imageUrls = new ArrayList<>();
|
|
||||||
File pdfFile = new File(fileNameFilePath);
|
|
||||||
if (!pdfFile.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int index = pdfFilePath.lastIndexOf(".");
|
|
||||||
String folder = pdfFilePath.substring(0, index);
|
|
||||||
File path = new File(folder);
|
|
||||||
if (!path.exists() && !path.mkdirs()) {
|
|
||||||
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
doc = Loader.loadPDF(pdfFile, filePassword);
|
|
||||||
doc.setResourceCache(new NotResourceCache());
|
|
||||||
pageCount[0] = doc.getNumberOfPages();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
|
||||||
for (Throwable throwable : throwableArray) {
|
|
||||||
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
|
||||||
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
|
||||||
pdfPassword[0] = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!PDF_PASSWORD_MSG.equals(pdfPassword[0])) { //该文件异常 错误原因非密码原因输出错误
|
|
||||||
logger.error("Convert pdf exception, pdfFilePath:{}", pdfFilePath, e);
|
|
||||||
}
|
|
||||||
throw new Exception(e);
|
|
||||||
}
|
|
||||||
Callable <List<String>> call = () -> {
|
|
||||||
try {
|
|
||||||
String imageFilePath;
|
|
||||||
BufferedImage image = null;
|
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(doc);
|
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
|
||||||
for (int pageIndex = 0; pageIndex < pageCount[0]; pageIndex++) {
|
|
||||||
imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
|
|
||||||
image = pdfRenderer.renderImageWithDPI(pageIndex, ConfigConstants.getPdf2JpgDpi(), ImageType.RGB);
|
|
||||||
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
|
|
||||||
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, pageIndex);
|
|
||||||
imageUrls.add(imageUrl);
|
|
||||||
}
|
|
||||||
image.flush();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new Exception(e);
|
|
||||||
} finally {
|
|
||||||
doc.close();
|
|
||||||
}
|
|
||||||
return imageUrls;
|
|
||||||
};
|
|
||||||
Future<List<String>> result = pool.submit(call);
|
|
||||||
int pdftimeout;
|
|
||||||
if(pageCount[0] <=50){
|
|
||||||
pdftimeout = ConfigConstants.getPdfTimeout();
|
|
||||||
}else if(pageCount[0] <=200){
|
|
||||||
pdftimeout = ConfigConstants.getPdfTimeout80();
|
|
||||||
}else {
|
|
||||||
pdftimeout = ConfigConstants.getPdfTimeout200();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
result.get(pdftimeout, TimeUnit.SECONDS);
|
|
||||||
// 如果在超时时间内,没有数据返回:则抛出TimeoutException异常
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
throw new Exception(e);
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
throw new Exception("overtime");
|
|
||||||
} finally {
|
|
||||||
//关闭
|
|
||||||
doc.close();
|
|
||||||
}
|
|
||||||
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) { //加密文件 判断是否启用缓存命令
|
|
||||||
this.addPdf2jpgCache(pdfFilePath, pageCount[0]);
|
|
||||||
}
|
|
||||||
return imageUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* cad文件转pdf
|
|
||||||
*
|
|
||||||
* @param inputFilePath cad文件路径
|
|
||||||
* @param outputFilePath pdf输出文件路径
|
|
||||||
* @return 转换是否成功
|
|
||||||
*/
|
|
||||||
public String cadToPdf(String inputFilePath, String outputFilePath, String cadPreviewType, FileAttribute fileAttribute) throws Exception {
|
|
||||||
final InterruptionTokenSource source = new InterruptionTokenSource();//CAD延时
|
|
||||||
final SvgOptions SvgOptions = new SvgOptions();
|
|
||||||
final PdfOptions pdfOptions = new PdfOptions();
|
|
||||||
final TiffOptions TiffOptions = new TiffOptions(TiffExpectedFormat.TiffJpegRgb);
|
|
||||||
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
|
|
||||||
int index = outputFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
|
|
||||||
String folder = outputFilePath.substring(0, index);
|
|
||||||
File path = new File(folder);
|
|
||||||
//目录不存在 创建新的目录
|
|
||||||
if (!path.exists()) {
|
|
||||||
path.mkdirs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File outputFile = new File(outputFilePath);
|
|
||||||
try {
|
|
||||||
LoadOptions opts = new LoadOptions();
|
|
||||||
opts.setSpecifiedEncoding(CodePages.SimpChinese);
|
|
||||||
final Image cadImage = Image.load(inputFilePath, opts);
|
|
||||||
try {
|
|
||||||
RasterizationQuality rasterizationQuality = new RasterizationQuality();
|
|
||||||
rasterizationQuality.setArc(RasterizationQualityValue.High);
|
|
||||||
rasterizationQuality.setHatch(RasterizationQualityValue.High);
|
|
||||||
rasterizationQuality.setText(RasterizationQualityValue.High);
|
|
||||||
rasterizationQuality.setOle(RasterizationQualityValue.High);
|
|
||||||
rasterizationQuality.setObjectsPrecision(RasterizationQualityValue.High);
|
|
||||||
rasterizationQuality.setTextThicknessNormalization(true);
|
|
||||||
CadRasterizationOptions cadRasterizationOptions = new CadRasterizationOptions();
|
|
||||||
cadRasterizationOptions.setBackgroundColor(Color.getWhite());
|
|
||||||
cadRasterizationOptions.setPageWidth(cadImage.getWidth());
|
|
||||||
cadRasterizationOptions.setPageHeight(cadImage.getHeight());
|
|
||||||
cadRasterizationOptions.setUnitType(cadImage.getUnitType());
|
|
||||||
cadRasterizationOptions.setAutomaticLayoutsScaling(false);
|
|
||||||
cadRasterizationOptions.setNoScaling(false);
|
|
||||||
cadRasterizationOptions.setQuality(rasterizationQuality);
|
|
||||||
cadRasterizationOptions.setDrawType(CadDrawTypeMode.UseObjectColor);
|
|
||||||
cadRasterizationOptions.setExportAllLayoutContent(true);
|
|
||||||
cadRasterizationOptions.setVisibilityMode(VisibilityMode.AsScreen);
|
|
||||||
switch (cadPreviewType) { //新增格式方法
|
|
||||||
case "svg":
|
|
||||||
SvgOptions.setVectorRasterizationOptions(cadRasterizationOptions);
|
|
||||||
SvgOptions.setInterruptionToken(source.getToken());
|
|
||||||
break;
|
|
||||||
case "pdf":
|
|
||||||
pdfOptions.setVectorRasterizationOptions(cadRasterizationOptions);
|
|
||||||
pdfOptions.setInterruptionToken(source.getToken());
|
|
||||||
break;
|
|
||||||
case "tif":
|
|
||||||
TiffOptions.setVectorRasterizationOptions(cadRasterizationOptions);
|
|
||||||
TiffOptions.setInterruptionToken(source.getToken());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Callable<String> call = () -> {
|
|
||||||
try (OutputStream stream = new FileOutputStream(outputFile)) {
|
|
||||||
switch (cadPreviewType) {
|
|
||||||
case "svg":
|
|
||||||
cadImage.save(stream, SvgOptions);
|
|
||||||
break;
|
|
||||||
case "pdf":
|
|
||||||
cadImage.save(stream, pdfOptions);
|
|
||||||
break;
|
|
||||||
case "tif":
|
|
||||||
cadImage.save(stream, TiffOptions);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("CADFileNotFoundException,inputFilePath:{}", inputFilePath, e);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
cadImage.dispose();
|
|
||||||
source.interrupt(); //结束任务
|
|
||||||
source.dispose();
|
|
||||||
}
|
|
||||||
return "true";
|
|
||||||
};
|
|
||||||
Future<String> result = pool.submit(call);
|
|
||||||
try {
|
|
||||||
result.get(Long.parseLong(ConfigConstants.getCadTimeout()), TimeUnit.SECONDS);
|
|
||||||
// 如果在超时时间内,没有数据返回:则抛出TimeoutException异常
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
logger.error("CAD转换文件异常:", e);
|
|
||||||
return null;
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
logger.error("CAD转换在尝试取得任务结果时出错:", e);
|
|
||||||
return null;
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
logger.error("CAD转换时间超时:", e);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
source.interrupt(); //结束任务
|
|
||||||
source.dispose();
|
|
||||||
cadImage.dispose();
|
|
||||||
// pool.shutdownNow();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
source.dispose();
|
|
||||||
cadImage.dispose();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
source.dispose();
|
|
||||||
}
|
|
||||||
return "true";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param str 原字符串(待截取原串)
|
* @param str 原字符串(待截取原串)
|
||||||
@@ -474,7 +244,7 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
|
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
|
||||||
if (isCompressFile) { //判断是否使用特定压缩包符号
|
if (isCompressFile) { //判断是否使用特定压缩包符号
|
||||||
try {
|
try {
|
||||||
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
originFileName = URLDecoder.decode(compressFilePath, uriEncoding); //转义的文件名 解下出原始文件名
|
||||||
attribute.setSkipDownLoad(true);
|
attribute.setSkipDownLoad(true);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
logger.error("Failed to decode file name: {}", originFileName, e);
|
logger.error("Failed to decode file name: {}", originFileName, e);
|
||||||
@@ -484,12 +254,23 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
try {
|
try {
|
||||||
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
logger.error("Failed to decode file name: {}", originFileName, e);
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义
|
url = Objects.requireNonNull(WebUtils.encodeUrlFileName(url))
|
||||||
|
.replaceAll("\\+", "%20")
|
||||||
|
.replaceAll("%3A", ":")
|
||||||
|
.replaceAll("%2F", "/")
|
||||||
|
.replaceAll("%3F", "?")
|
||||||
|
.replaceAll("%26", "&")
|
||||||
|
.replaceAll("%3D", "=");
|
||||||
|
|
||||||
}
|
}
|
||||||
originFileName = KkFileUtils.htmlEscape(originFileName); //文件名处理
|
originFileName = KkFileUtils.htmlEscape(originFileName); //文件名处理
|
||||||
|
if (!KkFileUtils.validateFileNameLength(originFileName)) {
|
||||||
|
// 处理逻辑:抛出异常、记录日志、返回错误等
|
||||||
|
throw new IllegalArgumentException("文件名超过系统限制");
|
||||||
|
}
|
||||||
boolean isHtmlView = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx") || suffix.equalsIgnoreCase("csv") || suffix.equalsIgnoreCase("xlsm") || suffix.equalsIgnoreCase("xlt") || suffix.equalsIgnoreCase("xltm") || suffix.equalsIgnoreCase("et") || suffix.equalsIgnoreCase("ett") || suffix.equalsIgnoreCase("xlam");
|
boolean isHtmlView = suffix.equalsIgnoreCase("xls") || suffix.equalsIgnoreCase("xlsx") || suffix.equalsIgnoreCase("csv") || suffix.equalsIgnoreCase("xlsm") || suffix.equalsIgnoreCase("xlt") || suffix.equalsIgnoreCase("xltm") || suffix.equalsIgnoreCase("et") || suffix.equalsIgnoreCase("ett") || suffix.equalsIgnoreCase("xlam");
|
||||||
String cacheFilePrefixName = null;
|
String cacheFilePrefixName = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ public interface FilePreview {
|
|||||||
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
|
String NOT_SUPPORTED_FILE_PAGE = "fileNotSupported";
|
||||||
String XLSX_FILE_PREVIEW_PAGE = "officeweb";
|
String XLSX_FILE_PREVIEW_PAGE = "officeweb";
|
||||||
String CSV_FILE_PREVIEW_PAGE = "csv";
|
String CSV_FILE_PREVIEW_PAGE = "csv";
|
||||||
|
String MSG_FILE_PREVIEW_PAGE = "msg";
|
||||||
|
String HEIC_FILE_PREVIEW_PAGE = "heic";
|
||||||
|
String CADVIEWER_FILE_PREVIEW_PAGE = "cadviewer";
|
||||||
|
String WAITING_FILE_PREVIEW_PAGE = "waiting";
|
||||||
|
|
||||||
String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);
|
String filePreviewHandle(String url, Model model, FileAttribute fileAttribute);
|
||||||
}
|
}
|
||||||
|
|||||||
607
server/src/main/java/cn/keking/service/Mediatomp4Service.java
Normal file
607
server/src/main/java/cn/keking/service/Mediatomp4Service.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ public class OfficeToPdfService {
|
|||||||
|
|
||||||
|
|
||||||
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
|
public static void converterFile(File inputFile, String outputFilePath_end, FileAttribute fileAttribute) throws OfficeException {
|
||||||
|
Instant startTime = Instant.now();
|
||||||
File outputFile = new File(outputFilePath_end);
|
File outputFile = new File(outputFilePath_end);
|
||||||
// 假如目标路径不存在,则新建该路径
|
// 假如目标路径不存在,则新建该路径
|
||||||
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
|
if (!outputFile.getParentFile().exists() && !outputFile.getParentFile().mkdirs()) {
|
||||||
@@ -65,7 +68,33 @@ public class OfficeToPdfService {
|
|||||||
} else {
|
} else {
|
||||||
builder = LocalConverter.builder().storeProperties(customProperties);
|
builder = LocalConverter.builder().storeProperties(customProperties);
|
||||||
}
|
}
|
||||||
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);
|
return inputFilePath.substring(inputFilePath.lastIndexOf(".") + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
855
server/src/main/java/cn/keking/service/PdfToJpgService.java
Normal file
855
server/src/main/java/cn/keking/service/PdfToJpgService.java
Normal file
File diff suppressed because it is too large
Load Diff
473
server/src/main/java/cn/keking/service/TifToPdfService.java
Normal file
473
server/src/main/java/cn/keking/service/TifToPdfService.java
Normal 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转换服务已关闭");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
package cn.keking.service.cache.impl;
|
package cn.keking.service.cache.impl;
|
||||||
|
|
||||||
import cn.keking.service.cache.CacheService;
|
import cn.keking.service.cache.CacheService;
|
||||||
import org.redisson.Redisson;
|
|
||||||
import org.redisson.api.RBlockingQueue;
|
import org.redisson.api.RBlockingQueue;
|
||||||
import org.redisson.api.RMapCache;
|
import org.redisson.api.RMapCache;
|
||||||
import org.redisson.api.RedissonClient;
|
import org.redisson.api.RedissonClient;
|
||||||
import org.redisson.config.Config;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -23,8 +21,9 @@ public class CacheServiceRedisImpl implements CacheService {
|
|||||||
|
|
||||||
private final RedissonClient redissonClient;
|
private final RedissonClient redissonClient;
|
||||||
|
|
||||||
public CacheServiceRedisImpl(Config config) {
|
// 直接注入 Spring 容器中的 RedissonClient Bean
|
||||||
this.redissonClient = Redisson.create(config);
|
public CacheServiceRedisImpl(RedissonClient redissonClient) {
|
||||||
|
this.redissonClient = redissonClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package cn.keking.service.impl;
|
|||||||
import cn.keking.config.ConfigConstants;
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
|
import cn.keking.service.CadToPdfService;
|
||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
import cn.keking.utils.DownloadUtils;
|
import cn.keking.utils.*;
|
||||||
import cn.keking.utils.KkFileUtils;
|
|
||||||
import cn.keking.utils.WebUtils;
|
|
||||||
import cn.keking.web.filter.BaseUrlFilter;
|
import cn.keking.web.filter.BaseUrlFilter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -15,7 +14,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author chenjh
|
* @author chenjh
|
||||||
@@ -29,62 +30,157 @@ public class CadFilePreviewImpl implements FilePreview {
|
|||||||
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
||||||
|
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
|
private final CadToPdfService cadtopdfservice;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
|
private final OfficeFilePreviewImpl officefilepreviewimpl;
|
||||||
|
|
||||||
public CadFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
// 用于处理回调的线程池
|
||||||
|
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
|
public CadFilePreviewImpl(FileHandlerService fileHandlerService,
|
||||||
|
OtherFilePreviewImpl otherFilePreview,
|
||||||
|
CadToPdfService cadtopdfservice,
|
||||||
|
OfficeFilePreviewImpl officefilepreviewimpl) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
|
this.cadtopdfservice = cadtopdfservice;
|
||||||
|
this.officefilepreviewimpl = officefilepreviewimpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
// 预览Type,参数传了就取参数的,没传取系统默认
|
// 预览Type,参数传了就取参数的,没传取系统默认
|
||||||
String officePreviewType = fileAttribute.getOfficePreviewType() == null ? ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
|
String officePreviewType = fileAttribute.getOfficePreviewType() == null ?
|
||||||
String baseUrl = BaseUrlFilter.getBaseUrl();
|
ConfigConstants.getOfficePreviewType() : fileAttribute.getOfficePreviewType();
|
||||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
String fileName = fileAttribute.getName();
|
String fileName = fileAttribute.getName();
|
||||||
String cadPreviewType = ConfigConstants.getCadPreviewType();
|
String cadPreviewType = ConfigConstants.getCadPreviewType();
|
||||||
String cacheName = fileAttribute.getCacheName();
|
String cacheName = fileAttribute.getCacheName();
|
||||||
String outFilePath = fileAttribute.getOutFilePath();
|
String outFilePath = fileAttribute.getOutFilePath();
|
||||||
|
|
||||||
|
// 查询转换状态
|
||||||
|
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
|
||||||
|
if (statusResult != null) {
|
||||||
|
return statusResult;
|
||||||
|
}
|
||||||
|
|
||||||
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
|
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
|
||||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName)
|
||||||
|
|| !ConfigConstants.isCacheEnabled()) {
|
||||||
|
|
||||||
|
// 检查是否已在转换中
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||||
if (response.isFailure()) {
|
if (response.isFailure()) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
}
|
}
|
||||||
|
|
||||||
String filePath = response.getContent();
|
String filePath = response.getContent();
|
||||||
String imageUrls = null;
|
int refreshSchedule = ConfigConstants.getTime();
|
||||||
if (StringUtils.hasText(outFilePath)) {
|
if (StringUtils.hasText(outFilePath)) {
|
||||||
try {
|
try {
|
||||||
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
|
// 启动异步转换,并添加回调处理
|
||||||
|
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
|
||||||
|
// 返回等待页面
|
||||||
|
model.addAttribute("fileName", fileName);
|
||||||
|
model.addAttribute("time", refreshSchedule);
|
||||||
|
model.addAttribute("message", "文件正在转换中,请稍候...");
|
||||||
|
return WAITING_FILE_PREVIEW_PAGE;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to convert CAD file: {}", filePath, e);
|
logger.error("Failed to start CAD conversion: {}", filePath, e);
|
||||||
}
|
|
||||||
if (imageUrls == null) {
|
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员");
|
||||||
}
|
}
|
||||||
//是否保留CAD源文件
|
|
||||||
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)) {
|
if ("tif".equalsIgnoreCase(cadPreviewType)) {
|
||||||
model.addAttribute("currentUrl", cacheName);
|
model.addAttribute("currentUrl", cacheName);
|
||||||
return TIFF_FILE_PREVIEW_PAGE;
|
return TIFF_FILE_PREVIEW_PAGE;
|
||||||
} else if ("svg".equalsIgnoreCase(cadPreviewType)) {
|
} else if ("svg".equalsIgnoreCase(cadPreviewType)) {
|
||||||
model.addAttribute("currentUrl", cacheName);
|
model.addAttribute("currentUrl", cacheName);
|
||||||
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);
|
model.addAttribute("pdfUrl", cacheName);
|
||||||
return PDF_FILE_PREVIEW_PAGE;
|
return PDF_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,9 @@ public class CodeFilePreviewImpl implements FilePreview {
|
|||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
filePreviewHandle.filePreviewHandle(url, model, fileAttribute);
|
filePreviewHandle.filePreviewHandle(url, model, fileAttribute);
|
||||||
|
String suffix = fileAttribute.getSuffix();
|
||||||
|
boolean isHtmlFile = suffix.equalsIgnoreCase("htm") || suffix.equalsIgnoreCase("html") || suffix.equalsIgnoreCase("shtml");
|
||||||
|
model.addAttribute("isHtmlFile", isHtmlFile);
|
||||||
return CODE_FILE_PREVIEW_PAGE;
|
return CODE_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package cn.keking.service.impl;
|
package cn.keking.service.impl;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
@@ -10,6 +11,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -32,17 +34,26 @@ public class CommonPreviewImpl implements FilePreview {
|
|||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
// 不是http开头,浏览器不能直接访问,需下载到本地
|
// 不是http开头,浏览器不能直接访问,需下载到本地
|
||||||
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
|
||||||
|
String fileName = fileAttribute.getName(); //获取原始文件名
|
||||||
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
|
||||||
if (url != null && !url.toLowerCase().startsWith("http")) {
|
if (url != null && !url.toLowerCase().startsWith("http")) {
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, null);
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||||
if (response.isFailure()) {
|
if (response.isFailure()) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
} else {
|
} else {
|
||||||
String file = fileHandlerService.getRelativePath(response.getContent());
|
String file = fileHandlerService.getRelativePath(response.getContent());
|
||||||
model.addAttribute("currentUrl", file);
|
model.addAttribute("currentUrl", file);
|
||||||
|
if (ConfigConstants.isCacheEnabled()) {
|
||||||
|
fileHandlerService.addConvertedFile(fileName, file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
model.addAttribute("currentUrl", url);
|
model.addAttribute("currentUrl", url);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
model.addAttribute("currentUrl", fileHandlerService.getConvertedFile(fileName));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,25 @@ import cn.keking.model.FileType;
|
|||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
|
import cn.keking.service.Mediatomp4Service;
|
||||||
import cn.keking.utils.DownloadUtils;
|
import cn.keking.utils.DownloadUtils;
|
||||||
import org.bytedeco.ffmpeg.global.avcodec;
|
import cn.keking.utils.FileConvertStatusManager;
|
||||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
|
||||||
import org.bytedeco.javacv.Frame;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author : kl
|
* @author : kl
|
||||||
* @authorboke : kailing.pub
|
* @authorboke : kailing.pub
|
||||||
* @create : 2018-03-25 上午11:58
|
* @create : 2018-03-25 上午11:58
|
||||||
* @description:
|
* @description: 异步视频文件预览实现
|
||||||
**/
|
**/
|
||||||
@Service
|
@Service
|
||||||
public class MediaFilePreviewImpl implements FilePreview {
|
public class MediaFilePreviewImpl implements FilePreview {
|
||||||
@@ -31,11 +32,20 @@ public class MediaFilePreviewImpl implements FilePreview {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
|
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
private static final String mp4 = "mp4";
|
private final Mediatomp4Service mediatomp4Service;
|
||||||
|
private final OfficeFilePreviewImpl officefilepreviewimpl;
|
||||||
|
|
||||||
public MediaFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
// 用于处理回调的线程池
|
||||||
|
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
|
public MediaFilePreviewImpl(FileHandlerService fileHandlerService,
|
||||||
|
OtherFilePreviewImpl otherFilePreview,
|
||||||
|
Mediatomp4Service mediatomp4Service,
|
||||||
|
OfficeFilePreviewImpl officefilepreviewimpl) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
|
this.mediatomp4Service = mediatomp4Service;
|
||||||
|
this.officefilepreviewimpl = officefilepreviewimpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -46,122 +56,140 @@ public class MediaFilePreviewImpl implements FilePreview {
|
|||||||
String outFilePath = fileAttribute.getOutFilePath();
|
String outFilePath = fileAttribute.getOutFilePath();
|
||||||
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
FileType type = fileAttribute.getType();
|
FileType type = fileAttribute.getType();
|
||||||
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES; //获取支持的转换格式
|
|
||||||
|
// 检查是否是需要转换的视频格式
|
||||||
boolean mediaTypes = false;
|
boolean mediaTypes = false;
|
||||||
|
String[] mediaTypesConvert = FileType.MEDIA_CONVERT_TYPES;
|
||||||
for (String temp : mediaTypesConvert) {
|
for (String temp : mediaTypesConvert) {
|
||||||
if (suffix.equals(temp)) {
|
if (suffix.equalsIgnoreCase(temp)) {
|
||||||
mediaTypes = true;
|
mediaTypes = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!url.toLowerCase().startsWith("http") || checkNeedConvert(mediaTypes)) { //不是http协议的 // 开启转换方式并是支持转换格式的
|
// 查询转换状态
|
||||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) { //查询是否开启缓存
|
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
if (statusResult != null) {
|
||||||
if (response.isFailure()) {
|
return statusResult;
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
}
|
||||||
|
// 非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 {
|
try {
|
||||||
if (mediaTypes) {
|
// 启动异步转换,并添加回调处理
|
||||||
convertedUrl = convertToMp4(filePath, outFilePath, fileAttribute);
|
startAsyncConversion(filePath, outFilePath, cacheName, fileAttribute);
|
||||||
} else {
|
int refreshSchedule = ConfigConstants.getTime();
|
||||||
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
|
// 返回等待页面
|
||||||
}
|
model.addAttribute("fileName", fileName);
|
||||||
|
model.addAttribute("time", refreshSchedule);
|
||||||
|
model.addAttribute("message", "视频文件正在转换中,请稍候...");
|
||||||
|
return WAITING_FILE_PREVIEW_PAGE;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to convert media file: {}", filePath, e);
|
logger.error("Failed to start video conversion: {}", filePath, e);
|
||||||
}
|
|
||||||
if (convertedUrl == null) {
|
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
|
||||||
}
|
}
|
||||||
if (ConfigConstants.isCacheEnabled()) {
|
|
||||||
// 加入缓存
|
|
||||||
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
|
|
||||||
}
|
|
||||||
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
|
|
||||||
} else {
|
} else {
|
||||||
model.addAttribute("mediaUrl", fileHandlerService.listConvertedFiles().get(cacheName));
|
// 不需要转换的文件,直接返回
|
||||||
|
model.addAttribute("mediaUrl", fileHandlerService.getRelativePath(outFilePath));
|
||||||
|
return MEDIA_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
return MEDIA_FILE_PREVIEW_PAGE;
|
|
||||||
}
|
}
|
||||||
if (type.equals(FileType.MEDIA)) { // 支持输出 只限默认格式
|
// HTTP协议的媒体文件,直接播放
|
||||||
|
if (type.equals(FileType.MEDIA)) {
|
||||||
model.addAttribute("mediaUrl", url);
|
model.addAttribute("mediaUrl", url);
|
||||||
return MEDIA_FILE_PREVIEW_PAGE;
|
return MEDIA_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "系统还不支持该格式文件的在线预览");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查视频文件转换是否已开启,以及当前文件是否需要转换
|
* 启动异步转换,并在转换完成后处理后续操作
|
||||||
*
|
*/
|
||||||
* @return
|
private void startAsyncConversion(String filePath, String outFilePath,
|
||||||
|
String cacheName, FileAttribute fileAttribute) {
|
||||||
|
// 启动异步转换
|
||||||
|
CompletableFuture<Boolean> conversionFuture = mediatomp4Service.convertToMp4Async(
|
||||||
|
filePath,
|
||||||
|
outFilePath,
|
||||||
|
cacheName,
|
||||||
|
fileAttribute
|
||||||
|
);
|
||||||
|
|
||||||
|
// 添加转换完成后的回调
|
||||||
|
conversionFuture.whenCompleteAsync((success, throwable) -> {
|
||||||
|
if (success != null && success) {
|
||||||
|
try {
|
||||||
|
// 1. 是否保留源文件(只在转换成功后才删除)
|
||||||
|
if (!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
|
||||||
|
KkFileUtils.deleteFileByPath(filePath);
|
||||||
|
}
|
||||||
|
// 2. 加入视频缓存(只在转换成功后才添加)
|
||||||
|
if (ConfigConstants.isCacheEnabled()) {
|
||||||
|
fileHandlerService.addConvertedMedias(cacheName,
|
||||||
|
fileHandlerService.getRelativePath(outFilePath));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("视频转换后续处理失败: {}", filePath, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 转换失败,保留源文件供排查问题
|
||||||
|
logger.error("视频转换失败,保留源文件: {}", filePath);
|
||||||
|
if (throwable != null) {
|
||||||
|
logger.error("视频转换失败原因: ", throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, callbackExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件大小是否超过限制
|
||||||
|
*/
|
||||||
|
private boolean isFileSizeExceeded(String filePath) {
|
||||||
|
try {
|
||||||
|
File inputFile = new File(filePath);
|
||||||
|
if (inputFile.exists()) {
|
||||||
|
long fileSizeMB = inputFile.length() / (1024 * 1024);
|
||||||
|
int maxSizeMB = ConfigConstants.getMediaConvertMaxSize();
|
||||||
|
|
||||||
|
if (fileSizeMB > maxSizeMB) {
|
||||||
|
logger.warn("视频文件大小超过限制: {}MB > {}MB", fileSizeMB, maxSizeMB);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("检查文件大小时出错: {}", filePath, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否需要转换
|
||||||
*/
|
*/
|
||||||
private boolean checkNeedConvert(boolean mediaTypes) {
|
private boolean checkNeedConvert(boolean mediaTypes) {
|
||||||
//1.检查开关是否开启
|
// 检查转换开关是否开启
|
||||||
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
|
if ("true".equals(ConfigConstants.getMediaConvertDisable())) {
|
||||||
return mediaTypes;
|
return mediaTypes;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private static String convertToMp4(String filePath, String outFilePath, FileAttribute fileAttribute) throws Exception {
|
|
||||||
FFmpegFrameGrabber frameGrabber = FFmpegFrameGrabber.createDefault(filePath);
|
|
||||||
Frame captured_frame;
|
|
||||||
FFmpegFrameRecorder recorder = null;
|
|
||||||
try {
|
|
||||||
File desFile = new File(outFilePath);
|
|
||||||
//判断一下防止重复转换
|
|
||||||
if (desFile.exists()) {
|
|
||||||
return outFilePath;
|
|
||||||
}
|
|
||||||
if (fileAttribute.isCompressFile()) { //判断 是压缩包的创建新的目录
|
|
||||||
int index = outFilePath.lastIndexOf("/"); //截取最后一个斜杠的前面的内容
|
|
||||||
String folder = outFilePath.substring(0, index);
|
|
||||||
File path = new File(folder);
|
|
||||||
//目录不存在 创建新的目录
|
|
||||||
if (!path.exists()) {
|
|
||||||
path.mkdirs();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frameGrabber.start();
|
|
||||||
recorder = new FFmpegFrameRecorder(outFilePath, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
|
|
||||||
// recorder.setImageHeight(640);
|
|
||||||
// recorder.setImageWidth(480);
|
|
||||||
recorder.setFormat(mp4);
|
|
||||||
recorder.setFrameRate(frameGrabber.getFrameRate());
|
|
||||||
recorder.setSampleRate(frameGrabber.getSampleRate());
|
|
||||||
//视频编码属性配置 H.264 H.265 MPEG
|
|
||||||
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
|
||||||
//设置视频比特率,单位:b
|
|
||||||
recorder.setVideoBitrate(frameGrabber.getVideoBitrate());
|
|
||||||
recorder.setAspectRatio(frameGrabber.getAspectRatio());
|
|
||||||
// 设置音频通用编码格式
|
|
||||||
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
|
||||||
//设置音频比特率,单位:b (比特率越高,清晰度/音质越好,当然文件也就越大 128000 = 182kb)
|
|
||||||
recorder.setAudioBitrate(frameGrabber.getAudioBitrate());
|
|
||||||
recorder.setAudioOptions(frameGrabber.getAudioOptions());
|
|
||||||
recorder.setAudioChannels(frameGrabber.getAudioChannels());
|
|
||||||
recorder.start();
|
|
||||||
while (true) {
|
|
||||||
captured_frame = frameGrabber.grabFrame();
|
|
||||||
if (captured_frame == null) {
|
|
||||||
System.out.println("转码完成:" + filePath);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
recorder.record(captured_frame);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("Failed to convert video file to mp4: {}", filePath, e);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
if (recorder != null) { //关闭
|
|
||||||
recorder.stop();
|
|
||||||
recorder.close();
|
|
||||||
}
|
|
||||||
frameGrabber.stop();
|
|
||||||
frameGrabber.close();
|
|
||||||
}
|
|
||||||
return outFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,20 +6,25 @@ import cn.keking.model.ReturnResponse;
|
|||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
import cn.keking.service.OfficeToPdfService;
|
import cn.keking.service.OfficeToPdfService;
|
||||||
|
import cn.keking.service.PdfToJpgService;
|
||||||
import cn.keking.utils.DownloadUtils;
|
import cn.keking.utils.DownloadUtils;
|
||||||
|
import cn.keking.utils.FileConvertStatusManager;
|
||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import cn.keking.utils.OfficeUtils;
|
import cn.keking.utils.OfficeUtils;
|
||||||
import cn.keking.utils.WebUtils;
|
import cn.keking.utils.WebUtils;
|
||||||
import cn.keking.web.filter.BaseUrlFilter;
|
import cn.keking.web.filter.BaseUrlFilter;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
|
||||||
import org.jodconverter.core.office.OfficeException;
|
import org.jodconverter.core.office.OfficeException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by kl on 2018/1/17.
|
* Created by kl on 2018/1/17.
|
||||||
@@ -28,18 +33,23 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class OfficeFilePreviewImpl implements FilePreview {
|
public class OfficeFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OfficeFilePreviewImpl.class);
|
||||||
public static final String OFFICE_PREVIEW_TYPE_IMAGE = "image";
|
public static final String OFFICE_PREVIEW_TYPE_IMAGE = "image";
|
||||||
public static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
public static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
||||||
private static final String OFFICE_PASSWORD_MSG = "password";
|
|
||||||
|
// 用于处理回调的线程池
|
||||||
|
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OfficeToPdfService officeToPdfService;
|
private final OfficeToPdfService officeToPdfService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
|
private final PdfToJpgService pdftojpgservice;
|
||||||
|
|
||||||
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview) {
|
public OfficeFilePreviewImpl(FileHandlerService fileHandlerService, OfficeToPdfService officeToPdfService, OtherFilePreviewImpl otherFilePreview, PdfToJpgService pdftojpgservice) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.officeToPdfService = officeToPdfService;
|
this.officeToPdfService = officeToPdfService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
|
this.pdftojpgservice = pdftojpgservice;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -51,12 +61,19 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
|||||||
String suffix = fileAttribute.getSuffix(); //获取文件后缀
|
String suffix = fileAttribute.getSuffix(); //获取文件后缀
|
||||||
String fileName = fileAttribute.getName(); //获取文件原始名称
|
String fileName = fileAttribute.getName(); //获取文件原始名称
|
||||||
String filePassword = fileAttribute.getFilePassword(); //获取密码
|
String filePassword = fileAttribute.getFilePassword(); //获取密码
|
||||||
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
|
||||||
boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html
|
boolean isHtmlView = fileAttribute.isHtmlView(); //xlsx 转换成html
|
||||||
String cacheName = fileAttribute.getCacheName(); //转换后的文件名
|
String cacheName = fileAttribute.getCacheName(); //转换后的文件名
|
||||||
String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径
|
String outFilePath = fileAttribute.getOutFilePath(); //转换后生成文件的路径
|
||||||
|
|
||||||
|
// 查询转换状态
|
||||||
|
String convertStatusResult = checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
|
||||||
|
if (convertStatusResult != null) {
|
||||||
|
return convertStatusResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!officePreviewType.equalsIgnoreCase("html")) {
|
if (!officePreviewType.equalsIgnoreCase("html")) {
|
||||||
if (ConfigConstants.getOfficeTypeWeb() .equalsIgnoreCase("web")) {
|
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
|
||||||
if (suffix.equalsIgnoreCase("xlsx")) {
|
if (suffix.equalsIgnoreCase("xlsx")) {
|
||||||
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
|
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
|
||||||
return XLSX_FILE_PREVIEW_PAGE;
|
return XLSX_FILE_PREVIEW_PAGE;
|
||||||
@@ -67,14 +84,192 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (forceUpdatedCache|| !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
|
|
||||||
// 下载远程文件到本地,如果文件在本地已存在不会重复下载
|
// 图片预览模式(异步转换)
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
if (!isHtmlView && baseUrl != null && (OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType))) {
|
||||||
if (response.isFailure()) {
|
boolean jiami = false;
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
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();
|
String filePath = response.getContent();
|
||||||
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件
|
|
||||||
|
boolean isPwdProtectedOffice = OfficeUtils.isPwdProtected(filePath); // 判断是否加密文件
|
||||||
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
|
if (isPwdProtectedOffice && !StringUtils.hasLength(filePassword)) {
|
||||||
// 加密文件需要密码
|
// 加密文件需要密码
|
||||||
model.addAttribute("needFilePassword", true);
|
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识别
|
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName)); //输出转义文件名 方便url识别
|
||||||
return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE;
|
return isHtmlView ? EXEL_FILE_PREVIEW_PAGE : PDF_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String getPreviewType(Model model, FileAttribute fileAttribute, String officePreviewType, String pdfName, String outFilePath, FileHandlerService fileHandlerService, String officePreviewTypeImage, OtherFilePreviewImpl otherFilePreview) {
|
/**
|
||||||
String suffix = fileAttribute.getSuffix();
|
* 异步方法
|
||||||
boolean isPPT = suffix.equalsIgnoreCase("ppt") || suffix.equalsIgnoreCase("pptx");
|
*/
|
||||||
List<String> imageUrls = null;
|
public String checkAndHandleConvertStatus(Model model, String fileName, String cacheName, FileAttribute fileAttribute) {
|
||||||
try {
|
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
|
||||||
imageUrls = fileHandlerService.pdf2jpg(outFilePath,outFilePath, pdfName, fileAttribute);
|
int refreshSchedule = ConfigConstants.getTime();
|
||||||
} catch (Exception e) {
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
|
||||||
for (Throwable throwable : throwableArray) {
|
if (status != null) {
|
||||||
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
if (status.getStatus() == FileConvertStatusManager.Status.CONVERTING) {
|
||||||
if (e.getMessage().toLowerCase().contains(OFFICE_PASSWORD_MSG)) {
|
// 正在转换中,返回等待页面
|
||||||
model.addAttribute("needFilePassword", true);
|
model.addAttribute("fileName", fileName);
|
||||||
return EXEL_FILE_PREVIEW_PAGE;
|
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 null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,24 @@ import cn.keking.model.FileAttribute;
|
|||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
|
import cn.keking.service.PdfToJpgService;
|
||||||
import cn.keking.utils.DownloadUtils;
|
import cn.keking.utils.DownloadUtils;
|
||||||
|
import cn.keking.utils.FileConvertStatusManager;
|
||||||
import cn.keking.utils.WebUtils;
|
import cn.keking.utils.WebUtils;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by kl on 2018/1/17.
|
* Created by kl on 2018/1/17.
|
||||||
@@ -22,78 +31,251 @@ import java.util.List;
|
|||||||
@Service
|
@Service
|
||||||
public class PdfFilePreviewImpl implements FilePreview {
|
public class PdfFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(PdfFilePreviewImpl.class);
|
||||||
|
private static final String PDF_PASSWORD_MSG = "password";
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
private static final String PDF_PASSWORD_MSG = "password";
|
private final PdfToJpgService pdftojpgservice;
|
||||||
public PdfFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
private final OfficeFilePreviewImpl officefilepreviewimpl;
|
||||||
|
|
||||||
|
// 用于处理回调的线程池
|
||||||
|
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
|
public PdfFilePreviewImpl(FileHandlerService fileHandlerService,
|
||||||
|
OtherFilePreviewImpl otherFilePreview,
|
||||||
|
OfficeFilePreviewImpl officefilepreviewimpl,
|
||||||
|
PdfToJpgService pdftojpgservice) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
|
this.pdftojpgservice = pdftojpgservice;
|
||||||
|
this.officefilepreviewimpl = officefilepreviewimpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
String pdfName = fileAttribute.getName(); //获取原始文件名
|
String pdfName = fileAttribute.getName(); //获取原始文件名
|
||||||
String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型
|
String officePreviewType = fileAttribute.getOfficePreviewType(); //转换类型
|
||||||
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache(); //是否启用强制更新命令
|
||||||
String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径
|
String outFilePath = fileAttribute.getOutFilePath(); //生成的文件路径
|
||||||
String originFilePath = fileAttribute.getOriginFilePath(); //原始文件路径
|
String originFilePath; //原始文件路径
|
||||||
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) || OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) {
|
String cacheName = pdfName+officePreviewType;
|
||||||
//当文件不存在时,就去下载
|
String filePassword = fileAttribute.getFilePassword(); // 获取密码
|
||||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
|
if("demo.pdf".equals(pdfName)){
|
||||||
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "不能使用该文件名,请更换其他文件名在进行转换");
|
||||||
|
}
|
||||||
|
// 查询转换状态
|
||||||
|
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, pdfName, cacheName, fileAttribute);
|
||||||
|
if (statusResult != null) {
|
||||||
|
return statusResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean jiami=false;
|
||||||
|
if(!ObjectUtils.isEmpty(filePassword)){
|
||||||
|
jiami=pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath);
|
||||||
|
}
|
||||||
|
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType) ||
|
||||||
|
OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_ALL_IMAGES.equals(officePreviewType)) {
|
||||||
|
// 判断之前是否已转换过,如果转换过,直接返回,否则执行转换
|
||||||
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
|
||||||
|
if(jiami){
|
||||||
|
return renderPreview(model, cacheName, outFilePath,
|
||||||
|
officePreviewType, fileAttribute);
|
||||||
|
}
|
||||||
|
// 当文件不存在时,就去下载
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
|
||||||
if (response.isFailure()) {
|
if (response.isFailure()) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
}
|
}
|
||||||
originFilePath = response.getContent();
|
originFilePath = response.getContent();
|
||||||
if (ConfigConstants.isCacheEnabled()) {
|
// 检查文件是否需要密码,但不启动转换
|
||||||
// 加入缓存
|
if (filePassword == null || filePassword.trim().isEmpty()) {
|
||||||
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(originFilePath));
|
// 没有提供密码,先检查文件是否需要密码
|
||||||
|
if (checkIfPdfNeedsPassword(originFilePath, cacheName, pdfName)) {
|
||||||
|
model.addAttribute("needFilePassword", true);
|
||||||
|
model.addAttribute("fileName", pdfName);
|
||||||
|
model.addAttribute("cacheName", pdfName);
|
||||||
|
return EXEL_FILE_PREVIEW_PAGE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
} else {
|
||||||
try {
|
// 处理普通PDF预览(非图片转换)
|
||||||
imageUrls = fileHandlerService.pdf2jpg(originFilePath,outFilePath, pdfName, fileAttribute);
|
return handleRegularPdfPreview(url, model, fileAttribute, pdfName, forceUpdatedCache, outFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查PDF文件是否需要密码(不进行实际转换)
|
||||||
|
*/
|
||||||
|
private boolean checkIfPdfNeedsPassword(String originFilePath, String cacheName, String pdfName) {
|
||||||
|
try {
|
||||||
|
// 尝试用空密码加载PDF,检查是否需要密码
|
||||||
|
File pdfFile = new File(originFilePath);
|
||||||
|
if (!pdfFile.exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用try-with-resources确保资源释放
|
||||||
|
try (org.apache.pdfbox.pdmodel.PDDocument tempDoc = org.apache.pdfbox.Loader.loadPDF(pdfFile, "")) {
|
||||||
|
// 如果能加载成功,说明不需要密码
|
||||||
|
int pageCount = tempDoc.getNumberOfPages();
|
||||||
|
logger.info("PDF文件不需要密码,总页数: {},文件: {}", pageCount, originFilePath);
|
||||||
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
||||||
for (Throwable throwable : throwableArray) {
|
for (Throwable throwable : throwableArray) {
|
||||||
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
||||||
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
||||||
model.addAttribute("needFilePassword", true);
|
FileConvertStatusManager.convertSuccess(cacheName);
|
||||||
return EXEL_FILE_PREVIEW_PAGE;
|
logger.info("PDF文件需要密码: {}", originFilePath);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常,请联系管理员");
|
logger.warn("PDF文件检查异常: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (imageUrls == null || imageUrls.size() < 1) {
|
} catch (Exception e) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "pdf转图片异常,请联系管理员");
|
logger.error("检查PDF密码状态失败: {}", originFilePath, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动异步PDF转换
|
||||||
|
*/
|
||||||
|
private void startAsyncPdfConversion(String originFilePath, String outFilePath,
|
||||||
|
String cacheName, String pdfName,
|
||||||
|
FileAttribute fileAttribute) {
|
||||||
|
// 启动异步转换
|
||||||
|
CompletableFuture<List<String>> conversionFuture = CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
// 更新状态
|
||||||
|
FileConvertStatusManager.startConvert(cacheName);
|
||||||
|
FileConvertStatusManager.updateProgress(cacheName, "正在启动PDF转换", 10);
|
||||||
|
|
||||||
|
List<String> imageUrls = pdftojpgservice.pdf2jpg(originFilePath, outFilePath,
|
||||||
|
fileAttribute);
|
||||||
|
|
||||||
|
if (imageUrls != null && !imageUrls.isEmpty()) {
|
||||||
|
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
|
||||||
|
String filePassword = fileAttribute.getFilePassword();
|
||||||
|
if (ConfigConstants.isCacheEnabled() && (ObjectUtils.isEmpty(filePassword) || usePasswordCache)) {
|
||||||
|
fileHandlerService.addConvertedFile(cacheName, fileHandlerService.getRelativePath(outFilePath));
|
||||||
|
}
|
||||||
|
FileConvertStatusManager.updateProgress(cacheName, "转换完成", 100);
|
||||||
|
// 短暂延迟后清理状态
|
||||||
|
FileConvertStatusManager.convertSuccess(cacheName);
|
||||||
|
return imageUrls;
|
||||||
|
} else {
|
||||||
|
FileConvertStatusManager.markError(cacheName, "PDF转换失败,未生成图片");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
||||||
|
for (Throwable throwable : throwableArray) {
|
||||||
|
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
||||||
|
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
||||||
|
// 标记为需要密码的状态
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.error("PDF转换执行失败: {}", cacheName, e);
|
||||||
|
|
||||||
|
// 检查是否已经标记为超时
|
||||||
|
FileConvertStatusManager.ConvertStatus status = FileConvertStatusManager.getConvertStatus(cacheName);
|
||||||
|
if (status == null || status.getStatus() != FileConvertStatusManager.Status.TIMEOUT) {
|
||||||
|
FileConvertStatusManager.markError(cacheName, "转换失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加转换完成后的回调
|
||||||
|
conversionFuture.whenCompleteAsync((imageUrls, throwable) -> {
|
||||||
|
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||||
|
logger.error("PDF转换失败,保留源文件: {}", originFilePath);
|
||||||
|
if (throwable != null) {
|
||||||
|
logger.error("转换失败原因: ", throwable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, callbackExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染预览页面
|
||||||
|
*/
|
||||||
|
private String renderPreview(Model model, String cacheName,
|
||||||
|
String outFilePath, String officePreviewType,
|
||||||
|
FileAttribute fileAttribute) {
|
||||||
|
try {
|
||||||
|
List<String> imageUrls;
|
||||||
|
if(pdftojpgservice.hasEncryptedPdfCacheSimple(outFilePath)){
|
||||||
|
imageUrls = pdftojpgservice.getEncryptedPdfCache(outFilePath);
|
||||||
|
}else {
|
||||||
|
imageUrls = fileHandlerService.loadPdf2jpgCache(outFilePath);
|
||||||
|
}
|
||||||
|
if (imageUrls == null || imageUrls.isEmpty()) {
|
||||||
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "PDF转换缓存异常,请联系管理员");
|
||||||
|
}
|
||||||
|
|
||||||
model.addAttribute("imgUrls", imageUrls);
|
model.addAttribute("imgUrls", imageUrls);
|
||||||
model.addAttribute("currentUrl", imageUrls.get(0));
|
model.addAttribute("currentUrl", imageUrls.getFirst());
|
||||||
|
|
||||||
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) {
|
if (OfficeFilePreviewImpl.OFFICE_PREVIEW_TYPE_IMAGE.equals(officePreviewType)) {
|
||||||
return OFFICE_PICTURE_FILE_PREVIEW_PAGE;
|
return OFFICE_PICTURE_FILE_PREVIEW_PAGE;
|
||||||
} else {
|
} else {
|
||||||
return PICTURE_FILE_PREVIEW_PAGE;
|
return PICTURE_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
} else {
|
} catch (Exception e) {
|
||||||
// 不是http开头,浏览器不能直接访问,需下载到本地
|
logger.error("渲染PDF预览页面失败: {}", cacheName, e);
|
||||||
if (url != null && !url.toLowerCase().startsWith("http")) {
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "渲染预览页面异常,请联系管理员");
|
||||||
if (!fileHandlerService.listConvertedFiles().containsKey(pdfName) || !ConfigConstants.isCacheEnabled()) {
|
}
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, pdfName);
|
}
|
||||||
if (response.isFailure()) {
|
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
/**
|
||||||
}
|
* 处理普通PDF预览(非图片转换)
|
||||||
model.addAttribute("pdfUrl", fileHandlerService.getRelativePath(response.getContent()));
|
*/
|
||||||
if (ConfigConstants.isCacheEnabled()) {
|
private String handleRegularPdfPreview(String url, Model model, FileAttribute fileAttribute,
|
||||||
// 加入缓存
|
String pdfName, boolean forceUpdatedCache,
|
||||||
fileHandlerService.addConvertedFile(pdfName, fileHandlerService.getRelativePath(outFilePath));
|
String outFilePath) {
|
||||||
}
|
// 不是http开头,浏览器不能直接访问,需下载到本地
|
||||||
} else {
|
if (url != null && !url.toLowerCase().startsWith("http")) {
|
||||||
model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName));
|
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 {
|
} else {
|
||||||
model.addAttribute("pdfUrl", url);
|
model.addAttribute("pdfUrl", WebUtils.encodeFileName(pdfName));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
model.addAttribute("pdfUrl", url);
|
||||||
}
|
}
|
||||||
return PDF_FILE_PREVIEW_PAGE;
|
return PDF_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
@@ -27,16 +27,28 @@ public class PictureFilePreviewImpl extends CommonPreviewImpl {
|
|||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
url= KkFileUtils.htmlEscape(url);
|
url= KkFileUtils.htmlEscape(url);
|
||||||
|
String suffix = fileAttribute.getSuffix();
|
||||||
List<String> imgUrls = new ArrayList<>();
|
List<String> imgUrls = new ArrayList<>();
|
||||||
imgUrls.add(url);
|
imgUrls.add(url);
|
||||||
String compressFileKey = fileAttribute.getCompressFileKey();
|
String compressFileKey = fileAttribute.getCompressFileKey();
|
||||||
List<String> zipImgUrls = fileHandlerService.getImgCache(compressFileKey);
|
List<String> zipImgUrls = fileHandlerService.getImgCache(compressFileKey);
|
||||||
if (!CollectionUtils.isEmpty(zipImgUrls)) {
|
if (!CollectionUtils.isEmpty(zipImgUrls)) {
|
||||||
imgUrls.addAll(zipImgUrls);
|
imgUrls.addAll(zipImgUrls);
|
||||||
|
model.addAttribute("imgUrls", imgUrls);
|
||||||
|
}else {
|
||||||
|
// 不是http开头,浏览器不能直接访问,需下载到本地
|
||||||
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!file.exists() || file.length() == 0) {
|
if (!file.exists() || file.length() == 0) {
|
||||||
return "";
|
return "KK提醒您:文件不存在或者已经被删除了!";
|
||||||
} else {
|
} else {
|
||||||
String charset = EncodingDetects.getJavaEncode(filePath);
|
String charset = EncodingDetects.getJavaEncode(filePath);
|
||||||
if ("ASCII".equals(charset)) {
|
if ("ASCII".equals(charset)) {
|
||||||
|
|||||||
@@ -5,37 +5,60 @@ import cn.keking.model.FileAttribute;
|
|||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import cn.keking.service.FileHandlerService;
|
import cn.keking.service.FileHandlerService;
|
||||||
import cn.keking.service.FilePreview;
|
import cn.keking.service.FilePreview;
|
||||||
import cn.keking.utils.ConvertPicUtil;
|
import cn.keking.service.TifToPdfService;
|
||||||
import cn.keking.utils.DownloadUtils;
|
import cn.keking.utils.DownloadUtils;
|
||||||
|
import cn.keking.utils.FileConvertStatusManager;
|
||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import cn.keking.utils.WebUtils;
|
import cn.keking.utils.WebUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* tiff 图片文件处理
|
* tiff 图片文件处理
|
||||||
*
|
|
||||||
* @author kl (http://kailing.pub)
|
* @author kl (http://kailing.pub)
|
||||||
* @since 2021/2/8
|
* @since 2021/2/8
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class TiffFilePreviewImpl implements FilePreview {
|
public class TiffFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TiffFilePreviewImpl.class);
|
||||||
|
|
||||||
|
// 用于处理回调的线程池
|
||||||
|
private static final ExecutorService callbackExecutor = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
public TiffFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
|
private final TifToPdfService tiftoservice;
|
||||||
|
private final OfficeFilePreviewImpl officefilepreviewimpl;
|
||||||
|
|
||||||
|
public TiffFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview, TifToPdfService tiftoservice, OfficeFilePreviewImpl officefilepreviewimpl) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
|
this.tiftoservice = tiftoservice;
|
||||||
|
this.officefilepreviewimpl = officefilepreviewimpl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
String fileName = fileAttribute.getName();
|
String fileName = fileAttribute.getName();
|
||||||
String tifPreviewType = ConfigConstants.getTifPreviewType();
|
String tifPreviewType = ConfigConstants.getTifPreviewType();
|
||||||
String cacheName = fileAttribute.getCacheName();
|
String cacheName = fileAttribute.getCacheName();
|
||||||
String outFilePath = fileAttribute.getOutFilePath();
|
String outFilePath = fileAttribute.getOutFilePath();
|
||||||
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache();
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
|
|
||||||
|
// 查询转换状态
|
||||||
|
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, fileName, cacheName, fileAttribute);
|
||||||
|
if (statusResult != null) {
|
||||||
|
return statusResult;
|
||||||
|
}
|
||||||
|
|
||||||
if ("jpg".equalsIgnoreCase(tifPreviewType) || "pdf".equalsIgnoreCase(tifPreviewType)) {
|
if ("jpg".equalsIgnoreCase(tifPreviewType) || "pdf".equalsIgnoreCase(tifPreviewType)) {
|
||||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(cacheName) || !ConfigConstants.isCacheEnabled()) {
|
||||||
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||||
@@ -43,66 +66,113 @@ public class TiffFilePreviewImpl implements FilePreview {
|
|||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
}
|
}
|
||||||
String filePath = response.getContent();
|
String filePath = response.getContent();
|
||||||
|
|
||||||
|
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)) {
|
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));
|
model.addAttribute("pdfUrl", WebUtils.encodeFileName(cacheName));
|
||||||
return PDF_FILE_PREVIEW_PAGE;
|
return PDF_FILE_PREVIEW_PAGE;
|
||||||
}else {
|
} else if ("jpg".equalsIgnoreCase(tifPreviewType)) {
|
||||||
// 将tif转换为jpg,返回转换后的文件路径、文件名的list
|
List<String> imgCache = fileHandlerService.getImgCache(cacheName);
|
||||||
List<String> listPic2Jpg;
|
if (imgCache == null || imgCache.isEmpty()) {
|
||||||
try {
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "TIF转换缓存异常,请联系系统管理员!");
|
||||||
listPic2Jpg = ConvertPicUtil.convertTif2Jpg(filePath, outFilePath,forceUpdatedCache);
|
|
||||||
} catch (Exception e) {
|
|
||||||
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异常,请联系系统管理员!" );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//是否保留源文件,转换失败保留源文件,转换成功删除源文件
|
model.addAttribute("imgUrls", imgCache);
|
||||||
if(!fileAttribute.isCompressFile() && ConfigConstants.getDeleteSourceFile()) {
|
model.addAttribute("currentUrl", imgCache.getFirst());
|
||||||
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));
|
|
||||||
return PICTURE_FILE_PREVIEW_PAGE;
|
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开头,浏览器不能直接访问,需下载到本地
|
// 不是http开头,浏览器不能直接访问,需下载到本地
|
||||||
if (url != null && !url.toLowerCase().startsWith("http")) {
|
if (url != null && !url.toLowerCase().startsWith("http")) {
|
||||||
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
|
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
|
||||||
@@ -116,12 +186,11 @@ public class TiffFilePreviewImpl implements FilePreview {
|
|||||||
fileHandlerService.addConvertedFile(fileName, fileHandlerService.getRelativePath(outFilePath));
|
fileHandlerService.addConvertedFile(fileName, fileHandlerService.getRelativePath(outFilePath));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName));
|
model.addAttribute("currentUrl", WebUtils.encodeFileName(fileName));
|
||||||
}
|
}
|
||||||
return TIFF_FILE_PREVIEW_PAGE;
|
return TIFF_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
model.addAttribute("currentUrl", url);
|
model.addAttribute("currentUrl", url);
|
||||||
return TIFF_FILE_PREVIEW_PAGE;
|
return TIFF_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
79
server/src/main/java/cn/keking/utils/AESUtil.java
Normal file
79
server/src/main/java/cn/keking/utils/AESUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,31 +3,21 @@ package cn.keking.utils;
|
|||||||
import cn.keking.config.ConfigConstants;
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import io.mola.galimatias.GalimatiasParseException;
|
import io.mola.galimatias.GalimatiasParseException;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.hc.client5.http.classic.HttpClient;
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
|
||||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.client.RequestCallback;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.nio.file.Files;
|
||||||
import java.util.Map;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static cn.keking.utils.KkFileUtils.isFtpUrl;
|
import static cn.keking.utils.KkFileUtils.*;
|
||||||
import static cn.keking.utils.KkFileUtils.isHttpUrl;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yudian-it
|
* @author yudian-it
|
||||||
@@ -39,10 +29,7 @@ public class DownloadUtils {
|
|||||||
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
|
private static final String URL_PARAM_FTP_USERNAME = "ftp.username";
|
||||||
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
|
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
|
||||||
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
|
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
|
||||||
private static final RestTemplate restTemplate = new RestTemplate();
|
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
|
||||||
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param fileAttribute fileAttribute
|
* @param fileAttribute fileAttribute
|
||||||
@@ -50,17 +37,17 @@ public class DownloadUtils {
|
|||||||
* @return 本地文件绝对路径
|
* @return 本地文件绝对路径
|
||||||
*/
|
*/
|
||||||
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
|
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
|
||||||
// 忽略ssl证书
|
|
||||||
String urlStr = null;
|
String urlStr = null;
|
||||||
try {
|
try {
|
||||||
SslUtils.ignoreSsl();
|
urlStr = fileAttribute.getUrl();
|
||||||
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("忽略SSL证书异常:", e);
|
logger.error("处理URL异常:", e);
|
||||||
}
|
}
|
||||||
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
|
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
|
||||||
String realPath = getRelFilePath(fileName, fileAttribute);
|
String realPath = getRelFilePath(fileName, fileAttribute);
|
||||||
|
// 获取文件后缀用于校验
|
||||||
|
final String fileSuffix = fileAttribute.getSuffix();
|
||||||
// 判断是否非法地址
|
// 判断是否非法地址
|
||||||
if (KkFileUtils.isIllegalFileName(realPath)) {
|
if (KkFileUtils.isIllegalFileName(realPath)) {
|
||||||
response.setCode(1);
|
response.setCode(1);
|
||||||
@@ -90,36 +77,33 @@ public class DownloadUtils {
|
|||||||
if (!fileAttribute.getSkipDownLoad()) {
|
if (!fileAttribute.getSkipDownLoad()) {
|
||||||
if (isHttpUrl(url)) {
|
if (isHttpUrl(url)) {
|
||||||
File realFile = new File(realPath);
|
File realFile = new File(realPath);
|
||||||
factory.setConnectionRequestTimeout(2000); //设置超时时间
|
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||||
factory.setConnectTimeout(10000);
|
String finalUrlStr = urlStr;
|
||||||
factory.setReadTimeout(72000);
|
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
|
||||||
HttpClient httpClient = HttpClientBuilder.create().setRedirectStrategy(new DefaultRedirectStrategy()).build();
|
// 获取响应头中的Content-Type
|
||||||
factory.setHttpClient(httpClient); //加入重定向方法
|
String contentType = responseWrapper.getContentType();
|
||||||
restTemplate.setRequestFactory(factory);
|
|
||||||
RequestCallback requestCallback = request -> {
|
// 如果是Office/设计文件,需要校验MIME类型
|
||||||
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
if (WebUtils.isMimeCheckRequired(fileSuffix)) {
|
||||||
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
|
if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
|
||||||
if(StringUtils.hasText(proxyAuthorization)){
|
logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}",
|
||||||
Map<String,String> proxyAuthorizationMap = mapper.readValue(proxyAuthorization, Map.class);
|
finalUrlStr, contentType);
|
||||||
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
|
responseWrapper.setHasError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
try {
|
// 保存文件
|
||||||
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
|
||||||
FileUtils.copyToFile(fileResponse.getBody(), realFile);
|
});
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
response.setCode(1);
|
|
||||||
response.setContent(null);
|
|
||||||
response.setMsg("下载失败:" + e);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
} else if (isFtpUrl(url)) {
|
} else if (isFtpUrl(url)) {
|
||||||
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
||||||
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
||||||
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
||||||
FtpUtils.download(fileAttribute.getUrl(), realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
|
||||||
|
FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||||
|
} else if (isFileUrl(url)) { // 添加对file协议的支持
|
||||||
|
handleFileProtocol(url, realPath);
|
||||||
} else {
|
} else {
|
||||||
response.setCode(1);
|
response.setCode(1);
|
||||||
response.setMsg("url不能识别url" + urlStr);
|
response.setMsg("url不能识别url" + urlStr);
|
||||||
@@ -138,9 +122,68 @@ public class DownloadUtils {
|
|||||||
response.setMsg(e.getMessage());
|
response.setMsg(e.getMessage());
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理file协议的文件下载
|
||||||
|
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
|
||||||
|
File sourceFile = new File(url.getPath());
|
||||||
|
if (!sourceFile.exists()) {
|
||||||
|
throw new FileNotFoundException("本地文件不存在: " + url.getPath());
|
||||||
|
}
|
||||||
|
if (!sourceFile.isFile()) {
|
||||||
|
throw new IOException("路径不是文件: " + url.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
File targetFile = new File(targetPath);
|
||||||
|
|
||||||
|
// 判断源文件和目标文件是否是同一个文件(防止自身复制覆盖)
|
||||||
|
if (isSameFile(sourceFile, targetFile)) {
|
||||||
|
// 如果是同一个文件,直接返回,不执行复制操作
|
||||||
|
logger.info("源文件和目标文件相同,跳过复制: {}", sourceFile.getAbsolutePath());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
File parentDir = targetFile.getParentFile();
|
||||||
|
if (parentDir != null && !parentDir.exists()) {
|
||||||
|
parentDir.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制文件
|
||||||
|
Files.copy(sourceFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断两个文件是否是同一个文件
|
||||||
|
* 通过比较规范化路径来避免符号链接、相对路径等问题
|
||||||
|
*/
|
||||||
|
private static boolean isSameFile(File file1, File file2) {
|
||||||
|
try {
|
||||||
|
// 使用规范化路径比较,可以处理符号链接、相对路径等情况
|
||||||
|
String canonicalPath1 = file1.getCanonicalPath();
|
||||||
|
String canonicalPath2 = file2.getCanonicalPath();
|
||||||
|
|
||||||
|
// 如果是Windows系统,忽略路径大小写
|
||||||
|
if (isWindows()) {
|
||||||
|
return canonicalPath1.equalsIgnoreCase(canonicalPath2);
|
||||||
|
}
|
||||||
|
return canonicalPath1.equals(canonicalPath2);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 如果获取规范化路径失败,使用绝对路径比较
|
||||||
|
logger.warn("无法获取文件的规范化路径,使用绝对路径比较: {}, {}", file1.getAbsolutePath(), file2.getAbsolutePath());
|
||||||
|
|
||||||
|
String absolutePath1 = file1.getAbsolutePath();
|
||||||
|
String absolutePath2 = file2.getAbsolutePath();
|
||||||
|
|
||||||
|
if (isWindows()) {
|
||||||
|
return absolutePath1.equalsIgnoreCase(absolutePath2);
|
||||||
|
}
|
||||||
|
return absolutePath1.equals(absolutePath2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取真实文件绝对路径
|
* 获取真实文件绝对路径
|
||||||
@@ -164,5 +207,4 @@ public class DownloadUtils {
|
|||||||
}
|
}
|
||||||
return realPath;
|
return realPath;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -3,57 +3,225 @@ package cn.keking.utils;
|
|||||||
import cn.keking.config.ConfigConstants;
|
import cn.keking.config.ConfigConstants;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.net.ftp.FTPClient;
|
import org.apache.commons.net.ftp.FTPClient;
|
||||||
import org.apache.commons.net.ftp.FTPReply;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.URL;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @auther: chenjh
|
* @auther: chenjh
|
||||||
* @since: 2019/6/18 14:36
|
* @since: 2019/6/18 14:36
|
||||||
*/
|
*/
|
||||||
public class FtpUtils {
|
public class FtpUtils {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(FtpUtils.class);
|
||||||
|
|
||||||
public static FTPClient connect(String host, int port, String username, String password, String controlEncoding) throws IOException {
|
/**
|
||||||
|
* 从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 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.setControlEncoding(controlEncoding);
|
||||||
|
ftpClient.connect(host, port);
|
||||||
|
|
||||||
|
if (!ftpClient.login(username, password)) {
|
||||||
|
throw new IOException("FTP登录失败,用户名或密码错误");
|
||||||
|
}
|
||||||
|
|
||||||
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
|
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
|
||||||
return ftpClient;
|
return ftpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void download(String ftpUrl, String localFilePath, String ftpUsername, String ftpPassword, String ftpControlEncoding) throws IOException {
|
|
||||||
String username = StringUtils.isEmpty(ftpUsername) ? ConfigConstants.getFtpUsername() : ftpUsername;
|
|
||||||
String password = StringUtils.isEmpty(ftpPassword) ? ConfigConstants.getFtpPassword() : ftpPassword;
|
|
||||||
String controlEncoding = StringUtils.isEmpty(ftpControlEncoding) ? ConfigConstants.getFtpControlEncoding() : ftpControlEncoding;
|
|
||||||
URL url = new URL(ftpUrl);
|
|
||||||
String host = url.getHost();
|
|
||||||
int port = (url.getPort() == -1) ? url.getDefaultPort() : url.getPort();
|
|
||||||
String remoteFilePath = url.getPath();
|
|
||||||
LOGGER.debug("FTP connection url:{}, username:{}, password:{}, controlEncoding:{}, localFilePath:{}", ftpUrl, username, password, controlEncoding, localFilePath);
|
|
||||||
FTPClient ftpClient = connect(host, port, username, password, controlEncoding);
|
|
||||||
OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath));
|
|
||||||
ftpClient.enterLocalPassiveMode();
|
|
||||||
boolean downloadResult = ftpClient.retrieveFile(new String(remoteFilePath.getBytes(controlEncoding), StandardCharsets.ISO_8859_1), outputStream);
|
|
||||||
LOGGER.debug("FTP download result {}", downloadResult);
|
|
||||||
outputStream.flush();
|
|
||||||
outputStream.close();
|
|
||||||
ftpClient.logout();
|
|
||||||
ftpClient.disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user