mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 00:14:56 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6df85be1b | ||
|
|
1568b3023d | ||
|
|
f97ed04ab0 |
117
.github/scripts/deploy_windows_winrm.py
vendored
117
.github/scripts/deploy_windows_winrm.py
vendored
@@ -1,117 +0,0 @@
|
||||
#!/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
327
.github/scripts/remote_windows_deploy.ps1
vendored
@@ -1,327 +0,0 @@
|
||||
$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"
|
||||
52
.github/workflows/master-auto-deploy.yml
vendored
52
.github/workflows/master-auto-deploy.yml
vendored
@@ -1,52 +0,0 @@
|
||||
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
|
||||
11
.github/workflows/maven.yml
vendored
11
.github/workflows/maven.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B package "-Dmaven.test.skip=true" --file pom.xml
|
||||
run: mvn -B package -Dmaven.test.skip=true --file pom.xml
|
||||
|
||||
- name: Upload Linux distribution package
|
||||
if: success() && runner.os == 'Linux'
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kkfileview-linux
|
||||
@@ -44,12 +44,9 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Windows distribution package
|
||||
if: success() && runner.os == 'Windows'
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kkfileview-windows
|
||||
path: server/target/*.zip
|
||||
retention-days: 7
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
2
.github/workflows/nightly-e2e.yml
vendored
2
.github/workflows/nightly-e2e.yml
vendored
@@ -115,4 +115,4 @@ jobs:
|
||||
name: nightly-e2e-service-logs
|
||||
path: |
|
||||
/tmp/kkfileview.log
|
||||
/tmp/fixture-server.log
|
||||
/tmp/fixture-server.log
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,7 +26,6 @@ nbdist/
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
.DS_Store
|
||||
.artifacts/
|
||||
|
||||
server/src/main/cache/
|
||||
server/src/main/file/
|
||||
|
||||
230
AGENTS.md
230
AGENTS.md
@@ -1,230 +0,0 @@
|
||||
# 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:5.0.0
|
||||
FROM keking/kkfileview-base:4.4.0
|
||||
ADD server/target/kkFileView-*.tar.gz /opt/
|
||||
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.2/bin
|
||||
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.2/config/application.properties","-jar","/opt/kkFileView-5.0.2/bin/kkFileView-5.0.2.jar"]
|
||||
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-4.4.0/bin
|
||||
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-4.4.0/config/application.properties","-jar","/opt/kkFileView-4.4.0/bin/kkFileView-4.4.0.jar"]
|
||||
|
||||
53
README.cn.md
53
README.cn.md
@@ -149,45 +149,7 @@ pdf预览模式预览效果如下
|
||||
|
||||
### 历史更新记录
|
||||
|
||||
#### > 2026年08月14日,v5.0.2 补丁版本发布 :
|
||||
|
||||
#### 安全修复
|
||||
1. 将不可信 HTML 预览放入不具有同源权限的 iframe 沙箱,并默认禁用其中的 JavaScript,避免被预览文件在 kkFileView 应用源中执行脚本(GHSA-9wcf-jxxf-w2g2)
|
||||
2. 默认禁用演示文件删除接口,将接口改为 POST,并要求显式配置密码后进行精确比较(GHSA-f3qx-xrwc-5428)
|
||||
|
||||
#### 修复问题
|
||||
1. 在 PDF 转图服务启动时刷新 ImageIO 插件,使 JBIG2 等嵌套 JAR 图像读取器能够被发现,避免 PDF 转图片预览时部分图像丢失
|
||||
|
||||
#### 升级说明
|
||||
1. 建议所有 v5.0.1 及更早版本用户尽快升级到 v5.0.2
|
||||
2. 本版本继续要求 JDK 21 及以上,现有 v5.0.1 配置可直接沿用
|
||||
3. 文件删除功能现在默认禁用;如确需启用,请通过 `KK_DELETE_PASSWORD` 或外部 `delete.password` 设置独立强密码,并将 `/deleteFile` 调用改为 POST
|
||||
4. `kk.scriptjs` 现在默认为 `false`;显式启用后,脚本仍只会在隔离的 iframe 沙箱内运行
|
||||
|
||||
#### > 2026年07月13日,v5.0.1 补丁版本发布 :
|
||||
|
||||
#### 安全修复
|
||||
1. 修复 `/addTask` 未经过信任主机和本地目录过滤,可能导致服务端请求伪造(SSRF)的问题(GHSA-gwwj-52hv-6g2m)
|
||||
2. 修复 `/listFiles` 的 `directory` 参数可越出演示目录,造成路径遍历和目录信息泄露的问题(GHSA-pmp8-g8p2-p6jq)
|
||||
|
||||
#### 修复问题
|
||||
1. 修复 PDF 跨域、页码定位、文本高亮、打印和打印水印相关问题
|
||||
2. 修复 PDF 在反向代理场景下的绝对路径问题,以及水印和高亮内容包含特殊字符时的解析失败
|
||||
3. 修复 Redis 单机、集群、主从、哨兵模式配置不一致和地址协议缺失问题
|
||||
4. 修复下载 MIME 类型校验失败后仍返回成功、HTTP 错误原因不明确,以及共享 HTTP Client 被错误关闭的问题
|
||||
5. 修复 LuckyExcel 数据校验类型未映射时的 xlsx 解析崩溃
|
||||
|
||||
#### 优化内容
|
||||
1. 大型 xlsx 文件改用 Web Worker 执行 LuckyExcel 解析,并在 Worker 不可用或异常时自动回退主线程
|
||||
2. 新增 `pdf.sidebar.open` 配置,可控制 PDF 预览是否默认打开侧栏
|
||||
3. Maven CI 增加 Linux、Windows、macOS 构建验证
|
||||
4. 新增仓库安全策略和私密漏洞报告入口
|
||||
|
||||
#### 升级说明
|
||||
1. 建议所有 v5.0.0 及更早版本用户尽快升级到 v5.0.1
|
||||
2. 本版本继续要求 JDK 21 及以上,现有 v5.0.0 配置可直接沿用
|
||||
|
||||
#### > 2026年04月14日,v5.0.0 版本发布 :
|
||||
#### > 2026年01月20日,v5.0 版本发布 :
|
||||
#### 优化内容
|
||||
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能
|
||||
2. 图片解析优化 - 改进图片处理机制
|
||||
@@ -197,10 +159,6 @@ pdf预览模式预览效果如下
|
||||
6. ftp多客户端接入优化 - 提升FTP服务兼容性
|
||||
7. 首页目录访问优化 - 采用post服务端分页机制
|
||||
8. marked 解析优化 - 改进Markdown渲染
|
||||
9. 压缩包预览页重构为单工作区布局,支持目录折叠与右侧内嵌预览
|
||||
10. 优化压缩包内文件类型标识,以及单图预览页的展示样式
|
||||
11. 补充面向工程自动化与编码代理的仓库说明文档
|
||||
12. 重构演示门户页面,包括首页、接入说明、版本记录与赞助页
|
||||
|
||||
#### 新增功能
|
||||
1. msg邮件解析 - 新增msg格式邮件文件预览支持
|
||||
@@ -221,12 +179,6 @@ pdf预览模式预览效果如下
|
||||
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及以上版本
|
||||
@@ -237,8 +189,6 @@ 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 版本发布 :
|
||||
|
||||
@@ -518,3 +468,4 @@ dcm医疗数位影像 引用于 [dcmjs](https://github.com/dcmjs-org/dcmjs )开
|
||||
- 本项目诞生于[凯京集团],在取得公司高层同意后以 Apache 协议开源出来反哺社区,在此特别感谢凯京集团,以及集团领导[@唐老大](https://github.com/tangshd)的支持、@端木详笑的贡献。
|
||||
- 本项目已脱离公司由[KK开源社区]维护发展壮大,感谢所有给 kkFileView 提 Issue 、Pr 开发者
|
||||
- 本项目引入的第三方组件已在 '关于引用' 列表列出,感谢这些项目,让 kkFileView 更出色
|
||||
|
||||
|
||||
54
README.md
54
README.md
@@ -65,47 +65,9 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
||||
|
||||
## Change History
|
||||
|
||||
### Version 5.0.2 (August 14, 2026)
|
||||
### Version 5.0 (January 20, 2026)
|
||||
|
||||
#### Security Fixes
|
||||
1. Sandboxed untrusted HTML previews in an opaque-origin iframe and disabled embedded JavaScript by default, preventing previewed files from executing in the kkFileView application origin (GHSA-9wcf-jxxf-w2g2)
|
||||
2. Disabled the demo file deletion endpoint by default, changed it to POST, and required an explicitly configured password with exact comparison (GHSA-f3qx-xrwc-5428)
|
||||
|
||||
#### Fixes
|
||||
1. Refreshed ImageIO plugins when PDF conversion starts so nested JAR providers such as the JBIG2 reader are discovered, preventing images from disappearing in PDF-to-image previews
|
||||
|
||||
#### Upgrade Notes
|
||||
1. All users running v5.0.1 or earlier are strongly encouraged to upgrade to v5.0.2
|
||||
2. JDK 21 or higher remains required, and existing v5.0.1 configuration can be reused
|
||||
3. File deletion is now disabled unless `KK_DELETE_PASSWORD` or an external `delete.password` is set to an independent strong password; integrations must call `/deleteFile` with POST
|
||||
4. `kk.scriptjs` now defaults to `false`; when explicitly enabled, scripts still run only inside the isolated iframe sandbox
|
||||
|
||||
### Version 5.0.1 (July 13, 2026)
|
||||
|
||||
#### Security Fixes
|
||||
1. Fixed `/addTask` bypassing trusted-host and local-directory filters, which could allow server-side request forgery (SSRF) (GHSA-gwwj-52hv-6g2m)
|
||||
2. Fixed the `/listFiles` `directory` parameter escaping the demo directory, which could allow path traversal and directory information disclosure (GHSA-pmp8-g8p2-p6jq)
|
||||
|
||||
#### Fixes
|
||||
1. Fixed PDF cross-origin access, page positioning, text highlighting, printing, and print watermark issues
|
||||
2. Fixed PDF absolute paths behind reverse proxies and parsing failures when watermark or highlight text contains special characters
|
||||
3. Fixed inconsistent Redis settings across standalone, cluster, master-replica, and sentinel modes, including missing address protocols
|
||||
4. Fixed successful responses after MIME validation failures, unclear HTTP error reporting, and accidental closure of a shared HTTP client
|
||||
5. Fixed xlsx parsing crashes when LuckyExcel data-validation types have no mapping
|
||||
|
||||
#### Improvements
|
||||
1. Moved LuckyExcel parsing for large xlsx files into a Web Worker, with automatic main-thread fallback when the Worker is unavailable or fails
|
||||
2. Added `pdf.sidebar.open` to control whether the PDF sidebar opens by default
|
||||
3. Added Linux, Windows, and macOS validation to Maven CI
|
||||
4. Added a repository security policy and private vulnerability reporting guidance
|
||||
|
||||
#### Upgrade Notes
|
||||
1. All users running v5.0.0 or earlier are strongly encouraged to upgrade to v5.0.1
|
||||
2. JDK 21 or higher remains required, and existing v5.0.0 configuration can be reused
|
||||
|
||||
### Version 5.0.0 (April 14, 2026)
|
||||
|
||||
#### Improvements
|
||||
#### Optimizations
|
||||
1. Enhanced xlsx front-end parsing - Improved Excel file front-end rendering performance
|
||||
2. Optimized image parsing - Enhanced image processing mechanism
|
||||
3. Improved tif parsing - Enhanced TIF format support
|
||||
@@ -114,10 +76,6 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
||||
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
|
||||
@@ -138,12 +96,6 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
||||
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
|
||||
@@ -154,8 +106,6 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
||||
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)
|
||||
|
||||
|
||||
66
SECURITY.md
66
SECURITY.md
@@ -1,66 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes are handled for the latest released version of kkFileView and the
|
||||
current `master` branch. Older versions may be evaluated case by case, but users
|
||||
are encouraged to upgrade to the latest release before reporting or verifying a
|
||||
security issue.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities privately through GitHub Private
|
||||
Vulnerability Reporting:
|
||||
|
||||
https://github.com/kekingcn/kkFileView/security/advisories/new
|
||||
|
||||
Do not publish vulnerability details, proof-of-concept code, exploit steps,
|
||||
sensitive logs, or private deployment information in public GitHub issues,
|
||||
discussions, pull requests, or comments.
|
||||
|
||||
When reporting a vulnerability, please include as much of the following
|
||||
information as you can safely share:
|
||||
|
||||
- Affected kkFileView version or commit
|
||||
- Deployment mode, operating system, JDK version, and related middleware
|
||||
- Clear reproduction steps
|
||||
- Impact assessment and affected feature or endpoint
|
||||
- Sanitized logs, screenshots, or sample files if they are required to reproduce
|
||||
the issue
|
||||
- Whether the issue is already being disclosed elsewhere
|
||||
|
||||
The maintainers will review private reports, ask for additional information when
|
||||
needed, coordinate a fix, and publish disclosure information when appropriate.
|
||||
|
||||
If the private reporting link is unavailable, please open a public issue only to
|
||||
request a private contact channel, without including technical vulnerability
|
||||
details.
|
||||
|
||||
---
|
||||
|
||||
# 安全策略
|
||||
|
||||
## 支持版本
|
||||
|
||||
kkFileView 安全修复主要覆盖最新发布版本和当前 `master` 分支。旧版本问题会视影响范围和维护成本单独评估,但建议用户优先升级到最新版本后再验证或报告安全问题。
|
||||
|
||||
## 报告安全漏洞
|
||||
|
||||
请通过 GitHub Private Vulnerability Reporting 私密提交安全漏洞:
|
||||
|
||||
https://github.com/kekingcn/kkFileView/security/advisories/new
|
||||
|
||||
请不要在公开 GitHub issue、discussion、pull request 或评论中发布漏洞细节、PoC、利用步骤、敏感日志或私有部署信息。
|
||||
|
||||
提交漏洞时,请在可安全分享的前提下尽量提供以下信息:
|
||||
|
||||
- 受影响的 kkFileView 版本或提交
|
||||
- 部署方式、操作系统、JDK 版本和相关中间件信息
|
||||
- 清晰的复现步骤
|
||||
- 影响范围,以及受影响的功能或接口
|
||||
- 复现所需的脱敏日志、截图或样例文件
|
||||
- 该问题是否已在其他渠道披露
|
||||
|
||||
维护者会在私密渠道中评估报告,在需要时继续确认细节,协调修复,并在适当时发布披露信息。
|
||||
|
||||
如果私密报告链接不可用,请只在公开 issue 中请求私密联系方式,不要包含任何技术漏洞细节。
|
||||
@@ -1,57 +0,0 @@
|
||||
# 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 替换/回滚
|
||||
@@ -7,10 +7,10 @@
|
||||
然后使用 kkfileview-base 作为基础镜像进行构建,加快 kkfileview docker 镜像构建与发布。
|
||||
|
||||
执行如下命令即可构建基础镜像:
|
||||
> 这里镜像 tag 以 5.0.0 为例,本项目所维护的 Dockerfile 文件考虑了跨平台兼容性。 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
|
||||
> 这里镜像 tag 以 4.4.0 为例,本项目所维护的 Dockerfile 文件考虑了跨平台兼容性。 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
|
||||
|
||||
```shell
|
||||
docker build --tag keking/kkfileview-base:5.0.0 .
|
||||
docker build --tag keking/kkfileview-base:4.4.0 .
|
||||
```
|
||||
|
||||
|
||||
@@ -46,5 +46,5 @@ docker build --tag keking/kkfileview-base:5.0.0 .
|
||||
现在就可以愉快地开始构建了,构建命令示例:
|
||||
|
||||
```shell
|
||||
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:5.0.0 --push .
|
||||
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.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:
|
||||
|
||||
> 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.
|
||||
> 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.
|
||||
|
||||
```shell
|
||||
docker build --tag keking/kkfileview-base:5.0.0 .
|
||||
docker build --tag keking/kkfileview-base:4.4.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:
|
||||
|
||||
```shell
|
||||
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:5.0.0 --push .
|
||||
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
|
||||
```
|
||||
|
||||
4
pom.xml
4
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>cn.keking</groupId>
|
||||
<artifactId>kkFileView-parent</artifactId>
|
||||
<version>5.0.2</version>
|
||||
<version>5.0</version>
|
||||
|
||||
<properties>
|
||||
<!-- ========== Java 和编译配置 ========== -->
|
||||
@@ -110,4 +110,4 @@
|
||||
<system>github</system>
|
||||
<url>https://github.com/kekingcn/kkFileView/issues</url>
|
||||
</issueManagement>
|
||||
</project>
|
||||
</project>
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>kkFileView-parent</artifactId>
|
||||
<groupId>cn.keking</groupId>
|
||||
<version>5.0.2</version>
|
||||
<version>5.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>kkFileView</artifactId>
|
||||
@@ -52,12 +52,6 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- ========== 文档格式转换 ========== -->
|
||||
<dependency>
|
||||
@@ -372,4 +366,4 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
</project>
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/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,20 +1,10 @@
|
||||
@echo off
|
||||
set "KKFILEVIEW_BIN_FOLDER=%cd%"
|
||||
cd "%KKFILEVIEW_BIN_FOLDER%"
|
||||
set "JAR_NAME="
|
||||
for %%F in (kkFileView-*.jar) do (
|
||||
set "JAR_NAME=%%~nxF"
|
||||
goto :jar_found
|
||||
)
|
||||
echo Error: kkFileView jar not found in %KKFILEVIEW_BIN_FOLDER%
|
||||
exit /b 1
|
||||
|
||||
:jar_found
|
||||
echo Using KKFILEVIEW_BIN_FOLDER %KKFILEVIEW_BIN_FOLDER%
|
||||
echo Using JAR_NAME %JAR_NAME%
|
||||
echo Starting kkFileView...
|
||||
echo Please check log file in ../log/kkFileView.log for more information
|
||||
echo You can get help in our official home site: https://kkview.cn
|
||||
echo If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ
|
||||
echo If this project is helpful to you, please star it on https://gitee.com/kekingcn/file-online-preview/stargazers
|
||||
java -Dspring.config.location=..\config\application.properties -jar "%JAR_NAME%" > ..\log\kkFileView.log 2>&1
|
||||
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0.jar -> ..\log\kkFileView.log
|
||||
|
||||
@@ -49,16 +49,9 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
JAR_PATH=$(ls kkFileView-*.jar 2>/dev/null | head -n 1)
|
||||
if [ -z "${JAR_PATH}" ]; then
|
||||
echo "kkFileView jar not found in ${KKFILEVIEW_BIN_FOLDER}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## 启动kkFileView
|
||||
echo "Starting kkFileView..."
|
||||
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 &
|
||||
nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -jar kkFileView-4.4.0.jar > ../log/kkFileView.log 2>&1 &
|
||||
echo "Please execute ./showlog.sh to check log for more information"
|
||||
echo "You can get help in our official home site: https://kkview.cn"
|
||||
echo "If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ"
|
||||
|
||||
@@ -96,12 +96,12 @@ office.documentopenpasswords = ${KK_OFFICE_DOCUMENTOPENPASSWORD:true}
|
||||
office.type.web = ${KK_OFFICE_TYPE_WEB:web}
|
||||
|
||||
# Office文档预览类型
|
||||
# 支持动态配置,可选值:image/pdf,默认使用pdf模式
|
||||
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:pdf}
|
||||
# 支持动态配置,可选值:image/pdf
|
||||
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
|
||||
|
||||
# 是否关闭Office预览模式切换开关,默认为true(关闭切换)
|
||||
# 设置为false时,用户可以在图片和PDF模式间切换
|
||||
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:true}
|
||||
# 是否关闭Office预览模式切换开关,默认为false(允许切换)
|
||||
# 设置为true时,用户无法在图片和PDF模式间切换
|
||||
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
|
||||
|
||||
|
||||
###############################################################################
|
||||
@@ -155,9 +155,6 @@ pdf.bookmark.disable = ${KK_PDF_BOOKMARK_DISABLE:true}
|
||||
# 是否禁止PDF编辑功能(注释、表单等),默认为false(允许编辑)
|
||||
pdf.disable.editing = ${KK_PDF_DISABLE_EDITING:false}
|
||||
|
||||
# 是否默认打开PDF侧边栏(缩略图面板),默认为true(打开)
|
||||
pdf.sidebar.open = ${KK_PDF_SIDEBAR_OPEN:true}
|
||||
|
||||
# PDF处理最大线程数,控制并发处理能力
|
||||
pdf.max.threads = 10
|
||||
|
||||
@@ -408,9 +405,8 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
||||
# 启用后删除文件需要输入验证码,防止误删
|
||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||
|
||||
# 删除文件密码,默认为false(禁用删除接口)
|
||||
# 如需启用删除功能,请通过环境变量或外部配置设置独立的强密码
|
||||
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||
# 删除文件密码,默认为123456
|
||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
||||
|
||||
# 是否删除转换后的源文件,默认为true(删除)
|
||||
# 启用可节约磁盘空间,但会丢失原始文件
|
||||
@@ -470,8 +466,8 @@ kk.xlsxshowtoolbar = false
|
||||
# 首页是否显示key密钥 默认为false(禁用)
|
||||
kk.isshowkey = false
|
||||
|
||||
# 预览html文件 是否在隔离沙箱中启用JavaScript,默认为false(禁用)
|
||||
kk.scriptjs = false
|
||||
# 预览html文件 是否启用JavaScript 默认为true(启用)
|
||||
kk.scriptjs = true
|
||||
|
||||
|
||||
###############################################################################
|
||||
@@ -479,4 +475,4 @@ kk.scriptjs = false
|
||||
###############################################################################
|
||||
|
||||
# 纯文本文件类型,直接显示
|
||||
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}
|
||||
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}
|
||||
@@ -405,8 +405,8 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
||||
# 启用后删除文件需要输入验证码,防止误删
|
||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||
|
||||
# 删除文件密码,默认为false(禁用删除接口)
|
||||
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||
# 删除文件密码,默认为123456
|
||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
||||
|
||||
# 是否删除转换后的源文件,默认为true(删除)
|
||||
# 启用可节约磁盘空间,但会丢失原始文件
|
||||
@@ -466,8 +466,8 @@ kk.xlsxshowtoolbar = true
|
||||
# 首页是否显示key密钥 默认为false(禁用)
|
||||
kk.isshowkey = true
|
||||
|
||||
# 预览html文件 是否在隔离沙箱中启用JavaScript,默认为false(禁用)
|
||||
kk.scriptjs = false
|
||||
# 预览html文件 是否启用JavaScript 默认为true(启用)
|
||||
kk.scriptjs = true
|
||||
|
||||
|
||||
###############################################################################
|
||||
@@ -475,4 +475,4 @@ kk.scriptjs = false
|
||||
###############################################################################
|
||||
|
||||
# 纯文本文件类型,直接显示
|
||||
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}
|
||||
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}
|
||||
@@ -31,7 +31,7 @@ public class ConfigConstants {
|
||||
// ==================================================
|
||||
public static final String DEFAULT_VALUE = "default";
|
||||
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
|
||||
public static final String DEFAULT_PASSWORD = "false";
|
||||
public static final String DEFAULT_PASSWORD = "123456";
|
||||
public static final String DEFAULT_SIZE = "500MB";
|
||||
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
||||
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
||||
@@ -77,7 +77,6 @@ public class ConfigConstants {
|
||||
public static final String DEFAULT_PDF_DOWNLOAD_DISABLE = "true";
|
||||
public static final String DEFAULT_PDF_BOOKMARK_DISABLE = "true";
|
||||
public static final String DEFAULT_PDF_DISABLE_EDITING = "true";
|
||||
public static final String DEFAULT_PDF_SIDEBAR_OPEN = "true";
|
||||
public static final String DEFAULT_PDF2_JPG_DPI = "105";
|
||||
public static final String DEFAULT_PDF_SMALL_DTI = "150";
|
||||
public static final String DEFAULT_PDF_MEDIUM_DPI = "120";
|
||||
@@ -195,7 +194,6 @@ public class ConfigConstants {
|
||||
private static String pdfPrintDisable;
|
||||
private static String pdfDownloadDisable;
|
||||
private static String pdfBookmarkDisable;
|
||||
private static String pdfSidebarOpen;
|
||||
private static int pdf2JpgDpi;
|
||||
private static boolean pdfDpiEnabled;
|
||||
private static int pdfSmallDpi;
|
||||
@@ -338,7 +336,6 @@ public class ConfigConstants {
|
||||
public static String getPdfDownloadDisable() { return pdfDownloadDisable; }
|
||||
public static String getPdfBookmarkDisable() { return pdfBookmarkDisable; }
|
||||
public static String getPdfDisableEditing() { return pdfDisableEditing; }
|
||||
public static String getPdfSidebarOpen() { return pdfSidebarOpen; }
|
||||
public static int getPdf2JpgDpi() { return pdf2JpgDpi; }
|
||||
public static int getPdfTimeoutSmall() { return pdfTimeoutSmall; }
|
||||
public static int getPdfTimeoutMedium() { return pdfTimeoutMedium; }
|
||||
@@ -566,10 +563,6 @@ public class ConfigConstants {
|
||||
public void setpdfDisableEditing(String pdfDisableEditing) { setPdfDisableEditingValue(pdfDisableEditing); }
|
||||
public static void setPdfDisableEditingValue(String pdfDisableEditing) { ConfigConstants.pdfDisableEditing = pdfDisableEditing; }
|
||||
|
||||
@Value("${pdf.sidebar.open:true}")
|
||||
public void setPdfSidebarOpen(String pdfSidebarOpen) { setPdfSidebarOpenValue(pdfSidebarOpen); }
|
||||
public static void setPdfSidebarOpenValue(String pdfSidebarOpen) { ConfigConstants.pdfSidebarOpen = pdfSidebarOpen; }
|
||||
|
||||
@Value("${pdf2jpg.dpi:105}")
|
||||
public void pdf2JpgDpi(int pdf2JpgDpi) { setPdf2JpgDpiValue(pdf2JpgDpi); }
|
||||
public static void setPdf2JpgDpiValue(int pdf2JpgDpi) { ConfigConstants.pdf2JpgDpi = pdf2JpgDpi; }
|
||||
@@ -664,7 +657,7 @@ public class ConfigConstants {
|
||||
public void setSize(String size) { setSizeValue(size); }
|
||||
public static void setSizeValue(String size) { ConfigConstants.size = size; }
|
||||
|
||||
@Value("${delete.password:false}")
|
||||
@Value("${delete.password:123456}")
|
||||
public void setPassword(String password) { setPasswordValue(password); }
|
||||
public static void setPasswordValue(String password) { ConfigConstants.password = password; }
|
||||
|
||||
@@ -853,4 +846,4 @@ public class ConfigConstants {
|
||||
@Value("${kk.scriptjs:false}")
|
||||
public void setscriptJs(String scriptJs) { setscriptJsValue(Boolean.parseBoolean(scriptJs)); }
|
||||
public static void setscriptJsValue(boolean scriptJs) { ConfigConstants.scriptJs = scriptJs; }
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,6 @@ public class ConfigRefreshComponent {
|
||||
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.setPdfSidebarOpenValue(getProperty(properties, "pdf.sidebar.open", ConfigConstants.DEFAULT_PDF_SIDEBAR_OPEN));
|
||||
ConfigConstants.setPdf2JpgDpiValue(Integer.parseInt(getProperty(properties, "pdf2jpg.dpi", ConfigConstants.DEFAULT_PDF2_JPG_DPI)));
|
||||
|
||||
// 8. CAD配置
|
||||
@@ -285,4 +284,4 @@ public class ConfigRefreshComponent {
|
||||
WatermarkConfigConstants.setWatermarkHeightValue(getProperty(properties, "watermark.height", WatermarkConfigConstants.DEFAULT_WATERMARK_HEIGHT));
|
||||
WatermarkConfigConstants.setWatermarkAngleValue(getProperty(properties, "watermark.angle", WatermarkConfigConstants.DEFAULT_WATERMARK_ANGLE));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package cn.keking.config;
|
||||
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RedissonClient;
|
||||
@@ -12,8 +13,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Redisson 客户端配置(完善版)
|
||||
* 支持 single / cluster / master-slave / sentinel 四种模式,配置完整,统一参数。
|
||||
* Redisson 客户端配置
|
||||
* Created by kl on 2017/09/26.
|
||||
*/
|
||||
@ConditionalOnExpression("'${cache.type:default}'.equals('redis')")
|
||||
@ConfigurationProperties(prefix = "spring.redisson")
|
||||
@@ -21,71 +22,114 @@ import org.springframework.util.ClassUtils;
|
||||
public class RedissonConfig {
|
||||
|
||||
// ========================== 连接配置 ==========================
|
||||
private String address;
|
||||
private String password;
|
||||
private String clientName;
|
||||
private int database = 0;
|
||||
private String mode = "single";
|
||||
private String masterName = "kkfile";
|
||||
private static String address;
|
||||
private static String password;
|
||||
private static String clientName;
|
||||
private static int database = 0;
|
||||
private static String mode = "single";
|
||||
private static String masterName = "kkfile";
|
||||
|
||||
// ========================== 超时配置 ==========================
|
||||
private int idleConnectionTimeout = 10000;
|
||||
private int connectTimeout = 10000;
|
||||
private int timeout = 3000;
|
||||
private static int idleConnectionTimeout = 10000;
|
||||
private static int connectTimeout = 10000;
|
||||
private static int timeout = 3000;
|
||||
|
||||
// ========================== 重试配置 ==========================
|
||||
private int retryAttempts = 3;
|
||||
private int retryInterval = 1500;
|
||||
private static int retryAttempts = 3;
|
||||
private static int retryInterval = 1500;
|
||||
|
||||
// ========================== 连接池配置 ==========================
|
||||
private int connectionMinimumIdleSize = 10;
|
||||
private int connectionPoolSize = 64;
|
||||
private int subscriptionsPerConnection = 5;
|
||||
private int subscriptionConnectionMinimumIdleSize = 1;
|
||||
private int subscriptionConnectionPoolSize = 50;
|
||||
|
||||
// ========================== 集群专用配置 ==========================
|
||||
private int scanInterval = 2000;
|
||||
private static int connectionMinimumIdleSize = 10;
|
||||
private static int connectionPoolSize = 64;
|
||||
private static int subscriptionsPerConnection = 5;
|
||||
private static int subscriptionConnectionMinimumIdleSize = 1;
|
||||
private static int subscriptionConnectionPoolSize = 50;
|
||||
|
||||
// ========================== 其他配置 ==========================
|
||||
private int dnsMonitoringInterval = 5000;
|
||||
private int threads; // 默认为0,表示使用 CPU 核数 * 2
|
||||
private String codec = "org.redisson.codec.JsonJacksonCodec";
|
||||
private static int dnsMonitoringInterval = 5000;
|
||||
private static int thread; // 当前处理核数量 * 2
|
||||
private static String codec = "org.redisson.codec.JsonJacksonCodec";
|
||||
|
||||
@Bean
|
||||
public RedissonClient redissonClient() {
|
||||
public static RedissonClient config() throws Exception {
|
||||
Config config = new Config();
|
||||
|
||||
// 密码处理:空字符串转为 null
|
||||
String pwd = StringUtils.isBlank(password) ? null : password;
|
||||
// 密码处理
|
||||
if (StringUtils.isBlank(password)) {
|
||||
password = null;
|
||||
}
|
||||
|
||||
// 根据模式构建配置
|
||||
switch (mode.toLowerCase()) {
|
||||
// 根据模式创建对应的 Redisson 配置
|
||||
switch (mode) {
|
||||
case "cluster":
|
||||
configureClusterMode(config, pwd);
|
||||
configureClusterMode(config);
|
||||
break;
|
||||
case "master-slave":
|
||||
configureMasterSlaveMode(config, pwd);
|
||||
configureMasterSlaveMode(config);
|
||||
break;
|
||||
case "sentinel":
|
||||
configureSentinelMode(config, pwd);
|
||||
configureSentinelMode(config);
|
||||
break;
|
||||
default:
|
||||
configureSingleMode(config, pwd);
|
||||
configureSingleMode(config);
|
||||
break;
|
||||
}
|
||||
|
||||
// 公共配置:编码器、线程数
|
||||
applyCommonConfig(config);
|
||||
return Redisson.create(config);
|
||||
}
|
||||
|
||||
// ========================== 配置方法 ==========================
|
||||
|
||||
private void configureSingleMode(Config config, String pwd) {
|
||||
String normalizedAddress = normalizeAddress(address);
|
||||
/**
|
||||
* 配置集群模式
|
||||
*/
|
||||
private static void configureClusterMode(Config config) {
|
||||
String[] clusterAddresses = address.split(",");
|
||||
config.useClusterServers()
|
||||
.setScanInterval(2000)
|
||||
.addNodeAddress(clusterAddresses)
|
||||
.setPassword(password)
|
||||
.setRetryAttempts(retryAttempts)
|
||||
.setTimeout(timeout)
|
||||
.setMasterConnectionPoolSize(100)
|
||||
.setSlaveConnectionPoolSize(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置主从模式
|
||||
*/
|
||||
private static void configureMasterSlaveMode(Config config) {
|
||||
String[] masterSlaveAddresses = address.split(",");
|
||||
validateMasterSlaveAddresses(masterSlaveAddresses);
|
||||
|
||||
String[] slaveAddresses = new String[masterSlaveAddresses.length - 1];
|
||||
System.arraycopy(masterSlaveAddresses, 1, slaveAddresses, 0, slaveAddresses.length);
|
||||
|
||||
config.useMasterSlaveServers()
|
||||
.setDatabase(database)
|
||||
.setPassword(password)
|
||||
.setMasterAddress(masterSlaveAddresses[0])
|
||||
.addSlaveAddress(slaveAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置哨兵模式
|
||||
*/
|
||||
private static void configureSentinelMode(Config config) {
|
||||
String[] sentinelAddresses = address.split(",");
|
||||
config.useSentinelServers()
|
||||
.setDatabase(database)
|
||||
.setPassword(password)
|
||||
.setMasterName(masterName)
|
||||
.addSentinelAddress(sentinelAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置单机模式
|
||||
*/
|
||||
private static void configureSingleMode(Config config) throws Exception {
|
||||
config.useSingleServer()
|
||||
.setAddress(normalizedAddress)
|
||||
.setAddress(address)
|
||||
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
||||
.setConnectionPoolSize(connectionPoolSize)
|
||||
.setDatabase(database)
|
||||
@@ -99,184 +143,183 @@ public class RedissonConfig {
|
||||
.setTimeout(timeout)
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||
.setPassword(pwd);
|
||||
}
|
||||
.setPassword(StringUtils.trimToNull(password));
|
||||
|
||||
private void configureClusterMode(Config config, String pwd) {
|
||||
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);
|
||||
}
|
||||
|
||||
private void configureMasterSlaveMode(Config config, String pwd) {
|
||||
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);
|
||||
}
|
||||
|
||||
private void configureSentinelMode(Config config, String pwd) {
|
||||
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);
|
||||
}
|
||||
|
||||
private void applyCommonConfig(Config config) {
|
||||
// 设置编码器
|
||||
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);
|
||||
}
|
||||
Class<?> codecClass = ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader());
|
||||
Codec codecInstance = (Codec) codecClass.getDeclaredConstructor().newInstance();
|
||||
config.setCodec(codecInstance);
|
||||
// 设置线程和事件循环组
|
||||
config.setThreads(thread);
|
||||
config.setEventLoopGroup(new NioEventLoopGroup());
|
||||
}
|
||||
|
||||
// ========================== 辅助方法 ==========================
|
||||
|
||||
/**
|
||||
* 自动补齐 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;
|
||||
}
|
||||
|
||||
private String[] normalizeAddresses(String[] addresses) {
|
||||
String[] normalized = new String[addresses.length];
|
||||
for (int i = 0; i < addresses.length; i++) {
|
||||
normalized[i] = normalizeAddress(addresses[i]);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private void validateMasterSlaveAddresses(String[] addresses) {
|
||||
if (addresses.length < 2) {
|
||||
private static void validateMasterSlaveAddresses(String[] addresses) {
|
||||
if (addresses.length == 1) {
|
||||
throw new IllegalArgumentException(
|
||||
"Master-slave mode requires at least 2 addresses: master and at least one slave. " +
|
||||
"Current addresses: " + String.join(",", addresses));
|
||||
"redis.redisson.address MUST have multiple redis addresses for master-slave mode.");
|
||||
}
|
||||
}
|
||||
|
||||
// ========================== Getter / Setter(供 Spring 绑定配置) ==========================
|
||||
// 以下所有字段都需要提供 getter/setter,示例中只列出关键字段,实际使用时请补全所有字段。
|
||||
// 建议使用 Lombok @Data 或 IDE 自动生成。这里只展示部分,避免篇幅过长。
|
||||
// ========================== Getter和Setter方法 ==========================
|
||||
|
||||
public String getAddress() { return address; }
|
||||
public void setAddress(String address) { this.address = address; }
|
||||
// 连接配置
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public void setAddress(String address) {
|
||||
RedissonConfig.address = address;
|
||||
}
|
||||
|
||||
public String getClientName() { return clientName; }
|
||||
public void setClientName(String clientName) { this.clientName = clientName; }
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public int getDatabase() { return database; }
|
||||
public void setDatabase(int database) { this.database = database; }
|
||||
public void setPassword(String password) {
|
||||
RedissonConfig.password = password;
|
||||
}
|
||||
|
||||
public String getMode() { return mode; }
|
||||
public void setMode(String mode) { this.mode = mode; }
|
||||
public String getClientName() {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
public String getMasterName() { return masterName; }
|
||||
public void setMasterName(String masterName) { this.masterName = masterName; }
|
||||
public void setClientName(String clientName) {
|
||||
RedissonConfig.clientName = clientName;
|
||||
}
|
||||
|
||||
public int getIdleConnectionTimeout() { return idleConnectionTimeout; }
|
||||
public void setIdleConnectionTimeout(int idleConnectionTimeout) { this.idleConnectionTimeout = idleConnectionTimeout; }
|
||||
public int getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public int getConnectTimeout() { return connectTimeout; }
|
||||
public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; }
|
||||
public void setDatabase(int database) {
|
||||
RedissonConfig.database = database;
|
||||
}
|
||||
|
||||
public int getTimeout() { return timeout; }
|
||||
public void setTimeout(int timeout) { this.timeout = timeout; }
|
||||
public static String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public int getRetryAttempts() { return retryAttempts; }
|
||||
public void setRetryAttempts(int retryAttempts) { this.retryAttempts = retryAttempts; }
|
||||
public void setMode(String mode) {
|
||||
RedissonConfig.mode = mode;
|
||||
}
|
||||
|
||||
public int getRetryInterval() { return retryInterval; }
|
||||
public void setRetryInterval(int retryInterval) { this.retryInterval = retryInterval; }
|
||||
public static String getMasterNamee() {
|
||||
return masterName;
|
||||
}
|
||||
|
||||
public int getConnectionMinimumIdleSize() { return connectionMinimumIdleSize; }
|
||||
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) { this.connectionMinimumIdleSize = connectionMinimumIdleSize; }
|
||||
public void setMasterNamee(String masterName) {
|
||||
RedissonConfig.masterName = masterName;
|
||||
}
|
||||
|
||||
public int getConnectionPoolSize() { return connectionPoolSize; }
|
||||
public void setConnectionPoolSize(int connectionPoolSize) { this.connectionPoolSize = connectionPoolSize; }
|
||||
// 超时配置
|
||||
public int getIdleConnectionTimeout() {
|
||||
return idleConnectionTimeout;
|
||||
}
|
||||
|
||||
public int getSubscriptionsPerConnection() { return subscriptionsPerConnection; }
|
||||
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) { this.subscriptionsPerConnection = subscriptionsPerConnection; }
|
||||
public void setIdleConnectionTimeout(int idleConnectionTimeout) {
|
||||
RedissonConfig.idleConnectionTimeout = idleConnectionTimeout;
|
||||
}
|
||||
|
||||
public int getSubscriptionConnectionMinimumIdleSize() { return subscriptionConnectionMinimumIdleSize; }
|
||||
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) { this.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize; }
|
||||
public int getConnectTimeout() {
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
public int getSubscriptionConnectionPoolSize() { return subscriptionConnectionPoolSize; }
|
||||
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) { this.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize; }
|
||||
public void setConnectTimeout(int connectTimeout) {
|
||||
RedissonConfig.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public int getScanInterval() { return scanInterval; }
|
||||
public void setScanInterval(int scanInterval) { this.scanInterval = scanInterval; }
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public int getDnsMonitoringInterval() { return dnsMonitoringInterval; }
|
||||
public void setDnsMonitoringInterval(int dnsMonitoringInterval) { this.dnsMonitoringInterval = dnsMonitoringInterval; }
|
||||
public void setTimeout(int timeout) {
|
||||
RedissonConfig.timeout = timeout;
|
||||
}
|
||||
|
||||
public int getThreads() { return threads; }
|
||||
public void setThreads(int threads) { this.threads = threads; }
|
||||
// 重试配置
|
||||
public int getRetryAttempts() {
|
||||
return retryAttempts;
|
||||
}
|
||||
|
||||
public String getCodec() { return codec; }
|
||||
public void setCodec(String codec) { this.codec = codec; }
|
||||
public void setRetryAttempts(int retryAttempts) {
|
||||
RedissonConfig.retryAttempts = retryAttempts;
|
||||
}
|
||||
|
||||
public int getRetryInterval() {
|
||||
return retryInterval;
|
||||
}
|
||||
|
||||
public void setRetryInterval(int retryInterval) {
|
||||
RedissonConfig.retryInterval = retryInterval;
|
||||
}
|
||||
|
||||
// 连接池配置
|
||||
public int getConnectionMinimumIdleSize() {
|
||||
return connectionMinimumIdleSize;
|
||||
}
|
||||
|
||||
public void setConnectionMinimumIdleSize(int connectionMinimumIdleSize) {
|
||||
RedissonConfig.connectionMinimumIdleSize = connectionMinimumIdleSize;
|
||||
}
|
||||
|
||||
public int getConnectionPoolSize() {
|
||||
return connectionPoolSize;
|
||||
}
|
||||
|
||||
public void setConnectionPoolSize(int connectionPoolSize) {
|
||||
RedissonConfig.connectionPoolSize = connectionPoolSize;
|
||||
}
|
||||
|
||||
public int getSubscriptionsPerConnection() {
|
||||
return subscriptionsPerConnection;
|
||||
}
|
||||
|
||||
public void setSubscriptionsPerConnection(int subscriptionsPerConnection) {
|
||||
RedissonConfig.subscriptionsPerConnection = subscriptionsPerConnection;
|
||||
}
|
||||
|
||||
public int getSubscriptionConnectionMinimumIdleSize() {
|
||||
return subscriptionConnectionMinimumIdleSize;
|
||||
}
|
||||
|
||||
public void setSubscriptionConnectionMinimumIdleSize(int subscriptionConnectionMinimumIdleSize) {
|
||||
RedissonConfig.subscriptionConnectionMinimumIdleSize = subscriptionConnectionMinimumIdleSize;
|
||||
}
|
||||
|
||||
public int getSubscriptionConnectionPoolSize() {
|
||||
return subscriptionConnectionPoolSize;
|
||||
}
|
||||
|
||||
public void setSubscriptionConnectionPoolSize(int subscriptionConnectionPoolSize) {
|
||||
RedissonConfig.subscriptionConnectionPoolSize = subscriptionConnectionPoolSize;
|
||||
}
|
||||
|
||||
// 其他配置
|
||||
public int getDnsMonitoringInterval() {
|
||||
return dnsMonitoringInterval;
|
||||
}
|
||||
|
||||
public void setDnsMonitoringInterval(int dnsMonitoringInterval) {
|
||||
RedissonConfig.dnsMonitoringInterval = dnsMonitoringInterval;
|
||||
}
|
||||
|
||||
public int getThread() {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public void setThread(int thread) {
|
||||
RedissonConfig.thread = thread;
|
||||
}
|
||||
|
||||
public static String getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void setCodec(String codec) {
|
||||
RedissonConfig.codec = codec;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,11 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
filterUri.add("/picturesPreview");
|
||||
filterUri.add("/getCorsFile");
|
||||
filterUri.add("/addTask");
|
||||
filterUri.add("/pdfjs/web/viewer.html");
|
||||
filterUri.add("/msg/index.html");
|
||||
filterUri.add("/eml/index.html");
|
||||
filterUri.add("/heic/index.html");
|
||||
filterUri.add("/drawio/index.html");
|
||||
TrustHostFilter filter = new TrustHostFilter();
|
||||
FilterRegistrationBean<TrustHostFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(filter);
|
||||
|
||||
@@ -38,7 +38,7 @@ public enum FileType {
|
||||
|
||||
private static final String[] OFFICE_TYPES = {"docx", "wps", "doc", "docm", "xls", "xlsx", "csv" ,"xlsm", "ppt", "pptx", "vsd", "rtf", "odt", "wmf", "emf", "dps", "et", "ods", "ots", "tsv", "odp", "otp", "sxi", "ott", "vsdx", "fodt", "fods", "xltx","tga","psd","dotm","ett","xlt","xltm","wpt","dot","xlam","dotx","xla","pages", "eps", "pptm"};
|
||||
private static final String[] PICTURE_TYPES = {"jpg", "jpeg", "png", "gif", "bmp", "ico", "jfif", "webp", "heic", "avif", "heif"};
|
||||
private static final String[] ARCHIVE_TYPES = {"rar", "zip", "jar", "7-zip", "tar", "gzip", "7z"};
|
||||
private static final String[] ARCHIVE_TYPES = {"rar", "zip", "jar", "7-zip", "tar", "gzip", "7z", "tgz"};
|
||||
private static final String[] ONLINE3D_TYPES = {"obj", "3ds", "stl", "ply", "off", "3dm", "fbx", "dae", "wrl", "3mf", "ifc","glb","o3dv","gltf","stp","bim","fcstd","step","iges","brep"};
|
||||
private static final String[] EML_TYPES = {"eml"};
|
||||
private static final String[] MSG_TYPES = {"msg"};
|
||||
|
||||
@@ -59,26 +59,21 @@ public class CompressFileReader {
|
||||
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
|
||||
if (!item.isFolder()) {
|
||||
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
|
||||
Files.deleteIfExists(filePathInsideArchive);
|
||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), false))) {
|
||||
ExtractOperationResult result = item.extractSlow(data -> {
|
||||
try {
|
||||
out.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return data.length;
|
||||
}, filePassword);
|
||||
if (result != ExtractOperationResult.OK) {
|
||||
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
|
||||
if (result1.equals(result)) {
|
||||
throw new Exception("Password");
|
||||
} else {
|
||||
throw new Exception("Failed to extract RAR file.");
|
||||
}
|
||||
ExtractOperationResult result = item.extractSlow(data -> {
|
||||
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), true))) {
|
||||
out.write(data);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return data.length;
|
||||
}, filePassword);
|
||||
if (result != ExtractOperationResult.OK) {
|
||||
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
|
||||
if (result1.equals(result)) {
|
||||
throw new Exception("Password");
|
||||
}else {
|
||||
throw new Exception("Failed to extract RAR file.");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
|
||||
@@ -115,4 +110,4 @@ public class CompressFileReader {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package cn.keking.service;
|
||||
import cn.keking.model.FileAttribute;
|
||||
import cn.keking.model.FileType;
|
||||
import cn.keking.service.cache.CacheService;
|
||||
import cn.keking.web.filter.TrustDirFilter;
|
||||
import cn.keking.web.filter.TrustHostFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -58,6 +60,10 @@ public class FileConvertQueueTask {
|
||||
try {
|
||||
url = cacheService.takeQueueTask();
|
||||
if (url != null) {
|
||||
if (!TrustHostFilter.isTrustedSourceUrl(url) || !TrustDirFilter.isTrustedFileUrl(url)) {
|
||||
logger.warn("拒绝处理不受信任的预览转换任务,url:{}", url);
|
||||
continue;
|
||||
}
|
||||
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(url, null);
|
||||
FileType fileType = fileAttribute.getType();
|
||||
logger.info("正在处理预览转换任务,url:{},预览类型:{}", url, fileType);
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -94,8 +93,6 @@ public class PdfToJpgService {
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
refreshImageIoPlugins();
|
||||
|
||||
int maxThreads = ConfigConstants.getPdfMaxThreads();
|
||||
// 使用固定大小的虚拟线程池
|
||||
this.virtualThreadExecutor = Executors.newFixedThreadPool(maxThreads,
|
||||
@@ -107,13 +104,6 @@ public class PdfToJpgService {
|
||||
scheduleCacheCleanup();
|
||||
}
|
||||
|
||||
static void refreshImageIoPlugins() {
|
||||
// ImageIO only scans once automatically. If another launcher or Java agent initializes
|
||||
// it before Spring Boot installs its application class loader, nested JAR providers such
|
||||
// as jbig2-imageio remain invisible until the application class path is scanned again.
|
||||
ImageIO.scanForPlugins();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
logger.info("开始关闭PDF转换服务...");
|
||||
@@ -862,4 +852,4 @@ public class PdfToJpgService {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package cn.keking.service.cache.impl;
|
||||
|
||||
import cn.keking.service.cache.CacheService;
|
||||
import org.redisson.Redisson;
|
||||
import org.redisson.api.RBlockingQueue;
|
||||
import org.redisson.api.RMapCache;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.redisson.config.Config;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -21,9 +23,8 @@ public class CacheServiceRedisImpl implements CacheService {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
// 直接注入 Spring 容器中的 RedissonClient Bean
|
||||
public CacheServiceRedisImpl(RedissonClient redissonClient) {
|
||||
this.redissonClient = redissonClient;
|
||||
public CacheServiceRedisImpl(Config config) {
|
||||
this.redissonClient = Redisson.create(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -348,4 +348,4 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import org.apache.commons.io.FileUtils;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -47,8 +46,9 @@ public class DownloadUtils {
|
||||
}
|
||||
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
|
||||
String realPath = getRelFilePath(fileName, fileAttribute);
|
||||
// 获取文件后缀用于校验
|
||||
final String fileSuffix = fileAttribute.getSuffix();
|
||||
|
||||
// 判断是否非法地址
|
||||
if (KkFileUtils.isIllegalFileName(realPath)) {
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
@@ -61,17 +61,17 @@ public class DownloadUtils {
|
||||
response.setMsg("下载失败:不支持的类型!" + urlStr);
|
||||
return response;
|
||||
}
|
||||
if (fileAttribute.isCompressFile()) {
|
||||
if (fileAttribute.isCompressFile()) { //压缩包文件 直接赋予路径 不予下载
|
||||
response.setContent(fileDir + fileName);
|
||||
response.setMsg(fileName);
|
||||
return response;
|
||||
}
|
||||
// 如果文件是否已经存在、且不强制更新,则直接返回文件路径
|
||||
if (KkFileUtils.isExist(realPath) && !fileAttribute.forceUpdatedCache()) {
|
||||
response.setContent(realPath);
|
||||
response.setMsg(fileName);
|
||||
return response;
|
||||
}
|
||||
|
||||
try {
|
||||
URL url = WebUtils.normalizedURL(urlStr);
|
||||
if (!fileAttribute.getSkipDownLoad()) {
|
||||
@@ -79,59 +79,39 @@ public class DownloadUtils {
|
||||
File realFile = new File(realPath);
|
||||
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||
String finalUrlStr = urlStr;
|
||||
|
||||
final boolean[] hasMimeError = {false};
|
||||
final String[] mimeErrorMessage = {null};
|
||||
|
||||
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
|
||||
// 获取响应头中的Content-Type
|
||||
String contentType = responseWrapper.getContentType();
|
||||
|
||||
// 如果是Office/设计文件,需要校验MIME类型
|
||||
if (WebUtils.isMimeCheckRequired(fileSuffix)) {
|
||||
if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
|
||||
logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}",
|
||||
finalUrlStr, contentType);
|
||||
hasMimeError[0] = true;
|
||||
mimeErrorMessage[0] = "期望二进制文件但接收到文本类型,Content-Type: " + contentType;
|
||||
responseWrapper.setHasError(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
|
||||
});
|
||||
|
||||
if (hasMimeError[0]) {
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg(mimeErrorMessage[0]);
|
||||
return response;
|
||||
}
|
||||
|
||||
} else if (isFtpUrl(url)) {
|
||||
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
||||
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
||||
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
||||
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
|
||||
FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
} else if (isFileUrl(url)) {
|
||||
} else if (isFileUrl(url)) { // 添加对file协议的支持
|
||||
handleFileProtocol(url, realPath);
|
||||
} else {
|
||||
response.setCode(1);
|
||||
response.setMsg("url不能识别url" + urlStr);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
response.setContent(realPath);
|
||||
response.setMsg(fileName);
|
||||
return response;
|
||||
|
||||
} catch (HttpClientErrorException e) {
|
||||
logger.error("HTTP请求失败,状态码:{},url:{}", e.getStatusCode(), urlStr);
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
if (e.getStatusCode().is4xxClientError()) {
|
||||
response.setMsg("文件不存在或无法访问 (HTTP " + e.getStatusCode() + ")");
|
||||
} else {
|
||||
response.setMsg("下载失败: " + e.getMessage());
|
||||
}
|
||||
return response;
|
||||
} catch (IOException | GalimatiasParseException e) {
|
||||
logger.error("文件下载失败,url:{}", urlStr);
|
||||
response.setCode(1);
|
||||
@@ -143,11 +123,7 @@ public class DownloadUtils {
|
||||
}
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
logger.error("下载文件时发生未知异常,url:{}", urlStr, e);
|
||||
response.setCode(1);
|
||||
response.setContent(null);
|
||||
response.setMsg("下载失败: " + e.getMessage());
|
||||
return response;
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package cn.keking.utils;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
@Component
|
||||
public class HttpClientLifecycle {
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
System.out.println("Spring 容器关闭,释放 HTTP 连接池资源...");
|
||||
HttpRequestUtils.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import cn.keking.utils.WebUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -34,8 +33,6 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
||||
@@ -151,6 +148,28 @@ public class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
private Path getDemoBasePath() {
|
||||
return Paths.get(fileDir, demoDir).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private Path resolveDemoPath(String path) {
|
||||
Path demoBasePath = getDemoBasePath();
|
||||
try {
|
||||
if (ObjectUtils.isEmpty(path)) {
|
||||
return demoBasePath;
|
||||
}
|
||||
Path normalizedPath = demoBasePath.resolve(path).normalize();
|
||||
if (!normalizedPath.startsWith(demoBasePath)) {
|
||||
logger.warn("检测到非法目录访问,path:{}", path);
|
||||
return null;
|
||||
}
|
||||
return normalizedPath;
|
||||
} catch (InvalidPathException e) {
|
||||
logger.warn("解析目录路径失败,path:{}", path, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/fileUpload")
|
||||
public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "path", defaultValue = "") String path) {
|
||||
@@ -221,7 +240,7 @@ public class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/deleteFile")
|
||||
@GetMapping("/deleteFile")
|
||||
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
||||
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
||||
if (checkResult.isFailure()) {
|
||||
@@ -345,39 +364,32 @@ public class FileController {
|
||||
}
|
||||
|
||||
// ==================== 2. 构建路径和验证 ====================
|
||||
Path currentDir;
|
||||
try {
|
||||
currentDir = resolveDirectoryUnderRoot(Paths.get(fileDir, demoDir), path);
|
||||
} catch (InvalidPathException | SecurityException e) {
|
||||
logger.warn("拒绝访问 demo 目录之外的文件列表路径");
|
||||
result.put("total", 0);
|
||||
result.put("data", Collections.emptyList());
|
||||
result.put("error", "非法目录路径");
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
logger.error("解析 demo 目录失败", e);
|
||||
Path resolvedPath = resolveDemoPath(path);
|
||||
if (resolvedPath == null) {
|
||||
result.put("total", 0);
|
||||
result.put("data", Collections.emptyList());
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!Files.isDirectory(currentDir)) {
|
||||
File currentDir = resolvedPath.toFile();
|
||||
if (!currentDir.exists() || !currentDir.isDirectory()) {
|
||||
result.put("total", 0);
|
||||
result.put("data", Collections.emptyList());
|
||||
return result;
|
||||
}
|
||||
String basePath = resolvedPath.toString();
|
||||
|
||||
// ==================== 3. 收集所有文件路径 ====================
|
||||
List<Path> allPaths = new ArrayList<>();
|
||||
long collectStartTime = System.currentTimeMillis();
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(basePath))) {
|
||||
for (Path entry : stream) {
|
||||
allPaths.add(entry);
|
||||
stats.incrementFileCount();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("读取目录失败: {}", currentDir, e);
|
||||
logger.error("读取目录失败: {}", basePath, e);
|
||||
result.put("total", 0);
|
||||
result.put("data", Collections.emptyList());
|
||||
return result;
|
||||
@@ -506,46 +518,6 @@ public class FileController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an existing directory below the configured demo root.
|
||||
*
|
||||
* <p>Both lexical normalization and real-path checks are required: the
|
||||
* former blocks traversal and absolute paths, while the latter prevents a
|
||||
* symlink inside the demo directory from escaping the configured root.</p>
|
||||
*/
|
||||
static Path resolveDirectoryUnderRoot(Path root, String requestedPath) throws IOException {
|
||||
Path normalizedRoot = root.toAbsolutePath().normalize();
|
||||
String relativePath = requestedPath == null ? "" : requestedPath.replace('\\', '/');
|
||||
|
||||
if (relativePath.indexOf('\0') >= 0
|
||||
|| relativePath.startsWith("/")
|
||||
|| relativePath.matches("^[A-Za-z]:.*")) {
|
||||
throw new SecurityException("Absolute paths are not allowed");
|
||||
}
|
||||
|
||||
Path relative = Paths.get(relativePath);
|
||||
if (relative.isAbsolute()) {
|
||||
throw new SecurityException("Absolute paths are not allowed");
|
||||
}
|
||||
for (Path segment : relative) {
|
||||
if ("..".equals(segment.toString())) {
|
||||
throw new SecurityException("Parent path segments are not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
Path resolved = normalizedRoot.resolve(relative).normalize();
|
||||
if (!resolved.startsWith(normalizedRoot)) {
|
||||
throw new SecurityException("Path escapes the configured root");
|
||||
}
|
||||
|
||||
Path realRoot = normalizedRoot.toRealPath();
|
||||
Path realResolved = resolved.toRealPath();
|
||||
if (!realResolved.startsWith(realRoot)) {
|
||||
throw new SecurityException("Path escapes the configured root through a symbolic link");
|
||||
}
|
||||
return realResolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建性能统计信息
|
||||
*/
|
||||
@@ -778,22 +750,11 @@ public class FileController {
|
||||
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
||||
}
|
||||
|
||||
boolean captchaEnabled = ConfigConstants.getDeleteCaptcha();
|
||||
String expectedPassword = captchaEnabled ?
|
||||
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
|
||||
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
||||
ConfigConstants.getPassword();
|
||||
|
||||
if (!captchaEnabled && (!StringUtils.hasText(expectedPassword)
|
||||
|| "false".equalsIgnoreCase(expectedPassword))) {
|
||||
return ReturnResponse.failure("文件删除接口已禁用,请先配置 delete.password");
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(expectedPassword)) {
|
||||
return ReturnResponse.failure("验证码已失效,请刷新后重试!");
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(password.getBytes(StandardCharsets.UTF_8),
|
||||
expectedPassword.getBytes(StandardCharsets.UTF_8))) {
|
||||
if (!password.equalsIgnoreCase(expectedPassword)) {
|
||||
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
||||
return ReturnResponse.failure("删除文件失败,密码错误!");
|
||||
}
|
||||
@@ -825,4 +786,4 @@ public class FileController {
|
||||
File file = new File(fullPath + fileName);
|
||||
return file.exists();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,6 @@ public class IndexController {
|
||||
return "/main/integrated";
|
||||
}
|
||||
|
||||
@GetMapping( "/contact")
|
||||
public String go2Contact(){
|
||||
return "/main/contact";
|
||||
}
|
||||
|
||||
@GetMapping( "/")
|
||||
public String root() {
|
||||
return "/main/index";
|
||||
|
||||
@@ -8,6 +8,8 @@ import cn.keking.service.FilePreviewFactory;
|
||||
import cn.keking.service.cache.CacheService;
|
||||
import cn.keking.service.impl.OtherFilePreviewImpl;
|
||||
import cn.keking.utils.*;
|
||||
import cn.keking.web.filter.TrustDirFilter;
|
||||
import cn.keking.web.filter.TrustHostFilter;
|
||||
import fr.opensagres.xdocreport.core.io.IOUtils;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
@@ -23,8 +25,6 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
@@ -154,71 +154,34 @@ public class OnlinePreviewController {
|
||||
// 1. 验证接口是否开启
|
||||
if (!ConfigConstants.getGetCorsFile()) {
|
||||
logger.info("接口关闭,禁止访问!,url:{}", urlPath);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "接口已关闭");
|
||||
} catch (IOException ignored) {}
|
||||
return;
|
||||
}
|
||||
// 2. 验证访问权限
|
||||
//2. 验证访问权限
|
||||
if (WebUtils.validateKey(key)) {
|
||||
logger.info("访问不合法:访问密码不正确!,url:{}", urlPath);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "访问密码不正确");
|
||||
} catch (IOException ignored) {}
|
||||
return;
|
||||
}
|
||||
|
||||
URL url;
|
||||
try {
|
||||
urlPath = WebUtils.decodeUrl(urlPath, encryption);
|
||||
url = WebUtils.normalizedURL(urlPath);
|
||||
} catch (Exception ex) {
|
||||
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath), ex);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "URL 解析失败");
|
||||
} catch (IOException ignored) {}
|
||||
logger.error(String.format(BASE64_DECODE_ERROR_MSG, urlPath),ex);
|
||||
return;
|
||||
}
|
||||
|
||||
assert urlPath != null;
|
||||
if (!isHttpUrl(url) && !isFtpUrl(url)) {
|
||||
logger.info("读取跨域文件异常,可能存在非法访问,urlPath:{}", urlPath);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "不支持的协议");
|
||||
} catch (IOException ignored) {}
|
||||
return;
|
||||
}
|
||||
|
||||
FileAttribute fileAttribute = fileHandlerService.getFileAttribute(urlPath, req);
|
||||
InputStream inputStream = null;
|
||||
logger.info("读取跨域文件url:{}", urlPath);
|
||||
|
||||
if (!isFtpUrl(url)) {
|
||||
// HTTP/HTTPS 处理(修复:不关闭共享的 CloseableHttpClient)
|
||||
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||
try {
|
||||
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
|
||||
} catch (HttpClientErrorException e) {
|
||||
// 捕获 HTTP 4xx 错误(如 404)
|
||||
logger.error("HTTP 请求失败,状态码:{},url:{}", e.getStatusCode(), urlPath);
|
||||
try {
|
||||
if (e.getStatusCode().is4xxClientError()) {
|
||||
response.sendError(e.getStatusCode().value(), "文件不存在或无法访问");
|
||||
} else {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "下载文件时发生错误");
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 捕获其他异常(如连接超时、IO 异常等)
|
||||
logger.error("读取跨域文件异常,url:{}", urlPath, e);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "读取文件失败: " + e.getMessage());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
|
||||
} else {
|
||||
// FTP 处理
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
|
||||
String contentType = WebUtils.getContentTypeByFilename(filename);
|
||||
@@ -229,23 +192,10 @@ public class OnlinePreviewController {
|
||||
String ftpPassword = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PASSWORD);
|
||||
String ftpControlEncoding = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_CONTROL_ENCODING);
|
||||
String support = WebUtils.getUrlParameterReg(urlPath, URL_PARAM_FTP_PORT);
|
||||
inputStream = FtpUtils.preview(urlPath, support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
inputStream= FtpUtils.preview(urlPath,support, urlPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||
IOUtils.copy(inputStream, response.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
logger.error("读取跨域文件异常,url:{}", urlPath, e);
|
||||
try {
|
||||
// 根据异常信息判断是否为文件不存在
|
||||
if (e.getMessage() != null && (e.getMessage().contains("550") || e.getMessage().contains("File not found"))) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND, "FTP 文件不存在");
|
||||
} else {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 读取失败");
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
} catch (Exception e) {
|
||||
logger.error("FTP 预览发生未知异常,url:{}", urlPath, e);
|
||||
try {
|
||||
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "FTP 服务异常");
|
||||
} catch (IOException ignored) {}
|
||||
logger.error("读取跨域文件异常,url:{}", urlPath);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
@@ -283,6 +233,11 @@ public class OnlinePreviewController {
|
||||
logger.info("{},url:{}", errorMsg, fileUrls);
|
||||
return errorMsg;
|
||||
}
|
||||
if (!TrustHostFilter.isTrustedSourceUrl(fileUrls) || !TrustDirFilter.isTrustedFileUrl(fileUrls)) {
|
||||
String errorMsg = "访问不合法:来源地址不受信任!";
|
||||
logger.info("{},url:{}", errorMsg, fileUrls);
|
||||
return errorMsg;
|
||||
}
|
||||
logger.info("添加转码队列url:{}", fileUrls);
|
||||
cacheService.addQueueTask(fileUrls);
|
||||
return "success";
|
||||
|
||||
@@ -38,7 +38,6 @@ public class AttributeSetFilter implements Filter {
|
||||
request.setAttribute("pdfDownloadDisable", ConfigConstants.getPdfDownloadDisable());
|
||||
request.setAttribute("pdfBookmarkDisable", ConfigConstants.getPdfBookmarkDisable());
|
||||
request.setAttribute("pdfDisableEditing", ConfigConstants.getPdfDisableEditing());
|
||||
request.setAttribute("pdfSidebarOpen", ConfigConstants.getPdfSidebarOpen());
|
||||
request.setAttribute("switchDisabled", ConfigConstants.getOfficePreviewSwitchDisabled());
|
||||
request.setAttribute("fileUploadDisable", ConfigConstants.getFileUploadDisable());
|
||||
request.setAttribute("beian", ConfigConstants.getBeian());
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.Locale;
|
||||
public class TrustDirFilter implements Filter {
|
||||
|
||||
private String notTrustDirView;
|
||||
private final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(TrustDirFilter.class);
|
||||
|
||||
|
||||
@Override
|
||||
@@ -59,6 +59,47 @@ public class TrustDirFilter implements Filter {
|
||||
|
||||
}
|
||||
|
||||
public static boolean isTrustedFileUrl(String urlPath) {
|
||||
// 判断URL是否合法
|
||||
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
URL url = WebUtils.normalizedURL(urlPath);
|
||||
|
||||
if ("file".equals(url.getProtocol().toLowerCase(Locale.ROOT))) {
|
||||
String filePath = URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8.name());
|
||||
// 将文件路径转换为File对象
|
||||
File targetFile = new File(filePath);
|
||||
// 将配置目录也转换为File对象
|
||||
File fileDir = new File(ConfigConstants.getFileDir());
|
||||
File localPreviewDir = new File(ConfigConstants.getLocalPreviewDir());
|
||||
try {
|
||||
// 获取规范路径
|
||||
String canonicalFilePath = targetFile.getCanonicalPath();
|
||||
String canonicalFileDir = fileDir.getCanonicalPath();
|
||||
String canonicalLocalPreviewDir = localPreviewDir.getCanonicalPath();
|
||||
return isSubDirectory(canonicalFileDir, canonicalFilePath) || isSubDirectory(canonicalLocalPreviewDir, canonicalFilePath);
|
||||
} catch (IOException e) {
|
||||
LoggerFactory.getLogger(TrustDirFilter.class).warn("获取规范路径失败,使用原始路径比较", e);
|
||||
String absFilePath = targetFile.getAbsolutePath();
|
||||
String absFileDir = fileDir.getAbsolutePath();
|
||||
String absLocalPreviewDir = localPreviewDir.getAbsolutePath();
|
||||
absFilePath = absFilePath.replace('\\', '/');
|
||||
absFileDir = absFileDir.replace('\\', '/');
|
||||
absLocalPreviewDir = absLocalPreviewDir.replace('\\', '/');
|
||||
if (!absFileDir.endsWith("/")) absFileDir += "/";
|
||||
if (!absLocalPreviewDir.endsWith("/")) absLocalPreviewDir += "/";
|
||||
return absFilePath.startsWith(absFileDir) || absFilePath.startsWith(absLocalPreviewDir);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (IOException | GalimatiasParseException e) {
|
||||
LoggerFactory.getLogger(TrustDirFilter.class).error("解析URL异常,url:{}", urlPath, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allowPreview(String urlPath) {
|
||||
// 判断URL是否合法
|
||||
if (KkFileUtils.isIllegalFileName(urlPath) || !StringUtils.hasText(urlPath) || !WebUtils.isValidUrl(urlPath)) {
|
||||
@@ -107,7 +148,7 @@ public class TrustDirFilter implements Filter {
|
||||
/**
|
||||
* 检查子路径是否在父路径下(跨平台)
|
||||
*/
|
||||
private boolean isSubDirectory(String parentDir, String childPath) {
|
||||
private static boolean isSubDirectory(String parentDir, String childPath) {
|
||||
try {
|
||||
File parent = new File(parentDir);
|
||||
File child = new File(childPath);
|
||||
|
||||
@@ -48,7 +48,10 @@ public class TrustHostFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String url = WebUtils.getSourceUrl(request);
|
||||
String url = request.getParameter("file");
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
url = WebUtils.getSourceUrl(request);
|
||||
}
|
||||
String host = WebUtils.getHost(url);
|
||||
if (isNotTrustHost(host) || !WebUtils.isValidUrl(url)) {
|
||||
String currentHost = host == null ? "UNKNOWN" : host;
|
||||
@@ -67,6 +70,14 @@ public class TrustHostFilter implements Filter {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isTrustedSourceUrl(String url) {
|
||||
if (!WebUtils.isValidUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
String host = WebUtils.getHost(url);
|
||||
return !new TrustHostFilter().isNotTrustHost(host);
|
||||
}
|
||||
|
||||
public boolean isNotTrustHost(String host) {
|
||||
if (host == null || host.trim().isEmpty()) {
|
||||
logger.warn("主机名为空或无效,拒绝访问");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,302 +0,0 @@
|
||||
|
||||
<div id="IconMenuPanel">
|
||||
<div id="rdialogx_d" class="float_dialog2_fixed">
|
||||
<div id="icon_menu_panel" style="border:1" class="div_icon_menu_panel" >
|
||||
|
||||
<table id="savelinksHeaderTable" width="270" border="0" cellspacing="0" border-spacing="0" id="savelinks_table">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link2">
|
||||
<div id="dummy_header" class="tagSaveLinkHeader" style="text-indent: 1em;">Location Setup</div>
|
||||
<div style="float:right;margin-right:-34px;margin-top:-24px;" id="CloseObjectMenuX"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<table id="savelinksTable" width="270" border="0" cellspacing="0" border-spacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
xxxx
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="save_drawing">
|
||||
<img id="save_drawing_image" class="icon_img" src="../app/images/tools/SaveChanges_240x22_Inactive.png" border="0">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div id="link_tags_table">
|
||||
|
||||
<table id="savelinksTable_select" width="270" border="0" cellspacing="0" border-spacing="0" id="savelinks_table">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
____
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="unlink_location" class="select_link_text">Unlink Location</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="done_editing_location" class="select_link_text"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="edit_cancel_location" class="select_link_text">Edit Fields</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div id="location_text_dynamic">
|
||||
|
||||
<table id="locationTagsTable2" width="270" border="0" cellspacing="0" border-spacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link10_id_datalayer_name">
|
||||
<div id="loc_text" class="location_text">Connector</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link11" class="dropdown">
|
||||
<span class="dropdown-toggle" role="button" data-toggle="dropdown" href="#" data-target="#" id="drop_link_spaces">None Selected<b class="caret"></b></span>
|
||||
<ul id="spaces_drawing" class="dropdown-menu" role="menu" aria-labelledby="drop_link_spaces">
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<canvas width="10" height="10"></canvas>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12A">
|
||||
<div id="layer_text" class="location_text">Layer</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13A">
|
||||
<input id="layer_tag" type="text" class="input_tags" value=""/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12">
|
||||
<div id="loc_text" class="location_text">Type</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13">
|
||||
<input id="type_tag" type="text" class="input_tags" value=""/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link14">
|
||||
<div id="loc_text" class="location_text">Occupancy</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input id="occupancy_tag" type="text" class="input_tags" value=""/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td VALIGN=TOP>
|
||||
<div id="loc_text" class="location_text_tags">Tags</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link16">
|
||||
<textarea id="other_tags" class="styled_text_area"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="location_text_static">
|
||||
|
||||
<table id="locationTagsTable" width="270" border="0" cellspacing="0" border-spacing="0" >
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link10_s">
|
||||
<div id="loc_text" class="location_text">Name</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link11_s" class="location_text">
|
||||
<div id="location_s" class="location_text2">None Selected</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12A_s">
|
||||
<div id="loc_text" class="location_text">Layer</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13A_s">
|
||||
<div id="layer_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12_s">
|
||||
<div id="loc_text" class="location_text">Type</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13_s">
|
||||
<div id="type_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link14_s">
|
||||
<div id="loc_text" class="location_text">Occupancy</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link15_s">
|
||||
|
||||
<div id="occupancy_s" class="location_text2">-</div>
|
||||
|
||||
</div
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td VALIGN=TOP>
|
||||
<div id="cv_link16_s">
|
||||
<div id="loc_text" class="location_text_tags2">Tags</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link17_s">
|
||||
<div id="tags_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
|
||||
<div id="IconMenuPanel">
|
||||
<div id="rdialogx_d" class="float_dialog2_fixed">
|
||||
<div id="icon_menu_panel" style="border:1" class="div_icon_menu_panel" >
|
||||
|
||||
<table id="savelinksHeaderTable" width="270" border="0" cellspacing="0" border-spacing="0" id="savelinks_table">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link2">
|
||||
<div id="dummy_header" class="tagSaveLinkHeader" style="text-indent: 1em;">Location Setup</div>
|
||||
<div style="float:right;margin-right:-34px;margin-top:-24px;" id="CloseObjectMenuX"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
|
||||
<table id="savelinksTable" width="270" border="0" cellspacing="0" border-spacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
xxxx
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="save_drawing">
|
||||
<img id="save_drawing_image" class="icon_img" src="../app/images/tools/SaveChanges_240x22_Inactive.png" border="0">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
<div id="link_tags_table">
|
||||
|
||||
<table id="savelinksTable_select" width="270" border="0" cellspacing="0" border-spacing="0" id="savelinks_table">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
____
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="unlink_location" class="select_link_text">Unlink Location</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="done_editing_location" class="select_link_text"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="edit_cancel_location" class="select_link_text">Edit Fields</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="dummy">
|
||||
<canvas width="5" height="5"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
<div id="location_text_dynamic">
|
||||
|
||||
<table id="locationTagsTable2" width="270" border="0" cellspacing="0" border-spacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link10_id_datalayer_name">
|
||||
<div id="loc_text" class="location_text">Connector</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link11" class="dropdown">
|
||||
<span class="dropdown-toggle" role="button" data-toggle="dropdown" href="#" data-target="#" id="drop_link_spaces">None Selected<b class="caret"></b></span>
|
||||
<ul id="spaces_drawing" class="dropdown-menu" role="menu" aria-labelledby="drop_link_spaces">
|
||||
</ul>
|
||||
</div>
|
||||
<div id="cv_link11_B2">
|
||||
<input id="spaces_manual_tag" type="text" class="input_tags" value=""/>
|
||||
</div>
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link11_B3">
|
||||
<canvas width="10" height="10"></canvas>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12A">
|
||||
<div id="layer_text" class="location_text">Layer</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13A">
|
||||
<input id="layer_tag" type="text" class="input_tags" value=""/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12">
|
||||
<div id="loc_text" class="location_text">Type</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13">
|
||||
<input id="type_tag" type="text" class="input_tags" value=""/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link14">
|
||||
<div id="loc_text" class="location_text">Occupancy</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input id="occupancy_tag" type="text" class="input_tags" value=""/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td VALIGN=TOP>
|
||||
<div id="loc_text" class="location_text_tags">Tags</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link16">
|
||||
<textarea id="other_tags" class="styled_text_area"></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="location_text_static">
|
||||
|
||||
<table id="locationTagsTable" width="270" border="0" cellspacing="0" border-spacing="0" >
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link10_s">
|
||||
<div id="loc_text" class="location_text">Name</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link11_s" class="location_text">
|
||||
<div id="location_s" class="location_text2">None Selected</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12A_s">
|
||||
<div id="loc_text" class="location_text">Layer</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13A_s">
|
||||
<div id="layer_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link12_s">
|
||||
<div id="loc_text" class="location_text">Type</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link13_s">
|
||||
<div id="type_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
<div id="cv_link14_s">
|
||||
<div id="loc_text" class="location_text">Occupancy</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link15_s">
|
||||
|
||||
<div id="occupancy_s" class="location_text2">-</div>
|
||||
|
||||
</div
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td VALIGN=TOP>
|
||||
<div id="cv_link16_s">
|
||||
<div id="loc_text" class="location_text_tags2">Tags</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="whiteUnderbar">
|
||||
_
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="cv_link17_s">
|
||||
<div id="tags_s" class="location_text2">-</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
120722
server/src/main/resources/static/drawio/js/app.min.js
vendored
120722
server/src/main/resources/static/drawio/js/app.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Microsoft Excel</title><path d="M23 1.5q.41 0 .7.3.3.29.3.7v19q0 .41-.3.7-.29.3-.7.3H7q-.41 0-.7-.3-.3-.29-.3-.7V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h5V2.5q0-.41.3-.7.29-.3.7-.3zM6 13.28l1.42 2.66h2.14l-2.38-3.87 2.34-3.8H7.46l-1.3 2.4-.05.08-.04.09-.64-1.28-.66-1.29H2.59l2.27 3.82-2.48 3.85h2.16zM14.25 21v-3H7.5v3zm0-4.5v-3.75H12v3.75zm0-5.25V7.5H12v3.75zm0-5.25V3H7.5v3zm8.25 15v-3h-6.75v3zm0-4.5v-3.75h-6.75v3.75zm0-5.25V7.5h-6.75v3.75zm0-5.25V3h-6.75v3Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 563 B |
@@ -1 +0,0 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Adobe Acrobat Reader</title><path d="M23.63 15.3c-.71-.745-2.166-1.17-4.224-1.17-1.1 0-2.377.106-3.761.354a19.443 19.443 0 0 1-2.307-2.661c-.532-.71-.994-1.49-1.42-2.236.817-2.484 1.207-4.507 1.207-5.962 0-1.632-.603-3.336-2.342-3.336-.532 0-1.065.32-1.349.781-.78 1.384-.425 4.4.923 7.381a60.277 60.277 0 0 1-1.703 4.507c-.568 1.349-1.207 2.733-1.917 4.01C2.834 18.53.314 20.34.03 21.758c-.106.533.071 1.03.462 1.42.142.107.639.533 1.49.533 2.59 0 5.323-4.188 6.707-6.707 1.065-.355 2.13-.71 3.194-.994a34.963 34.963 0 0 1 3.407-.745c2.732 2.448 5.145 2.839 6.352 2.839 1.49 0 2.023-.604 2.2-1.1.32-.64.106-1.349-.213-1.704zm-1.42 1.03c-.107.532-.64.887-1.384.887-.213 0-.39-.036-.604-.071-1.348-.32-2.626-.994-3.903-2.059a17.717 17.717 0 0 1 2.98-.248c.746 0 1.385.035 1.81.142.497.106 1.278.426 1.1 1.348zm-7.524-1.668a38.01 38.01 0 0 0-2.945.674 39.68 39.68 0 0 0-2.52.745 40.05 40.05 0 0 0 1.207-2.555c.426-.994.78-2.023 1.136-2.981.354.603.745 1.207 1.135 1.739a50.127 50.127 0 0 0 1.987 2.378zM10.038 1.46a.768.768 0 0 1 .674-.425c.745 0 .887.851.887 1.526 0 1.135-.355 2.874-.958 4.861-1.03-2.768-1.1-5.074-.603-5.962zM6.134 17.997c-1.81 2.981-3.549 4.826-4.613 4.826a.872.872 0 0 1-.532-.177c-.213-.213-.32-.461-.249-.745.213-1.065 2.271-2.555 5.394-3.904Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user