Compare commits

..

12 Commits

Author SHA1 Message Date
kl
1c6691d785 Clarify config defaults in AGENTS guide 2026-04-14 08:06:52 +08:00
kl
36ae290cb6 Add AI-friendly repository guide 2026-04-14 07:54:42 +08:00
kl
597715ce33 Merge pull request #738 from kekingcn/paseo/kkfileview
Refine archive preview and PDF defaults
2026-04-13 21:09:42 +08:00
kl
a8a08c1dcc Address PR review feedback 2026-04-13 21:02:59 +08:00
kl
7757729efd Refine archive preview and PDF defaults 2026-04-13 20:54:27 +08:00
kl
b246bfdac7 Merge pull request #737 from kekingcn/codex/fix-index-form-layout
fix: correct index preview form layout
2026-04-11 21:36:37 +08:00
chenkailing
d35393ba22 fix: tighten index form layout 2026-04-11 21:30:25 +08:00
chenkailing
c893dd7095 fix: keep preview parameters on one row 2026-04-11 21:12:06 +08:00
kl
9bdb18d833 Merge pull request #736 from kekingcn/codex/ci-auto-deploy-server-build
feat: add server-build auto deploy
2026-04-11 20:30:13 +08:00
chenkailing
58fc1af74f fix: address deploy review comments 2026-04-11 20:22:15 +08:00
chenkailing
1b3cf33bf0 feat: add server-build auto deploy 2026-04-11 19:36:19 +08:00
kl
c9005d0c04 Merge pull request #735 from kekingcn/codex/ci-auto-deploy-fix
fix: harden master auto deploy artifact delivery
2026-04-11 17:16:26 +08:00
15 changed files with 1489 additions and 223 deletions

View File

@@ -29,10 +29,18 @@ def main() -> int:
port = optional_env("KK_DEPLOY_PORT", "5985")
username = require_env("KK_DEPLOY_USERNAME")
password = require_env("KK_DEPLOY_PASSWORD")
deploy_root = optional_env("KK_DEPLOY_ROOT", r"C:\kkFileView-5.0")
health_url = optional_env("KK_DEPLOY_HEALTH_URL", "http://127.0.0.1:8012/")
artifact_url = require_env("KK_DEPLOY_ARTIFACT_URL")
dry_run = optional_env("KK_DEPLOY_DRY_RUN", "false").lower()
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")
@@ -74,17 +82,19 @@ $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 {{
$env:KK_DEPLOY_ARTIFACT_URL = '{ps_quote(artifact_url)}'
$env:KK_DEPLOY_ROOT = '{ps_quote(deploy_root)}'
$env:KK_DEPLOY_HEALTH_URL = '{ps_quote(health_url)}'
$env:KK_DEPLOY_DRY_RUN = '{ps_quote(dry_run)}'
"""
+ "\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 {{
Remove-Item Env:KK_DEPLOY_ARTIFACT_URL -ErrorAction SilentlyContinue
Remove-Item Env:KK_DEPLOY_ROOT -ErrorAction SilentlyContinue
Remove-Item Env:KK_DEPLOY_HEALTH_URL -ErrorAction SilentlyContinue
Remove-Item Env:KK_DEPLOY_DRY_RUN -ErrorAction SilentlyContinue
"""
+ "\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
}}

View File

@@ -1,4 +1,5 @@
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
function Write-Step {
param([string]$Message)
@@ -30,17 +31,22 @@ function Get-OptionalEnv {
return $Value
}
$ArtifactDownloadUrl = Get-RequiredEnv 'KK_DEPLOY_ARTIFACT_URL'
$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'
$ArtifactZip = Join-Path $DeployTmp 'artifact.zip'
$ExtractDir = Join-Path $DeployTmp 'artifact'
$BuildOutputDir = Join-Path (Join-Path $SourceRoot 'server') 'target'
if (-not (Test-Path $DeployRoot)) {
throw "Deploy root not found: $DeployRoot"
@@ -59,6 +65,23 @@ 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
@@ -66,6 +89,74 @@ 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"
@@ -75,41 +166,51 @@ if ($DryRun -eq 'true') {
New-Item -ItemType Directory -Force -Path $ReleaseDir | Out-Null
New-Item -ItemType Directory -Force -Path $DeployTmp | Out-Null
if (Test-Path $ArtifactZip) {
Remove-Item $ArtifactZip -Force
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
}
if (Test-Path $ExtractDir) {
Remove-Item $ExtractDir -Recurse -Force
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
}
Write-Step 'Downloading workflow artifact via signed URL'
$PreviousProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
try {
Invoke-WebRequest -Uri $ArtifactDownloadUrl -OutFile $ArtifactZip -UseBasicParsing -TimeoutSec 120
} finally {
$ProgressPreference = $PreviousProgressPreference
}
Sync-Repository
Build-KkFileView
if (-not (Test-Path $ArtifactZip)) {
throw "Artifact zip was not created: $ArtifactZip"
}
$ArtifactZipInfo = Get-Item $ArtifactZip
if ($ArtifactZipInfo.Length -le 0) {
throw "Downloaded artifact zip is empty: $ArtifactZip"
}
Expand-Archive -LiteralPath $ArtifactZip -DestinationPath $ExtractDir -Force
$DownloadedJars = Get-ChildItem $ExtractDir -Filter 'kkFileView-*.jar' -Recurse
$DownloadedJars = Get-ChildItem $BuildOutputDir -Filter 'kkFileView-*.jar' -File
if (-not $DownloadedJars) {
throw 'No kkFileView jar found inside downloaded workflow artifact'
throw "No kkFileView jar found in build output: $BuildOutputDir"
}
if ($DownloadedJars.Count -ne 1) {
throw "Expected exactly one kkFileView jar inside downloaded workflow artifact, found $($DownloadedJars.Count)"
throw "Expected exactly one kkFileView jar in build output, found $($DownloadedJars.Count)"
}
$DownloadedJar = $DownloadedJars[0]
@@ -118,27 +219,33 @@ $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)
$Processes = Get-CimInstance Win32_Process | Where-Object {
return Get-CimInstance Win32_Process | Where-Object {
$_.Name -match '^java(\.exe)?$' -and $_.CommandLine -and $_.CommandLine -match $JarPattern
}
}
foreach ($Process in $Processes) {
Write-Step "Stopping java process $($Process.ProcessId)"
Stop-Process -Id $Process.ProcessId -Force -ErrorAction SilentlyContinue
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)
$JarPattern = [regex]::Escape($JarName)
for ($i = 0; $i -lt $TimeoutSeconds; $i++) {
$Processes = Get-CimInstance Win32_Process | Where-Object {
$_.Name -match '^java(\.exe)?$' -and $_.CommandLine -and $_.CommandLine -match $JarPattern
}
if (-not $Processes) {
$JavaProcesses = @(Get-KkFileViewJavaProcesses)
$CmdProcesses = @(Get-KkFileViewLauncherProcesses)
if ((@($JavaProcesses).Count + @($CmdProcesses).Count) -eq 0) {
return $true
}
@@ -150,20 +257,37 @@ function Wait-KkFileViewStopped {
function Start-KkFileView {
Write-Step "Starting kkFileView"
Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', "`"$StartupScript`"" -WorkingDirectory $BinDir -WindowStyle Hidden
$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) {
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
}
}

View File

@@ -11,46 +11,24 @@ concurrency:
permissions:
contents: read
actions: read
jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: temurin
cache: maven
- name: Build with Maven
run: mvn -B package -Dmaven.test.skip=true --file pom.xml
- name: Upload server jar artifact
uses: actions/upload-artifact@v4
with:
name: kkfileview-server-jar
path: server/target/kkFileView-*.jar
retention-days: 7
deploy-windows:
needs: build
runs-on: ubuntu-22.04
env:
GITHUB_REPOSITORY_NAME: ${{ github.repository }}
GITHUB_RUN_ID_VALUE: ${{ github.run_id }}
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_ARTIFACT_NAME: kkfileview-server-jar
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
@@ -70,26 +48,5 @@ jobs:
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: Resolve artifact download URL
env:
GH_TOKEN: ${{ github.token }}
run: |
artifact_json=$(curl -fsSL \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$GITHUB_REPOSITORY_NAME/actions/runs/$GITHUB_RUN_ID_VALUE/artifacts")
artifact_id=$(ARTIFACT_JSON="$artifact_json" ARTIFACT_NAME="$KK_DEPLOY_ARTIFACT_NAME" python -c "import json, os; payload=json.loads(os.environ['ARTIFACT_JSON']); name=os.environ['ARTIFACT_NAME']; matches=[artifact for artifact in payload.get('artifacts', []) if artifact.get('name') == name]; matches or (_ for _ in ()).throw(SystemExit(f\"Artifact '{name}' not found for run\")); len(matches) == 1 or (_ for _ in ()).throw(SystemExit(f\"Expected one artifact named '{name}', found {len(matches)}\")); print(matches[0]['id'])")
headers_file=$(mktemp)
curl -fsS -D "$headers_file" -o /dev/null \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/$GITHUB_REPOSITORY_NAME/actions/artifacts/$artifact_id/zip"
artifact_url=$(awk 'BEGIN{IGNORECASE=1} /^location:/ { sub(/\r$/, "", $0); print substr($0, index($0, ":") + 2); exit }' "$headers_file")
test -n "$artifact_url" || (echo "Failed to resolve artifact download redirect URL" && exit 1)
rm -f "$headers_file"
echo "KK_DEPLOY_ARTIFACT_URL=$artifact_url" >> "$GITHUB_ENV"
- name: Deploy to Windows server
run: python .github/scripts/deploy_windows_winrm.py

1
.gitignore vendored
View File

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

230
AGENTS.md Normal file
View File

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

View File

@@ -8,17 +8,14 @@
- 运行配置`C:\kkFileView-5.0\config\test.properties`
- 健康检查地址`http://127.0.0.1:8012/`
服务器当前没有安装 `git` `mvn`因此自动部署链路采用
当前自动部署链路采用服务器拉最新源码并本机编译的方式
1. GitHub Actions `master` 合并后构建 `kkFileView-*.jar`
2. GitHub Actions runner 解析当前 workflow artifact 的临时下载地址
3. 通过 WinRM 连接 Windows 服务器
4. 由服务器通过临时下载地址拉取 jar artifact
5. 备份线上 jar替换为新版本
6. 使用现有 `startup.bat` 重启并做健康检查
7. 如果健康检查失败则自动回滚旧 jar 并重新拉起
这样做的目的是不把 GitHub token 下发到生产服务器服务器只接触一次性 artifact 下载链接
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
@@ -26,16 +23,35 @@
- `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`
- 构建产物`kkfileview-server-jar`
- 部署方式WinRM + runner 侧解析 artifact 临时下载地址 + Windows 服务器拉取 artifact
- 部署方式WinRM + 服务器源码同步 + 服务器本机 Maven 编译 + jar 替换/回滚

View File

@@ -97,11 +97,11 @@ office.type.web = ${KK_OFFICE_TYPE_WEB:web}
# Office文档预览类型
# 支持动态配置可选值image/pdf
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:image}
office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:pdf}
# 是否关闭Office预览模式切换开关默认为false允许切换
# 设置为true时用户无法在图片和PDF模式间切换
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:false}
office.preview.switch.disabled = ${KK_OFFICE_PREVIEW_SWITCH_DISABLED:true}
###############################################################################
@@ -475,4 +475,4 @@ kk.scriptjs = true
###############################################################################
# 纯文本文件类型直接显示
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
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}

View File

@@ -59,21 +59,26 @@ public class CompressFileReader {
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (!item.isFolder()) {
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
ExtractOperationResult result = item.extractSlow(data -> {
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), true))) {
out.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
return data.length;
}, filePassword);
if (result != ExtractOperationResult.OK) {
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
if (result1.equals(result)) {
throw new Exception("Password");
}else {
throw new Exception("Failed to extract RAR file.");
Files.deleteIfExists(filePathInsideArchive);
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), false))) {
ExtractOperationResult result = item.extractSlow(data -> {
try {
out.write(data);
} catch (IOException e) {
throw new RuntimeException(e);
}
return data.length;
}, filePassword);
if (result != ExtractOperationResult.OK) {
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
if (result1.equals(result)) {
throw new Exception("Password");
} else {
throw new Exception("Failed to extract RAR file.");
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
@@ -110,4 +115,4 @@ public class CompressFileReader {
}
}
}

View File

@@ -348,4 +348,4 @@ public class OfficeFilePreviewImpl implements FilePreview {
}
return null;
}
}
}

View File

@@ -547,28 +547,31 @@ a:focus {
}
.preview-options {
display: flex;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: center;
gap: 12px;
margin-bottom: 14px;
overflow-x: auto;
padding-bottom: 4px;
overflow: visible;
padding-bottom: 0;
}
.preview-grid {
display: flex;
flex-wrap: nowrap;
display: grid;
grid-template-columns: repeat(5, minmax(108px, 1fr));
gap: 12px;
margin-bottom: 0;
overflow: visible;
padding-bottom: 0;
flex: 1 1 auto;
min-width: 620px;
min-width: 0;
width: 100%;
}
.preview-grid .form-control {
flex: 1 1 0;
min-width: 150px;
width: 100%;
min-width: 0;
padding-left: 16px;
padding-right: 16px;
}
.preview-switches {
@@ -578,7 +581,7 @@ a:focus {
margin-bottom: 0;
overflow: visible;
padding-bottom: 0;
flex: 0 0 auto;
width: 100%;
}
.preview-switches label {
@@ -586,7 +589,7 @@ a:focus {
align-items: center;
gap: 8px;
margin: 0;
padding: 10px 14px;
padding: 10px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.76);
border: 1px solid rgba(17, 19, 21, 0.08);
@@ -1264,6 +1267,10 @@ a:focus {
.archive-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.preview-grid {
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
}
@media (max-width: 768px) {
@@ -1304,16 +1311,8 @@ a:focus {
grid-template-columns: 1fr;
}
.preview-grid {
flex-wrap: wrap;
min-width: 0;
overflow-x: visible;
}
.preview-options {
display: block;
overflow-x: visible;
padding-bottom: 0;
grid-template-columns: 1fr;
}
.preview-url {

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"/>
<link rel="stylesheet" href="bootstrap-table/bootstrap-table.min.css"/>
<link rel="stylesheet" href="css/theme.css"/>
<link rel="stylesheet" href="css/main-pages.css?v=v1-polish-20260411-3"/>
<link rel="stylesheet" href="css/main-pages.css?v=v1-polish-20260411-5"/>
<script type="text/javascript" src="js/jquery-3.6.1.min.js"></script>
<script type="text/javascript" src="js/jquery.form.min.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
@@ -493,8 +493,8 @@
search: false,
searchOnEnterKey: false,
showSearchButton: false,
showRefresh: true,
showColumns: true,
showRefresh: false,
showColumns: false,
clickToSelect: true,
locale: 'zh-CN',
columns: [{

View File

@@ -27,7 +27,7 @@
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url))+ "&key=${kkkey}";
}
document.getElementsByTagName('iframe')[0].src = "${baseUrl}pdfjs/web/viewer.html?file=" + encodeURIComponent(url) + "&disablepresentationmode=${pdfPresentationModeDisable}&disableopenfile=${pdfOpenFileDisable}&disableprint=${pdfPrintDisable}&disabledownload=${pdfDownloadDisable}&disablebookmark=${pdfBookmarkDisable}&disableediting=${pdfDisableEditing}";
document.getElementsByTagName('iframe')[0].src = "${baseUrl}pdfjs/web/viewer.html?file=" + encodeURIComponent(url) + "&disablepresentationmode=${pdfPresentationModeDisable}&disableopenfile=${pdfOpenFileDisable}&disableprint=${pdfPrintDisable}&disabledownload=${pdfDownloadDisable}&disablebookmark=${pdfBookmarkDisable}&disableediting=${pdfDisableEditing}#page=1&pagemode=thumbs";
document.getElementsByTagName('iframe')[0].height = document.documentElement.clientHeight - 10;
/**
* 页面变化调整高度

View File

@@ -9,7 +9,15 @@
<script src="js/base64.min.js"></script>
<style>
body {
background-color: #404040;
background-color: #f1f3f5;
}
.viewer-container:focus {
outline: none !important;
}
.viewer-container:focus-visible {
outline: 2px solid rgba(95, 107, 122, 0.65) !important;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(95, 107, 122, 0.14);
}
#image { width: 800px; margin: 0 auto; font-size: 0;}
#image li { display: inline-block;width: 50px;height: 50px; margin-left: 1%; padding-top: 1%;}
@@ -77,4 +85,4 @@
}
</script>
</body>
</html>
</html>

View File

@@ -26,6 +26,20 @@ public class PdfViewerCompatibilityTests {
assertTrue(workerScript.contains("import \"../web/compatibility.mjs\";"));
}
@Test
void shouldOpenPdfPreviewWithThumbnailSidebarByDefault() throws IOException {
String pdfTemplate = readResource("/web/pdf.ftl");
assertTrue(pdfTemplate.contains("#page=1&pagemode=thumbs"));
}
@Test
void shouldPreferPdfForOfficePreviewByDefault() throws IOException {
String properties = readResource("/application.properties");
assertTrue(properties.contains("office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:pdf}"));
}
private String readResource(String resourcePath) throws IOException {
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
assertNotNull(inputStream);