Compare commits

...

53 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
c2ad23128c Initial plan 2026-03-09 04:00:02 +00:00
kl
9e68025c3b test(e2e): address remaining copilot review comments 2026-03-09 11:59:13 +08:00
kl
b5c95b261d test(e2e): keep legacy zip temp dir ignored 2026-03-09 11:36:37 +08:00
kl
21be47f560 test(e2e): make tgz fixture gzip header deterministic 2026-03-09 11:29:52 +08:00
kl
8dd9b78c48 test(e2e): add rar smoke coverage and align archive deps 2026-03-09 11:19:27 +08:00
kl
4ab383709c test: address copilot archive fixture review feedback 2026-03-09 11:09:52 +08:00
kl
4690a5353b test(e2e): expand archive coverage to tar/tgz/7z/rar 2026-03-09 10:12:48 +08:00
kl
b10e14899d test(e2e): unify E2E CI command and align preview URL encoding (#719)
* test(e2e): follow-up fixes for post-merge copilot review feedback

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

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

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

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

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

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

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

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

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

* docs(e2e): align README with npm gen scripts and python3 usage
2026-03-04 14:34:32 +08:00
kl
a0d78c57e3 test(e2e): add MVP end-to-end automation suite and CI workflow (#713)
* test(e2e): add mvp playwright suite and PR workflow

* ci(e2e): use JDK 21 for kkFileView build
2026-03-04 10:46:41 +08:00
kl
7f16243270 chore(github): add accelerated support notice for urgent issues (#712)
* chore(github): add support acceleration notice to issue templates and auto-comments

* chore(actions): make copilot auto-comment bilingual and single-message
2026-03-03 19:28:23 +08:00
kl
36a75e86ac chore(github): add bilingual feature request issue template (#711)
* chore(github): add bilingual feature request issue template

* chore(github): refine feature template wording and split intake path
2026-03-03 19:10:05 +08:00
kl
7c41200028 chore(actions): auto-close >1y issues and trigger copilot triage comments 2026-03-03 18:57:59 +08:00
kl
3da0c523e8 chore(github): add bilingual required issue template 2026-03-03 18:26:39 +08:00
kl
8c3bc81e08 fix(security): support wildcard/cidr host pattern matching (#710)
* fix(security): support wildcard/cidr host pattern matching

* fix(security): harden host matching against null and DNS rebinding

* fix(security): handle ipv4 unsigned range and deny template fallback

* test(security): verify CIDR matching for IPv4 upper boundary

* fix(security): set UTF-8 deny response and use Locale.ROOT

* fix(security): enforce whitelist with blacklist and harden wildcard rules
2026-03-03 15:26:35 +08:00
陈精华
92ca92bee6 支持Java25环境 2025-12-03 18:46:23 +08:00
kailing
64c82a2406 !334 update docker/kkfileview-base/Dockerfile.
Merge pull request !334 from 云上小朱/N/A
2025-11-17 00:53:58 +00:00
云上小朱
e44ef813a1 update docker/kkfileview-base/Dockerfile.
在文件docker/kkfileview-base/Dockerfile中
我看到POM里写的Java21,但是Dockerfile写的JDK8. 是否应改为JDK21,我在本地测试,改为JDK21可正常打包代码并正常运行。

Signed-off-by: 云上小朱 <13077784+ysxz2025@user.noreply.gitee.com>
2025-11-14 01:04:31 +00:00
kl
9f3b45a4c7 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性 (#692)
* 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性

* 安全:强制用户配置可访问域名的白名单或者黑名单,提高安全性

* CI:修复 CI 问题

* CI:修复 CI 问题
2025-10-20 14:29:05 +08:00
kl
b1af0c7d72 优化:日志输出重构 (#689) 2025-10-13 11:14:54 +08:00
kl
421640221b Kl (#688)
* 升级: JDK1.8 升级到 JDK21 ,spring-boot 版本从 2.4.2 升级到 3.5.6

* 优化:启动日志新增 java version 输出信息
2025-10-11 20:14:39 +08:00
kl
f6c6e22b0d 升级: JDK1.8 升级到 JDK21 ,spring-boot 版本从 2.4.2 升级到 3.5.6 (#687) 2025-10-11 20:05:43 +08:00
kl
51653483b9 Kl json (#686)
* 新增:JSON 文件格式化预览功能

* 优化:优化 JSON 文件格式化预览效果
2025-10-11 17:54:17 +08:00
kl
a9787b0add 新增:JSON 文件格式化预览功能 (#685) 2025-10-11 11:52:49 +08:00
kl
6cdbf92fb0 优化:完善文件上传禁用功能的用户体验 (#684) 2025-10-11 10:41:34 +08:00
kl
fdb40680d3 安全:禁用文件上传接口 (#656)
- 从配置文件中移除file.upload.disable配置项
- 删除FileController中的文件上传相关代码
- 移除/fileUpload POST接口
- 删除文件上传校验逻辑
- 增强系统安全性,防止恶意文件上传

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-30 17:57:45 +08:00
陈精华
6746325bf2 update Github workflow 2025-04-14 16:10:29 +08:00
陈精华
05a8bff1e0 优化:调整单元测试 2025-03-31 15:56:50 +08:00
陈精华
874ff5b3f6 优化:解决部分场景下, 页面元素变化导致水印覆盖不全问题 2025-03-31 15:28:36 +08:00
陈精华
2230cfa52b update README.cn.md 2025-01-22 11:08:28 +08:00
陈精华
83c581364d update README.cn.md 2025-01-22 11:04:40 +08:00
陈精华
1d39360c60 4.4.0版本发布 2025-01-16 10:44:41 +08:00
陈精华
8c763599fe !312 update server/src/main/resources/web/main/record.ftl.
Merge pull request !312 from 高雄/N/A
2025-01-15 03:28:17 +00:00
高雄
9632f6070c update server/src/main/resources/web/main/record.ftl.
更新日志文档

Signed-off-by: 高雄 <admin@cxcp.com>
2025-01-14 01:23:13 +00:00
陈精华
cc63659650 Merge pull request #593 from zp96324511/patch-1
容易引起歧义的环境变量命名
2024-09-30 16:05:29 +08:00
阿鹏
177f389814 容易引起歧义的环境变量命名 2024-09-14 12:09:24 +08:00
陈精华
406e9ea6ee Merge pull request #584 from gitchenjh/master
docker基础镜像调整为kkfileview-base
2024-08-27 10:36:59 +08:00
陈精华
bb461cd74a docker基础镜像调整为kkfileview-base 2024-08-26 10:29:27 +08:00
陈精华
782509376c Merge pull request #581 from wayne-cheng/optimize-dockerfile
优化Dockerfile,支持真正的跨平台构建镜像
2024-08-22 14:41:17 +08:00
郑威
63dc58d088 :construction_worker:优化Dockerfile,支持真正的跨平台构建镜像 2024-08-21 11:12:38 +08:00
陈精华
77f5adb19f !299 修复压缩获取路径错误,图片合集路径错误,水印问题等BUG
Merge pull request !299 from 高雄/yashuoba
2024-07-02 02:20:08 +00:00
陈精华
7abfb67451 !289 修复前端解析xlsx 包含emf格式文件错误
Merge pull request !289 from 高雄/lockkkk
2024-06-25 02:05:43 +00:00
gaoxiongzaq
c8dc638c29 修复压缩获取路径错误,图片合集路径错误,水印问题等BUG 2024-05-27 14:30:31 +08:00
gaoxiongzaq
48ac926289 修复压缩获取路径错误,图片合集路径错误,水印问题等BUG 2024-05-27 14:21:11 +08:00
陈精华
0a4ae41b0c !296 新增PDF线程管理,超时管理,内存缓存管理,更新PDF解析组件版本
Merge pull request !296 from 高雄/pdfddd
2024-05-27 03:32:27 +00:00
gaoxiongzaq
bb0139bee6 新增PDF线程管理,超时管理,内存缓存管理,更新PDF解析组件版本 2024-05-20 10:12:11 +08:00
陈精华
7bf07cb64c !286 修复远程文件,文件名带有穿越的BUG
Merge pull request !286 from 高雄/chuanyue
2024-04-17 02:40:52 +00:00
陈精华
ab370e66a5 Merge pull request #554 from kekingcn/kl
重构解压缩逻辑,fix  #553
2024-04-17 10:32:19 +08:00
kl
421a2760d5 重构解压缩逻辑,fix #553 2024-04-16 20:04:50 +08:00
gaoxiongzaq
9150346926 修复前端解析xlsx 包含emf格式文件错误 2024-03-28 11:26:25 +08:00
gaoxiongzaq
b65a04857c 修复远程文件文件名带有穿越漏洞的BUG 2024-03-27 08:55:28 +08:00
109 changed files with 3647 additions and 7999 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: "Security Report / 安全漏洞报告"
url: "https://github.com/kekingcn/kkFileView/security/advisories/new"
about: "For sensitive security issues, please use private security report. / 涉及敏感安全问题请使用私密安全报告。"

View File

@@ -0,0 +1,76 @@
name: "Feature Request / 功能建议"
description: "Propose a new feature with clear use case and acceptance criteria. / 提交功能建议,请明确场景与验收标准。"
title: "[FEATURE] "
labels: ["type/feature", "priority/p2", "status/needs-info"]
body:
- type: markdown
attributes:
value: |
Thanks for your idea! / 感谢你的建议
Please provide concrete business scenarios and the expected behavior.
请尽量提供明确业务场景和期望行为便于评估优先级与实现方案
For urgent production issues, you can use our Knowledge Planet channel for faster processing:
https://wx.zsxq.com/group/48844125114258
如为线上紧急问题可通过知识星球渠道加速处理
https://wx.zsxq.com/group/48844125114258
- type: textarea
id: background
attributes:
label: "Background / 背景"
description: "What problem are you trying to solve? / 你要解决什么问题?"
placeholder: "Describe current pain points... / 描述当前痛点..."
validations:
required: true
- type: textarea
id: proposal
attributes:
label: "Proposal / 建议方案"
description: "What do you expect kkFileView to support? / 期望 kkFileView 支持什么?"
placeholder: "Describe expected feature behavior... / 描述期望功能行为..."
validations:
required: true
- type: textarea
id: use_case
attributes:
label: "Use Case / 使用场景"
description: "Provide 1-3 concrete scenarios. / 提供 1-3 个具体场景"
placeholder: |
Scenario 1:
Scenario 2:
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: "Alternatives / 备选方案"
description: "What alternatives have you considered? / 是否考虑过替代方案?"
placeholder: "Existing workaround or alternative... / 当前替代做法..."
validations:
required: false
- type: textarea
id: acceptance
attributes:
label: "Acceptance Criteria / 验收标准"
description: "How do we know this feature is done? / 如何判断该功能完成?"
placeholder: |
- [ ] Criterion 1
- [ ] Criterion 2
validations:
required: true
- type: checkboxes
id: checklist
attributes:
label: "Checklist / 提交前检查"
options:
- label: "I have searched existing issues and did not find a duplicate feature request. / 我已搜索现有 issue未发现重复功能建议"
required: true
- label: "I provided concrete use cases and expected behavior. / 我已提供具体使用场景和期望行为"
required: true

121
.github/ISSUE_TEMPLATE/issue-report.yml vendored Normal file
View File

@@ -0,0 +1,121 @@
name: "Issue Report / 问题反馈"
description: "Please provide complete required information to help us reproduce and follow up. / 请完整填写必填信息,便于复现与跟进。"
title: "[ISSUE] "
labels: ["status/needs-info"]
body:
- type: markdown
attributes:
value: |
Thanks for your report! / 感谢反馈
**Please fill in all required fields.**
**请完整填写所有必填项**
Incomplete issues may be closed and asked to resubmit.
信息不完整的问题可能会被关闭并要求重新提交
For urgent production issues, you can use our Knowledge Planet channel for faster processing:
https://wx.zsxq.com/group/48844125114258
如为线上紧急问题可通过知识星球渠道加速处理
https://wx.zsxq.com/group/48844125114258
- type: dropdown
id: issue_type
attributes:
label: "Issue Type / 问题类型"
description: "Select the closest type. / 请选择最接近的问题类型"
options:
- "Bug / 缺陷"
- "Performance / 性能问题"
- "Security / 安全问题"
- "Question / 使用咨询"
validations:
required: true
- type: input
id: kkfileview_version
attributes:
label: "kkFileView Version / kkFileView 版本"
placeholder: "e.g. 4.4.0"
validations:
required: true
- type: input
id: deploy_mode
attributes:
label: "Deployment Mode / 部署方式"
description: "jar / docker / k8s / source, etc. / jar / docker / k8s / 源码部署等"
placeholder: "e.g. docker"
validations:
required: true
- type: textarea
id: environment
attributes:
label: "Environment / 环境信息"
description: "OS, JDK, LibreOffice/OpenOffice, browser, reverse proxy, etc. / 操作系统、JDK、Office组件、浏览器、反向代理等"
placeholder: |
- OS:
- JDK:
- LibreOffice/OpenOffice:
- Browser:
- Proxy (Nginx/Ingress):
validations:
required: true
- type: textarea
id: reproduce_steps
attributes:
label: "Steps to Reproduce / 复现步骤"
description: "Provide clear, minimal, reproducible steps. / 提供清晰、最小可复现步骤"
placeholder: |
1) ...
2) ...
3) ...
validations:
required: true
- type: textarea
id: expected_result
attributes:
label: "Expected Result / 期望结果"
placeholder: "What should happen? / 期望实际应该出现什么结果?"
validations:
required: true
- type: textarea
id: actual_result
attributes:
label: "Actual Result / 实际结果"
placeholder: "What happened instead? / 实际发生了什么?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: "Logs & Screenshots / 日志与截图"
description: "Paste key logs/error stack and attach screenshots (mask sensitive data). / 粘贴关键日志或异常堆栈,并上传截图(请脱敏)"
render: shell
validations:
required: true
- type: textarea
id: sample_file
attributes:
label: "Sample File / 样例文件(可选)"
description: "If possible, provide a minimal sample file or reproducible URL (desensitized). / 如可提供,请附最小样例文件或可复现 URL脱敏"
validations:
required: false
- type: checkboxes
id: checklist
attributes:
label: "Checklist / 提交前检查"
options:
- label: "I have searched existing issues and did not find a duplicate. / 我已搜索现有 issue未发现重复问题"
required: true
- label: "I can reproduce this issue on the stated version/environment. / 我可在上述版本与环境复现该问题"
required: true
- label: "I have masked sensitive information in logs/screenshots. / 我已对日志与截图中的敏感信息做脱敏处理"
required: true

View File

@@ -0,0 +1,78 @@
name: Auto Close Old Issues (1y)
on:
schedule:
# Daily at 02:20 UTC
- cron: '20 2 * * *'
workflow_dispatch:
permissions:
issues: write
jobs:
close_old_issues:
runs-on: ubuntu-latest
steps:
- name: Close issues older than 1 year
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const now = new Date();
const cutoff = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
const closeComment = ` Issue 已超过 1 年未活跃为便于维护当前问题队列先做关闭处理\n\n如该问题在最新版本仍存在欢迎直接 **Reopen** Issue或新建 Issue 并关联本单并补充\n1. 版本与部署方式\n2. 最小复现步骤\n3. 关键日志/截图请脱敏\n\n我们会优先跟进`;
let page = 1;
let processed = 0;
while (true) {
const { data: issues } = await github.rest.issues.listForRepo({
owner,
repo,
state: 'open',
per_page: 100,
page,
sort: 'created',
direction: 'asc'
});
if (!issues.length) break;
for (const issue of issues) {
// skip pull requests
if (issue.pull_request) continue;
const createdAt = new Date(issue.created_at);
if (createdAt > cutoff) {
// list is sorted asc by created time; remaining items are newer
core.info('Reached issues newer than cutoff, stop scanning.');
return;
}
try {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: closeComment,
});
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: 'closed',
});
processed += 1;
core.info(`Closed #${issue.number}`);
} catch (e) {
core.warning(`Failed to close #${issue.number}: ${e.message}`);
}
}
page += 1;
}
core.info(`Done. Closed ${processed} old issues.`);

View File

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

View File

@@ -12,13 +12,41 @@ on:
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
- name: Set up JDK 8
uses: actions/setup-java@v2
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '8'
distribution: 'adopt'
cache: maven
java-version: '21'
distribution: 'temurin' # 使用 Eclipse Temurin (AdoptOpenJDK 的继任者)
cache: 'maven'
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Build with Maven
run: mvn -B package --file pom.xml
run: mvn -B package -Dmaven.test.skip=true --file pom.xml
- name: Upload Linux distribution package
if: success()
uses: actions/upload-artifact@v4
with:
name: kkfileview-linux
path: server/target/*.tar.gz
retention-days: 7
- name: Upload Windows distribution package
if: success()
uses: actions/upload-artifact@v4
with:
name: kkfileview-windows
path: server/target/*.zip
retention-days: 7

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

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

100
.github/workflows/pr-e2e-mvp.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: PR E2E MVP
on:
pull_request:
branches: [master]
workflow_dispatch:
permissions:
contents: read
jobs:
e2e-mvp:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests/e2e/package-lock.json
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install LibreOffice + archive tools
run: |
sudo apt-get update
sudo apt-get install -y libreoffice zip p7zip-full
- name: Setup Python deps for office fixtures
run: |
python -m pip install --upgrade pip
pip install -r tests/e2e/requirements.txt
- name: Build kkFileView
run: mvn -q -pl server -DskipTests package
- name: Install E2E deps
working-directory: tests/e2e
run: |
npm install
npx playwright install --with-deps chromium
- name: Start fixture server
run: |
cd tests/e2e/fixtures
python3 -m http.server 18080 > /tmp/fixture-server.log 2>&1 &
- name: Start kkFileView
run: |
JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1)
nohup env KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH" > /tmp/kkfileview.log 2>&1 &
- name: Wait for services
run: |
for i in {1..60}; do
curl -fsS http://127.0.0.1:18080/sample.txt >/dev/null && break
sleep 1
done
for i in {1..120}; do
curl -fsS http://127.0.0.1:8012/ >/dev/null && break
sleep 1
done
- name: Run E2E
working-directory: tests/e2e
env:
KK_BASE_URL: http://127.0.0.1:8012
FIXTURE_BASE_URL: http://127.0.0.1:18080
run: npm test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: tests/e2e/playwright-report
- name: Upload service logs
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-service-logs
path: |
/tmp/kkfileview.log
/tmp/fixture-server.log

View File

@@ -1,5 +1,4 @@
FROM keking/kkfileview-jdk:latest
MAINTAINER chenjh "842761733@qq.com"
FROM keking/kkfileview-base:4.4.0
ADD server/target/kkFileView-*.tar.gz /opt/
ENV KKFILEVIEW_BIN_FOLDER /opt/kkFileView-4.4.0-beta/bin
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-4.4.0-beta/config/application.properties","-jar","/opt/kkFileView-4.4.0-beta/bin/kkFileView-4.4.0-beta.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"]

View File

@@ -53,80 +53,81 @@
#### 1. 文本预览
支持所有类型的文本文档预览 由于文本文档类型过多无法全部枚举默认开启的类型如下 txt,html,htm,asp,jsp,xml,xbrl,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
文本预览效果如下
![文本预览效果如下](https://kkview.cn/img/preview/preview-text.png)
![文本预览效果如下](./doc/img/preview/preview-text.png)
#### 2. 图片预览
支持jpgjpegpnggif等图片预览翻转缩放镜像预览效果如下
![图片预览](https://kkview.cn/img/preview/preview-image.png)
![图片预览](./doc/img/preview/preview-image.png)
#### 3. word文档预览
支持docdocx文档预览word预览有两种模式一种是每页word转为图片预览另一种是整个word文档转成pdf再预览pdf两种模式的适用场景如下
* 图片预览word文件大前台加载整个pdf过慢
* pdf预览内网访问加载pdf快
图片预览模式预览效果如下
![word文档预览1](https://kkview.cn/img/preview/preview-doc-image.png)
![word文档预览1](./doc/img/preview/preview-doc-image.png)
pdf预览模式预览效果如下
![word文档预览2](https://kkview.cn/img/preview/preview-doc-pdf.png)
![word文档预览2](./doc/img/preview/preview-doc-pdf.png)
#### 4. ppt文档预览
支持pptpptx文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![ppt文档预览1](https://kkview.cn/img/preview/preview-ppt-image.png)
![ppt文档预览1](./doc/img/preview/preview-ppt-image.png)
pdf预览模式预览效果如下
![ppt文档预览2](https://kkview.cn/img/preview/preview-ppt-pdf.png)
![ppt文档预览2](./doc/img/preview/preview-ppt-pdf.png)
#### 5. pdf文档预览
支持pdf文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![pdf文档预览1](https://kkview.cn/img/preview/preview-pdf-image.png)
![pdf文档预览1](./doc/img/preview/preview-pdf-image.png)
pdf预览模式预览效果如下
![pdf文档预览2](https://kkview.cn/img/preview/preview-pdf-pdf.png)
![pdf文档预览2](./doc/img/preview/preview-pdf-pdf.png)
#### 6. excel文档预览
支持xlsxlsx文档预览预览效果如下
![excel文档预览](https://kkview.cn/img/preview/preview-xls.png)
![excel文档预览](./doc/img/preview/preview-xls.png)
#### 7. 压缩文件预览
支持zip,rar,jar,tar,gzip等压缩包预览效果如下
![压缩文件预览1](https://kkview.cn/img/preview/preview-zip.png)
![压缩文件预览1](./doc/img/preview/preview-zip.png)
可点击压缩包中的文件名直接预览文件预览效果如下
![压缩文件预览2](https://kkview.cn/img/preview/preview-zip-inner.png)
![压缩文件预览2](./doc/img/preview/preview-zip-inner.png)
#### 8. 多媒体文件预览
理论上支持所有的视频音频文件由于无法枚举所有文件格式默认开启的类型如下
mp3,wav,mp4,flv
视频预览效果如下
![多媒体文件预览1](https://kkview.cn/img/preview/preview-video.png)
![多媒体文件预览1](./doc/img/preview/preview-video.png)
音频预览效果如下
![多媒体文件预览2](https://kkview.cn/img/preview/preview-audio.png)
![多媒体文件预览2](./doc/img/preview/preview-audio.png)
#### 9. CAD文档预览
支持CAD dwg文档预览和word文档一样有两种预览模式
图片预览模式预览效果如下
![cad文档预览1](https://kkview.cn/img/preview/preview-cad-image.png)
![cad文档预览1](./doc/img/preview/preview-cad-image.png)
pdf预览模式预览效果如下
![cad文档预览2](https://kkview.cn/img/preview/preview-cad-pdf.png)
考虑说明篇幅原因就不贴其他格式文件的预览效果了感兴趣的可以参考下面的实例搭建下
![cad文档预览2](./doc/img/preview/preview-cad-pdf.png)
#### 10. Excel文件纯前端渲染效果
![Excel文件纯前端渲染效果](https://kkview.cn/img/preview/preview-xlsx-web.png)
![Excel文件纯前端渲染效果](./doc/img/preview/preview-xlsx-web.png)
#### 11. 流程图bpmn文件预览效果
![流程图bpmn文件预览效果](https://kkview.cn/img/preview/preview-bpmn.png)
![流程图bpmn文件预览效果](./doc/img/preview/preview-bpmn.png)
#### 12. 3D模型文件预览效果
![3D模型文件预览效果](https://kkview.cn/img/preview/preview-3ds.png)
![3D模型文件预览效果](./doc/img/preview/preview-3ds.png)
#### 13. dcm医疗数位影像文件预览效果
![dcm医疗数位影像文件预览效果](https://kkview.cn/img/preview/preview-dcm.png)
![dcm医疗数位影像文件预览效果](./doc/img/preview/preview-dcm.png)
#### 14. drawio流程图预览效果
![dcdrawio流程图预览效果](https://kkview.cn/img/preview/preview-drawio.png)
![drawio流程图预览效果](./doc/img/preview/preview-drawio.png)
考虑说明篇幅原因就不贴其他格式文件的预览效果了感兴趣的可以参考下面的实例搭建下
### 快速开始
> 项目使用技术
@@ -148,7 +149,56 @@ pdf预览模式预览效果如下
### 历史更新记录
#### > 2023年07月05v4.3 版本发布
#### > 2025年01月16v4.4.0 版本发布
### 新增功能
1. xlsx 新增支持打印功能
2. 配置文件新增启用 GZIP 压缩
3. CAD 格式新增支持转换成 SVG TIF 格式新增超时结束线程管理
4. 新增删除文件使用验证码校验
5. 新增 xbrl 格式预览支持
6. PDF 预览新增控制签名绘图插图控制搜索定位页码定义显示内容等功能
7. 新增 CSV 格式前端解析支持
8. 新增 ARM64 下的 Docker 镜像支持
9. 新增 Office 预览支持转换超时属性设置功能
10. 新增预览文件 host 黑名单机制
### 优化
1. 优化 OFD 移动端预览 页面不自适应
2. 更新 xlsx 前端解析组件加速解析速度
3. 升级 CAD 组件
4. office 功能调整支持批注转换页码限制生成水印等功能
5. 升级 markdown 组件
6. 升级 dcm 解析组件
7. 升级 PDF.JS 解析组件
8. 更换视频播放插件为 ckplayer
9. tif 解析更加智能化支持被修改的图片格式
10. 针对大小文本文件检测字符编码的正确率处理并发隐患
11. 重构下载文件的代码新增通用的文件服务器认证访问的设计
12. 更新 bootstrap 组件并精简掉不需要的文件
13. 更新 epub 版本优化 epub 显示效果
14. 解决定时清除缓存时对于多媒体类型文件只删除了磁盘缓存文件
15. 自动检测已安装 Office 组件增加 LibreOffice 7.5 & 7.6 版本默认路径
16. 修改 drawio 默认为预览模式
17. 新增 PDF 线程管理超时管理内存缓存管理更新 PDF 解析组件版本
18. 优化 Dockerfile支持真正的跨平台构建镜像
### 修复
1. 修复 forceUpdatedCache 属性设置但本地缓存文件不更新的问题
2. 修复 PDF 解密加密文件转换成功后后台报错的问题
3. 修复 BPMN 不支持跨域的问题
4. 修复压缩包二级反代特殊符号错误问题
5. 修复视频跨域配置导致视频无法预览的问题
6. 修复 TXT 文本类分页二次加载问题
7. 修复 Drawio 缺少 Base64 组件的问题
8. 修复 Markdown 被转义问题
9. 修复 EPUB 跨域报错问题
10. 修复 URL 特殊符号问题
11. 修复压缩包穿越漏洞
12. 修复压缩获取路径错误图片合集路径错误水印问题等 BUG
13. 修复前端解析 XLSX 包含 EMF 格式文件错误问题
#### > 2023年07月05日v4.3.0 版本发布
#### 新增功能:
1. 新增dcm等医疗数位影像预

174
SECURITY_CONFIG.md Normal file
View File

@@ -0,0 +1,174 @@
# kkFileView 安全配置指南
## 重要安全更新
4.4.0 之后版本开始kkFileView 增强了安全性默认拒绝所有未配置的外部文件预览请求以防止 SSRF服务器端请求伪造攻击
## 🔒 安全配置说明
### 1. 信任主机白名单配置推荐
`application.properties` 中配置允许预览的域名
```properties
# 方式1通过配置文件
trust.host = kkview.cn,yourdomain.com,cdn.example.com
# 方式2通过环境变量
KK_TRUST_HOST=kkview.cn,yourdomain.com,cdn.example.com
```
**示例场景**
- 只允许预览来自 `oss.aliyuncs.com` `cdn.example.com` 的文件
```properties
trust.host = oss.aliyuncs.com,cdn.example.com
```
### 2. 允许所有主机不推荐仅测试环境
```properties
trust.host = *
```
**警告**此配置会允许访问任意外部地址存在安全风险仅应在测试环境使用
### 3. 黑名单配置高级
禁止特定域名或内网地址
```properties
# 禁止访问内网地址强烈推荐
not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*,169.254.*
# 禁止特定恶意域名
not.trust.host = malicious-site.com,spam-domain.net
```
**优先级**黑名单 > 白名单
### 4. Docker 环境配置
```bash
docker run -d \
-e KK_TRUST_HOST=yourdomain.com,cdn.example.com \
-e KK_NOT_TRUST_HOST=localhost,127.0.0.1 \
-p 8012:8012 \
keking/kkfileview:4.4.0
```
## 🛡 安全最佳实践
### 推荐配置
```properties
# 1. 明确配置信任主机白名单
trust.host = your-cdn.com,your-storage.com
# 2. 配置黑名单防止内网访问
not.trust.host = localhost,127.0.0.1,192.168.*,10.*,172.16.*
# 3. 禁用文件上传生产环境
file.upload.disable = true
# 4. 配置基础URL使用反向代理时
base.url = https://preview.yourdomain.com
```
### 不推荐配置
```properties
# 危险允许所有主机访问
trust.host = *
# 危险启用文件上传生产环境
file.upload.disable = false
```
## 🔍 配置验证
### 测试白名单是否生效
1. 配置白名单
```properties
trust.host = kkview.cn
```
2. 尝试预览白名单内的文件
```
http://localhost:8012/onlinePreview?url=https://kkview.cn/test.pdf
应该可以正常预览
```
3. 尝试预览白名单外的文件
```
http://localhost:8012/onlinePreview?url=https://other-domain.com/test.pdf
应该被拒绝显示"不信任的文件源"
```
### 测试黑名单是否生效
1. 配置黑名单
```properties
not.trust.host = localhost,127.0.0.1
```
2. 尝试访问本地文件
```
http://localhost:8012/getCorsFile?urlPath=http://127.0.0.1:8080/admin
应该被拒绝
```
## 📋 常见问题
### Q1: 升级后无法预览文件了
**原因**新版本默认拒绝未配置的主机
**解决**在配置文件中添加信任主机列表
```properties
trust.host = your-file-server.com
```
### Q2: 如何临时恢复旧版本行为
**不推荐**但如果确实需要
```properties
trust.host = *
```
### Q3: 配置了白名单但还是无法访问
检查以下几点
1. 域名是否完全匹配区分大小写
2. 是否配置了黑名单黑名单优先级更高
3. 查看日志中的 WARNING 信息
4. 确认环境变量是否正确设置
### Q4: 如何允许子域名
已支持通配符域名匹配可使用 `*.example.com`
```properties
trust.host = *.example.com
```
说明
- `*.example.com` 会匹配 `cdn.example.com``api.internal.example.com`但不匹配根域 `example.com`
- 对于 IP 风格通配 `192.168.*``10.*`仅匹配字面量 IPv4 地址不匹配域名
## 🚨 安全事件响应
如果发现可疑的预览请求
1. 检查日志文件搜索 "拒绝访问主机" 关键字
2. 确认 `trust.host` 配置是否合理
3. 检查是否有异常的网络请求
4. 如发现攻击行为及时更新黑名单配置
## 📞 获取帮助
- GitHub Issues: https://github.com/kekingcn/kkFileView/issues
- Gitee Issues: https://gitee.com/kekingcn/file-online-preview/issues
---
**安全提示**定期检查和更新信任主机列表遵循最小权限原则

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,28 @@
FROM ubuntu:24.04
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources &&\
sed -i 's@//security.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources &&\
sed -i 's@//ports.ubuntu.com@//mirrors.aliyun.com@g' /etc/apt/sources.list.d/ubuntu.sources &&\
apt-get update &&\
export DEBIAN_FRONTEND=noninteractive &&\
apt-get install -y --no-install-recommends openjdk-21-jre tzdata locales xfonts-utils fontconfig libreoffice-nogui &&\
echo 'Asia/Shanghai' > /etc/timezone &&\
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&\
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 &&\
locale-gen zh_CN.UTF-8 &&\
apt-get install -y --no-install-recommends ttf-mscorefonts-installer &&\
apt-get install -y --no-install-recommends ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy &&\
apt-get autoremove -y &&\
apt-get clean &&\
rm -rf /var/lib/apt/lists/*
# 内置一些常用的中文字体,避免普遍性乱码
ADD fonts/* /usr/share/fonts/chinese/
RUN cd /usr/share/fonts/chinese &&\
# 安装字体
mkfontscale &&\
mkfontdir &&\
fc-cache -fv
ENV LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8

View File

@@ -0,0 +1,50 @@
# 构建说明
由于 kkfileview 的基础运行环境很少变动且制作耗时较久 kkfileview 本身代码开发会频繁改动因此把制作其 Docker 镜像的步骤拆分为两次
首先制作 kkfileview 的基础镜像kkfileview-base
然后使用 kkfileview-base 作为基础镜像进行构建加快 kkfileview docker 镜像构建与发布
执行如下命令即可构建基础镜像
> 这里镜像 tag 4.4.0 为例本项目所维护的 Dockerfile 文件考虑了跨平台兼容性 如果你需要用到 arm64 架构镜像, 则在arm64 架构机器上同样执行下面的构建命令即可
```shell
docker build --tag keking/kkfileview-base:4.4.0 .
```
## 跨平台构建
`docker buildx` 支持在一台机器上构建出多种平台架构的镜像推荐使用该能力进行跨平台的镜像构建
例如执行 `docker buildx build` 命令时加上 `--platform=linux/arm64` 参数即可构建出 arm64 架构镜像这极大方便了那些没有arm64 架构机器却想构建 arm64 架构镜像的用户
> 当前本项目仅支持构建 linux/amd64 linux/arm64 两种平台架构的镜像
> buildx builder driver 可以使用默认的 `docker` 类型, 若使用 `docker-container` 类型可以支持并行构建多种架构, 本文不再赘述, 有兴趣可以自行了解参考 [Docker Buildx | Docker Documentation](https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images)
**前提要求**
以当前机器为 amd64 (x86_64)架构为例需要开启 docker buildx 特性以及开启 Linux QEMU 用户模式
> 使用 WSL2 Windows 用户如果安装了最新的 DockerDesktop, 则这些前提要求已满足, 无需额外下述设置
1. 安装 docker buildx 客户端插件
> docker 版本要求 >=19.03
若已安装, 则跳过详情参考 https://github.com/docker/buildx
2. 开启 QEMU 的用户模式功能, 并安装其它平台的模拟器:
> Linux 内核要求 >=4.8
使用 `tonistiigi/binfmt` 镜像可快速开启并安装模拟器执行下面命令:
```shell
docker run --privileged --rm tonistiigi/binfmt --install all
```
现在就可以愉快地开始构建了构建命令示例:
```shell
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
```

View File

@@ -0,0 +1,53 @@
# Build Instructions
Since the base runtime environment for kkfileview rarely changes and takes a long time to build, while the kkfileview code itself is frequently updated, the process of building its Docker image is split into two steps:
First, create the base image for kkfileview (kkfileview-base).
Then, use kkfileview-base as the base image to build and speed up the kkfileview Docker image build and release process.
To build the base image, run the following command:
> In this example, the image tag is 4.4.0. The Dockerfile maintained in this project considers cross-platform compatibility. If you need an arm64 architecture image, run the same build command on an arm64 architecture machine.
```shell
docker build --tag keking/kkfileview-base:4.4.0 .
```
## Cross-Platform Build
`docker buildx` supports building images for multiple platform architectures on a single machine. It is recommended to use this capability for cross-platform image builds.
For example, adding the `--platform=linux/arm64` parameter when executing the `docker buildx build` command will allow you to build an arm64 architecture image. This is particularly convenient for users who want to build arm64 images but don't have an arm64 machine.
> Currently, this project only supports building images for the linux/amd64 and linux/arm64 architectures.
> The buildx builder driver can use the default `docker` type, but if you use the `docker-container` type, you can build multiple architectures in parallel. This README will not cover that in detail, you can learn more on your own. Refer to [Docker Buildx | Docker Documentation](https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images)
**Prerequisites**
Assuming the current machine is amd64 (x86_64) architecture, you'll need to enable the docker buildx feature and enable Linux QEMU user mode:
> Windows users with WSL2 who have installed a recent version of Docker Desktop will already meet these prerequisites, so no additional setup is required.
1. Install the docker buildx client plugin:
> Docker version >=19.03 is required.
If it's already installed, you can skip this step. For more details, refer to https://github.com/docker/buildx.
2. Enable QEMU user mode and install emulators for other platforms:
> Linux kernel version >=4.8 is required.
You can quickly enable and install emulators using the tonistiigi/binfmt image by running the following command:
```shell
docker run --privileged --rm tonistiigi/binfmt --install all
```
Now you can enjoy the building. Heres an example build command:
```shell
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
```

View File

@@ -1,38 +0,0 @@
FROM ubuntu:20.04
MAINTAINER chenjh "842761733@qq.com"
# 内置一些常用的中文字体,避免普遍性乱码
COPY fonts/* /usr/share/fonts/chinese/
RUN apt-get clean && apt-get update &&\
sed -i 's/http:\/\/archive.ubuntu.com/https:\/\/mirrors.aliyun.com/g' /etc/apt/sources.list &&\
sed -i 's/# deb/deb/g' /etc/apt/sources.list &&\
apt-get install -y --reinstall ca-certificates &&\
apt-get clean && apt-get update &&\
apt-get install -y locales language-pack-zh-hans &&\
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 && locale-gen zh_CN.UTF-8 &&\
export DEBIAN_FRONTEND=noninteractive &&\
apt-get install -y tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&\
apt-get install -y fontconfig ttf-mscorefonts-installer ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy &&\
apt-get install -y wget &&\
cd /tmp &&\
wget https://kkview.cn/resource/server-jre-8u251-linux-x64.tar.gz &&\
tar -zxf /tmp/server-jre-8u251-linux-x64.tar.gz && mv /tmp/jdk1.8.0_251 /usr/local/ &&\
# 安装 libreoffice
apt-get install -y libxrender1 libxinerama1 libxt6 libxext-dev libfreetype6-dev libcairo2 libcups2 libx11-xcb1 libnss3 &&\
wget https://downloadarchive.documentfoundation.org/libreoffice/old/7.5.3.2/deb/x86_64/LibreOffice_7.5.3.2_Linux_x86-64_deb.tar.gz -cO libreoffice_deb.tar.gz &&\
tar -zxf /tmp/libreoffice_deb.tar.gz && cd /tmp/LibreOffice_7.5.3.2_Linux_x86-64_deb/DEBS &&\
dpkg -i *.deb &&\
# 清理临时文件
rm -rf /tmp/* && rm -rf /var/lib/apt/lists/* &&\
cd /usr/share/fonts/chinese &&\
mkfontscale &&\
mkfontdir &&\
fc-cache -fv
ENV JAVA_HOME /usr/local/jdk1.8.0_251
ENV CLASSPATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV PATH $PATH:$JAVA_HOME/bin
ENV LANG zh_CN.UTF-8
ENV LC_ALL zh_CN.UTF-8
CMD ["/bin/bash"]

View File

@@ -1,77 +0,0 @@
FROM arm64v8/ubuntu:20.04
MAINTAINER chenjh "842761733@qq.com"
# 内置一些常用的中文字体,避免普遍性乱码
COPY fonts/* /usr/share/fonts/chinese/
RUN apt-get clean && apt-get update &&\
sed -i 's/http:\/\/archive.ubuntu.com/https:\/\/mirrors.aliyun.com/g' /etc/apt/sources.list &&\
sed -i 's/# deb/deb/g' /etc/apt/sources.list &&\
apt-get install -y --reinstall ca-certificates &&\
apt-get clean && apt-get update &&\
apt-get install -y locales language-pack-zh-hans &&\
localedef -i zh_CN -c -f UTF-8 -A /usr/share/locale/locale.alias zh_CN.UTF-8 && locale-gen zh_CN.UTF-8 &&\
export DEBIAN_FRONTEND=noninteractive &&\
apt-get install -y tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime &&\
apt-get install -y fontconfig ttf-mscorefonts-installer ttf-wqy-microhei ttf-wqy-zenhei xfonts-wqy &&\
apt-get install -y wget
# 安装 arm64-jre8
RUN apt-get install -y openjdk-8-jre
# 编译 libreoffice
RUN apt-get install -y git build-essential zip ccache junit4 libkrb5-dev nasm graphviz python3 python3-dev qtbase5-dev libkf5coreaddons-dev libkf5i18n-dev libkf5config-dev libkf5windowsystem-dev libkf5kio-dev autoconf libcups2-dev libfontconfig1-dev gperf default-jdk doxygen libxslt1-dev xsltproc libxml2-utils libxrandr-dev libx11-dev bison flex libgtk-3-dev libgstreamer-plugins-base1.0-dev libgstreamer1.0-dev ant ant-optional libnss3-dev libavahi-client-dev libxt-dev &&\
# 安装 ccache重复编译时加快速度
apt-get install ccache &&\
ccache -M 10G &&\
# clone主代码
mkdir /opt/libreoffice
WORKDIR /opt/libreoffice
RUN git clone --depth=1 --branch libreoffice-7-5 git://go.suokunlong.cn/lo/core ./libreoffice-7-5
# 配置&抓取子模块
WORKDIR /opt/libreoffice/libreoffice-7-5
RUN git submodule init &&\
git config --unset-all submodule.dictionaries.active &&\
git config --unset-all submodule.dictionaries.url &&\
git config --unset-all submodule.helpcontent2.active &&\
git config --unset-all submodule.helpcontent2.url &&\
git submodule update --progress --depth=1 &&\
# 下载第三方依赖
mkdir -p /opt/libreoffice/ext &&\
wget --recursive --no-parent --no-check-certificate -P /opt/libreoffice/ext https://go.suokunlong.cn:88/dl/libreoffice/external_tarballs/
RUN mv /opt/libreoffice/ext/go.suokunlong.cn:88/dl/libreoffice/external_tarballs/* /opt/libreoffice/ext
# 配置编译选项
RUN cat << EOF > autogen.input \
&& echo "--without-help" >> autogen.input \
&& echo "--without-helppack-integration" >> autogen.input \
&& echo "--with-lang=zh-CN zh-TW" >> autogen.input \
&& echo "--disable-online-update" >> autogen.input \
&& echo "--disable-breakpad" >> autogen.input \
&& echo "--disable-odk" >> autogen.input \
&& echo "--without-doxygen" >> autogen.input \
&& echo "--with-external-tar=/opt/libreoffice/ext" >> autogen.input \
&& echo "--without-java" >> autogen.input \
&& echo "--enable-firebird-sdbc" >> autogen.input \
&& echo "--without-system-firebird" >> autogen.input \
&& echo "--enable-python=internal" >> autogen.input
# 预编译
RUN ./autogen.sh
# 因为libreoffice的安全策略不允许root用户执行编译操作可以改Makefile文件解决所以新建用户
RUN useradd libreoffice
# 切换用户
RUN su libreoffice
# 在普通用户下编译
RUN make || true
# !!!编译40分钟左右会报错此时需要执行以下操作重新编译
RUN cp ./workdir/UnpackedTarball/python3/build/lib.linux-aarch64-3.8/_sysconfigdata__linux_aarch64-linux-gnu.py ./workdir/UnpackedTarball/python3/build/lib.linux-aarch64-3.8/_sysconfigdata__linux_aarch64-unknown-linux-gnu.py
# 重新编译
RUN make &&\
make install
RUN ln -s /usr/local/lib/libreoffice/program/soffice /usr/bin/libreoffice
# 清理临时文件
RUN rm -rf /tmp/* && rm -rf /var/lib/apt/lists/* &&\
cd /usr/share/fonts/chinese &&\
mkfontscale &&\
mkfontdir &&\
fc-cache -fv
ENV LANG zh_CN.UTF-8
ENV LC_ALL zh_CN.UTF-8
CMD ["/bin/bash"]

View File

@@ -1,5 +0,0 @@
# 执行如下命令构建基础镜像加快kkfileview docker镜像构建与发布
docker build --tag keking/kkfileview-jdk:4.3.0 .
# arm64架构执行如下命令
docker build -f Dockerfile_arm64 --tag keking/kkfileview-jdk:4.3.0 .

11
pom.xml
View File

@@ -6,23 +6,23 @@
<groupId>cn.keking</groupId>
<artifactId>kkFileView-parent</artifactId>
<version>4.4.0-beta</version>
<version>4.4.0</version>
<properties>
<java.version>1.8</java.version>
<java.version>21</java.version>
<jodconverter.version>4.4.6</jodconverter.version>
<spring.boot.version>2.4.2</spring.boot.version>
<spring.boot.version>3.5.6</spring.boot.version>
<poi.version>5.2.2</poi.version>
<xdocreport.version>1.0.6</xdocreport.version>
<xstream.version>1.4.20</xstream.version>
<junrar.version>7.5.5</junrar.version>
<redisson.version>3.2.0</redisson.version>
<redisson.version>3.22.0</redisson.version>
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
<jchardet.version>1.0</jchardet.version>
<antlr.version>2.7.7</antlr.version>
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
<rocksdb.version>5.17.2</rocksdb.version>
<pdfbox.version>2.0.29</pdfbox.version>
<pdfbox.version>3.0.2</pdfbox.version>
<jai-imageio.version>1.4.0</jai-imageio.version>
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
<galimatias.version>0.2.1</galimatias.version>
@@ -43,6 +43,7 @@
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>kkFileView-parent</artifactId>
<groupId>cn.keking</groupId>
<version>4.4.0-beta</version>
<version>4.4.0</version>
</parent>
<artifactId>kkFileView</artifactId>
@@ -44,21 +44,16 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- web end -->
<!-- poi start -->
@@ -100,9 +95,8 @@
</dependency>
<!-- poi start -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpcomponents.version}</version>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<!-- rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
@@ -182,6 +176,12 @@
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.pdfbox</groupId>
@@ -327,6 +327,14 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>

View File

@@ -8,7 +8,7 @@ install_redhat() {
yum install -y libSM.x86_64 libXrender.x86_64 libXext.x86_64
yum groupinstall -y "X Window System"
yum localinstall -y *.rpm
echo 'install finshed...'
echo 'install finished...'
else
echo 'download package error...'
fi
@@ -20,7 +20,7 @@ install_ubuntu() {
if [ $? -eq 0 ];then
apt-get install -y libxinerama1 libcairo2 libcups2 libx11-xcb1
dpkg -i *.deb
echo 'install finshed...'
echo 'install finished...'
else
echo 'download package error...'
fi

View File

@@ -7,4 +7,4 @@ echo Please check log file in ../log/kkFileView.log for more information
echo You can get help in our official home site: https://kkview.cn
echo If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ
echo If this project is helpful to you, please star it on https://gitee.com/kekingcn/file-online-preview/stargazers
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0-beta.jar -> ..\log\kkFileView.log
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0.jar -> ..\log\kkFileView.log

View File

@@ -9,7 +9,7 @@
# Description: v1.1修改进程启动机制为pid形式
#############################
#
DIR_HOME=("/opt/openoffice.org3" "/opt/libreoffice" "/opt/libreoffice6.1" "/opt/libreoffice7.0" "/opt/libreoffice7.1" "/opt/libreoffice7.2" "/opt/libreoffice7.3" "/opt/libreoffice7.4" "/opt/libreoffice7.5" "/opt/libreoffice7.6" "/opt/openoffice4" "/usr/lib/openoffice" "/usr/lib/libreoffice")
DIR_HOME=("/opt/openoffice.org3" "/opt/libreoffice" "/opt/libreoffice6.1" "/opt/libreoffice7.0" "/opt/libreoffice7.1" "/opt/libreoffice7.2" "/opt/libreoffice7.3" "/opt/libreoffice7.4" "/opt/libreoffice7.5" "/opt/libreoffice7.6" "/opt/libreoffice24.2" "/opt/libreoffice24.8" "/opt/libreoffice25.2" "/opt/openoffice4" "/usr/lib/openoffice" "/usr/lib/libreoffice")
FLAG=
OFFICE_HOME=
KKFILEVIEW_BIN_FOLDER=$(cd "$(dirname "$0")" || exit 1 ;pwd)
@@ -29,7 +29,7 @@ if [ -s "${PID_FILE}" ]; then
else
cd "$KKFILEVIEW_BIN_FOLDER" || exit 1
echo "Using KKFILEVIEW_BIN_FOLDER $KKFILEVIEW_BIN_FOLDER"
grep 'office\.home' ../config/application.properties | grep '!^#'
grep 'office\.home' ../config/application.properties | grep -v '^#' | grep -v 'default'
if [ $? -eq 0 ]; then
echo "Using customized office.home"
else
@@ -51,7 +51,7 @@ else
## 启动kkFileView
echo "Starting kkFileView..."
nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -jar kkFileView-4.4.0-beta.jar > ../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"

View File

@@ -3,7 +3,7 @@ server.port = ${KK_SERVER_PORT:8012}
server.servlet.context-path= ${KK_CONTEXT_PATH:/}
server.servlet.encoding.charset = utf-8
#启用GZIP压缩功能
server.compression.enable= true
server.compression.enabled = true
#允许压缩的响应缓冲区最小字节数默认2048
server.compression.min-response-size = 2048
#压缩格式
@@ -22,6 +22,14 @@ spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.request-context-attribute = request
spring.freemarker.suffix = .ftl
# Spring Boot Actuator 健康检查配置
# 开启健康检查端点
management.endpoints.web.exposure.include=health,info,metrics
# 显示详细的健康检查信息生产环境建议设置为when-authorized
management.endpoint.health.show-details=always
# 启用健康检查组件
management.health.defaults.enabled=true
# office设置
#openoffice或LibreOffice home路径
@@ -78,11 +86,25 @@ cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
#提供预览服务的地址默认从请求url读如果使用nginx等反向代理需要手动设置
#base.url = https://file.keking.cn
base.url = ${KK_BASE_URL:default}
#信任站点多个用','隔开设置了之后会限制只能预览来自信任站点列表的文件默认不限制
#trust.host = kkview.cn
# ========== 安全配置重要==========
# 信任站点白名单配置多个用','隔开
# 安全提示为防止SSRF攻击强烈建议配置信任主机白名单
# 如果不配置系统将默认拒绝所有外部文件预览请求
#
# 配置示例
# trust.host = kkview.cn,yourdomain.com,cdn.example.com
#
# 如果需要允许所有域名不推荐仅用于测试环境请设置为
# trust.host = *
#
# 当前配置
trust.host = ${KK_TRUST_HOST:default}
#不信任站点多个用','隔开设置了之后会限制来自不信任站点列表的文件默认不限制
#not.trust.host = kkview.cn
# 不信任站点黑名单配置多个用','隔开
# 黑名单优先级高于白名单设置后将禁止预览来自这些站点的文件
# 建议配置禁止访问内网地址和本地地址
# not.trust.host = localhost,127.0.0.1,0.0.0.0,192.168.*,10.*,172.16.*
not.trust.host= ${KK_NOT_TRUST_HOST:default}
#文本类型默认如下可自定义添加
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}
@@ -110,6 +132,14 @@ convertMedias = ${KK_CONVERTMEDIAS:avi,mov,wmv,mkv,3gp,rm}
#PDF预览模块设置
#配置PDF文件生成图片的像素大小dpi 越高图片质量越清晰同时也会消耗更多的计算资源
pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
#PDF转换超时设置 (低于50页) 温馨提示这里数字仅供参考
pdf.timeout =${KK_pdf_TIMEOUT:90}
#PDF转换超时设置 (高于50小于200页)
pdf.timeout80 =${KK_PDF_TIMEOUT80:180}
#PDF转换超时设置 (大于200页)
pdf.timeout200 =${KK_PDF_TIMEOUT200:300}
#PDF转换线程设置
pdf.thread =${KK_PDF_THREAD:5}
#是否禁止演示模式
pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
#是否禁止打开文件
@@ -151,10 +181,9 @@ watermark.height = ${WATERMARK_HEIGHT:80}
#水印倾斜度数要求设置在大于等于0小于90
watermark.angle = ${WATERMARK_ANGLE:10}
#首页功能设置
#是否禁用首页文件上传
file.upload.disable = ${KK_FILE_UPLOAD_ENABLED:false}
file.upload.disable = ${KK_FILE_UPLOAD_DISABLE:true}
# 备案信息默认为空
beian = ${KK_BEIAN:default}
#禁止上传类型

View File

@@ -67,6 +67,10 @@ public class ConfigConstants {
private static String homePagination;
private static String homePageSize;
private static String homeSearch;
private static int pdfTimeout;
private static int pdfTimeout80;
private static int pdfTimeout200;
private static int pdfThread;
public static final String DEFAULT_CACHE_ENABLED = "true";
public static final String DEFAULT_TXT_TYPE = "txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd,xbrl";
@@ -107,6 +111,10 @@ public class ConfigConstants {
public static final String DEFAULT_HOME_PAGINATION = "true";
public static final String DEFAULT_HOME_PAGSIZE = "15";
public static final String DEFAULT_HOME_SEARCH = "true";
public static final String DEFAULT_PDF_TIMEOUT = "90";
public static final String DEFAULT_PDF_TIMEOUT80 = "180";
public static final String DEFAULT_PDF_TIMEOUT200 = "300";
public static final String DEFAULT_PDF_THREAD = "5";
public static Boolean isCacheEnabled() {
return cacheEnabled;
@@ -300,7 +308,8 @@ public class ConfigConstants {
if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) {
return new CopyOnWriteArraySet<>();
} else {
String[] trustHostArray = trustHost.toLowerCase().split(",");
// 去除空格并转小写
String[] trustHostArray = trustHost.toLowerCase().replaceAll("\\s+", "").split(",");
return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray));
}
}
@@ -418,7 +427,7 @@ public class ConfigConstants {
return fileUploadDisable;
}
@Value("${file.upload.disable:false}")
@Value("${file.upload.disable:true}")
public void setFileUploadDisable(Boolean fileUploadDisable) {
setFileUploadDisableValue(fileUploadDisable);
}
@@ -580,6 +589,65 @@ public class ConfigConstants {
ConfigConstants.cadThread = cadThread;
}
/**
* 以下为pdf转换模块设置
*/
public static int getPdfTimeout() {
return pdfTimeout;
}
@Value("${pdf.timeout:90}")
public void setPdfTimeout(int pdfTimeout) {
setPdfTimeoutValue(pdfTimeout);
}
public static void setPdfTimeoutValue(int pdfTimeout) {
ConfigConstants.pdfTimeout = pdfTimeout;
}
public static int getPdfTimeout80() {
return pdfTimeout80;
}
@Value("${pdf.timeout80:180}")
public void setPdfTimeout80(int pdfTimeout80) {
setPdfTimeout80Value(pdfTimeout80);
}
public static void setPdfTimeout80Value(int pdfTimeout80) {
ConfigConstants.pdfTimeout80 = pdfTimeout80;
}
public static int getPdfTimeout200() {
return pdfTimeout200;
}
@Value("${pdf.timeout200:300}")
public void setPdfTimeout200(int pdfTimeout200) {
setPdfTimeout200Value(pdfTimeout200);
}
public static void setPdfTimeout200Value(int pdfTimeout200) {
ConfigConstants.pdfTimeout200 = pdfTimeout200;
}
public static int getPdfThread() {
return pdfThread;
}
@Value("${pdf.thread:5}")
public void setPdfThread(int pdfThread) {
setPdfThreadValue(pdfThread);
}
public static void setPdfThreadValue(int pdfThread) {
ConfigConstants.pdfThread = pdfThread;
}
/**
* 以下为OFFICE转换模块设置
*/

View File

@@ -5,7 +5,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import jakarta.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
@@ -78,6 +78,10 @@ public class ConfigRefreshComponent {
String homePagination;
String homePageSize;
String homeSearch;
int pdfTimeout;
int pdfTimeout80;
int pdfTimeout200;
int pdfThread;
while (true) {
FileReader fileReader = new FileReader(configFilePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
@@ -126,6 +130,10 @@ public class ConfigRefreshComponent {
homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
cadThread = Integer.parseInt(properties.getProperty("cad.thread", ConfigConstants.DEFAULT_CAD_THREAD));
pdfTimeout = Integer.parseInt(properties.getProperty("pdf.timeout", ConfigConstants.DEFAULT_PDF_TIMEOUT));
pdfTimeout80 = Integer.parseInt(properties.getProperty("pdf.timeout80", ConfigConstants.DEFAULT_PDF_TIMEOUT80));
pdfTimeout200 = Integer.parseInt(properties.getProperty("pdf.timeout200", ConfigConstants.DEFAULT_PDF_TIMEOUT200));
pdfThread = Integer.parseInt(properties.getProperty("pdf.thread", ConfigConstants.DEFAULT_PDF_THREAD));
prohibitArray = prohibit.split(",");
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
@@ -169,6 +177,10 @@ public class ConfigRefreshComponent {
ConfigConstants.setHomePaginationValue(homePagination);
ConfigConstants.setHomePageSizeValue(homePageSize);
ConfigConstants.setHomeSearchValue(homeSearch);
ConfigConstants.setPdfTimeoutValue(pdfTimeout);
ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
ConfigConstants.setPdfThreadValue(pdfThread);
setWatermarkConfig(properties);
bufferedReader.close();
fileReader.close();

View File

@@ -50,26 +50,21 @@ public class RedissonConfig {
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
.setConnectionPoolSize(connectionPoolSize)
.setDatabase(database)
.setDnsMonitoring(dnsMonitoring)
.setDnsMonitoringInterval(dnsMonitoringInterval)
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
.setSubscriptionsPerConnection(subscriptionsPerConnection)
.setClientName(clientName)
.setFailedAttempts(failedAttempts)
.setRetryAttempts(retryAttempts)
.setRetryInterval(retryInterval)
.setReconnectionTimeout(reconnectionTimeout)
.setTimeout(timeout)
.setConnectTimeout(connectTimeout)
.setIdleConnectionTimeout(idleConnectionTimeout)
.setPingTimeout(pingTimeout)
.setPassword(StringUtils.trimToNull(password));
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
config.setCodec(codec);
config.setThreads(thread);
config.setEventLoopGroup(new NioEventLoopGroup());
config.setUseLinuxNativeEpoll(false);
return config;
}

View File

@@ -22,6 +22,7 @@ public enum FileType {
MEDIACONVERT("mediaFilePreviewImpl"),
MARKDOWN("markdownFilePreviewImpl"),
XML("xmlFilePreviewImpl"),
JSON("jsonFilePreviewImpl"),
CAD("cadFilePreviewImpl"),
TIFF("tiffFilePreviewImpl"),
OFD("ofdFilePreviewImpl"),
@@ -44,12 +45,13 @@ public enum FileType {
private static final String[] DCM_TYPES = {"dcm"};
private static final String[] DRAWIO_TYPES = {"drawio"};
private static final String[] XML_TYPES = {"xml","xbrl"};
private static final String[] JSON_TYPES = {"json"};
private static final String[] TIFF_TYPES = {"tif", "tiff"};
private static final String[] OFD_TYPES = {"ofd"};
private static final String[] SVG_TYPES = {"svg"};
private static final String[] CAD_TYPES = {"dwg", "dxf", "dwf", "iges", "igs", "dwt", "dng", "ifc", "dwfx", "stl", "cf2", "plt"};
private static final String[] SSIM_TEXT_TYPES = ConfigConstants.getSimText();
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "json", "h", "cpp", "cs", "aspx", "jsp", "sql"};
private static final String[] CODES = {"java", "c", "php", "go", "python", "py", "js", "html", "ftl", "css", "lua", "sh", "rb", "yaml", "yml", "h", "cpp", "cs", "aspx", "jsp", "sql"};
private static final String[] MEDIA_TYPES = ConfigConstants.getMedia();
public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias();
private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
@@ -109,6 +111,9 @@ public enum FileType {
for (String xml : XML_TYPES) {
FILE_TYPE_MAPPER.put(xml, FileType.XML);
}
for (String json : JSON_TYPES) {
FILE_TYPE_MAPPER.put(json, FileType.JSON);
}
FILE_TYPE_MAPPER.put("md", FileType.MARKDOWN);
FILE_TYPE_MAPPER.put("pdf", FileType.PDF);
FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN);

View File

@@ -12,12 +12,19 @@ import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.io.*;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
@@ -37,73 +44,70 @@ public class CompressFileReader {
public String unRar(String filePath, String filePassword, String fileName, FileAttribute fileAttribute) throws Exception {
List<String> imgUrls = new ArrayList<>();
String baseUrl = BaseUrlFilter.getBaseUrl();
String packagePath = "_"; //防止文件名重复 压缩包统一生成文件添加_符号
String packagePath = "_";
String folderName = filePath.replace(fileDir, ""); //修复压缩包 多重目录获取路径错误
if (fileAttribute.isCompressFile()) { //压缩包文件 直接赋予路径 不予下载
folderName = "_decompression" + folderName; //重新修改多重压缩包 生成文件路径
if (fileAttribute.isCompressFile()) {
folderName = "_decompression" + folderName;
}
RandomAccessFile randomAccessFile = null;
IInArchive inArchive = null;
try {
randomAccessFile = new RandomAccessFile(filePath, "r");
inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));
Path folderPath = Paths.get(fileDir, folderName + packagePath);
Files.createDirectories(folderPath);
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
IInArchive inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))) {
ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface();
final String[] str = {null};
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
if (!item.isFolder()) {
ExtractOperationResult result;
String finalFolderName = folderName;
result = item.extractSlow(data -> {
try {
str[0] = RarUtils.getUtf8String(item.getPath());
if (RarUtils.isMessyCode(str[0])) {
str[0] = new String(item.getPath().getBytes(StandardCharsets.ISO_8859_1), "gbk");
}
str[0] = str[0].replace("\\", File.separator); //Linux 下路径错误
String str1 = str[0].substring(0, str[0].lastIndexOf(File.separator) + 1);
File file = new File(fileDir, finalFolderName + packagePath + File.separator + str1);
if (!file.exists()) {
file.mkdirs();
}
OutputStream out = new FileOutputStream(fileDir + finalFolderName + packagePath + File.separator + str[0], true);
IOUtils.write(data, out);
out.close();
} catch (Exception e) {
e.printStackTrace();
return Integer.parseInt(null);
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) {
FileType type = FileType.typeFromUrl(str[0]);
if (type.equals(FileType.PICTURE)) {
imgUrls.add(baseUrl + folderName + packagePath + "/" + str[0].replace("\\", "/"));
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.");
}
fileHandlerService.putImgCache(fileName + packagePath, imgUrls);
} else {
return null;
}
FileType type = FileType.typeFromUrl(filePathInsideArchive.toString());
if (type.equals(FileType.PICTURE)) { //图片缓存到集合,为了特殊符号需要进行编码
imgUrls.add(baseUrl + URLEncoder.encode(fileName + packagePath+"/"+ folderPath.relativize(filePathInsideArchive).toString().replace("\\", "/"), "UTF-8"));
}
}
}
return folderName + packagePath;
fileHandlerService.putImgCache(fileName + packagePath, imgUrls);
} catch (Exception e) {
throw new Exception(e);
} finally {
if (inArchive != null) {
try {
inArchive.close();
} catch (SevenZipException e) {
System.err.println("Error closing archive: " + e);
}
}
if (randomAccessFile != null) {
try {
randomAccessFile.close();
} catch (IOException e) {
System.err.println("Error closing file: " + e);
}
}
throw new Exception("Error processing RAR file: " + e.getMessage(), e);
}
return folderName + packagePath;
}
}
private Path getFilePathInsideArchive(ISimpleInArchiveItem item, Path folderPath) throws SevenZipException, UnsupportedEncodingException {
String insideFileName = RarUtils.getUtf8String(item.getPath());
if (RarUtils.isMessyCode(insideFileName)) {
insideFileName = new String(item.getPath().getBytes(StandardCharsets.ISO_8859_1), "gbk");
}
// 正规化路径并验证是否安全
Path normalizedPath = folderPath.resolve(insideFileName).normalize();
if (!normalizedPath.startsWith(folderPath)) {
throw new SecurityException("Unsafe path detected: " + insideFileName);
}
try {
Files.createDirectories(normalizedPath.getParent());
} catch (IOException e) {
throw new RuntimeException("Failed to create directory: " + normalizedPath.getParent(), e);
}
return normalizedPath;
}
}

View File

@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.ExtendedModelMap;
import javax.annotation.PostConstruct;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;
/**
@@ -73,7 +73,7 @@ public class FileConvertQueueTask {
TimeUnit.SECONDS.sleep(10);
} catch (Exception ex) {
Thread.currentThread().interrupt();
ex.printStackTrace();
logger.error("Failed to sleep after exception", ex);
}
logger.info("处理预览转换任务异常url{}", url, e);
}

View File

@@ -14,8 +14,8 @@ import com.aspose.cad.*;
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
import com.aspose.cad.imageoptions.*;
import com.itextpdf.text.pdf.PdfReader;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
@@ -31,13 +31,16 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.stream.IntStream;
@@ -175,13 +178,13 @@ public class FileHandlerService implements InitializingBean {
sb.append("<script src=\"excel/excel.header.js\" type=\"text/javascript\"></script>");
sb.append("<link rel=\"stylesheet\" href=\"excel/excel.css\">");
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to read file: {}", outFilePath, e);
}
// 重新写入文件
try (FileOutputStream fos = new FileOutputStream(outFilePath); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
writer.write(sb.toString());
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to write file: {}", outFilePath, e);
}
}
@@ -236,9 +239,9 @@ public class FileHandlerService implements InitializingBean {
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
String filePassword = fileAttribute.getFilePassword();
String pdfPassword = null;
PDDocument doc = null;
PdfReader pdfReader = null;
PDDocument doc;
final String[] pdfPassword = {null};
final int[] pageCount = new int[1];
if (!forceUpdatedCache) {
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
if (!CollectionUtils.isEmpty(cacheResult)) {
@@ -246,64 +249,77 @@ public class FileHandlerService implements InitializingBean {
}
}
List<String> imageUrls = new ArrayList<>();
File pdfFile = new File(fileNameFilePath);
if (!pdfFile.exists()) {
return null;
}
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
File path = new File(folder);
if (!path.exists() && !path.mkdirs()) {
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
}
try {
File pdfFile = new File(fileNameFilePath);
if (!pdfFile.exists()) {
return null;
}
doc = PDDocument.load(pdfFile, filePassword);
doc = Loader.loadPDF(pdfFile, filePassword);
doc.setResourceCache(new NotResourceCache());
int pageCount = doc.getNumberOfPages();
PDFRenderer pdfRenderer = new PDFRenderer(doc);
int index = pdfFilePath.lastIndexOf(".");
String folder = pdfFilePath.substring(0, index);
File path = new File(folder);
if (!path.exists() && !path.mkdirs()) {
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
}
String imageFilePath;
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, ConfigConstants.getPdf2JpgDpi(), ImageType.RGB);
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, pageIndex);
imageUrls.add(imageUrl);
}
try {
if (!ObjectUtils.isEmpty(filePassword)) { //获取到密码 判断是否是加密文件
pdfReader = new PdfReader(fileNameFilePath); //读取PDF文件 通过异常获取该文件是否有密码字符
}
} catch (Exception e) { //获取异常方法 判断是否有加密字符串
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
pdfPassword = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
}
pageCount[0] = doc.getNumberOfPages();
} catch (IOException e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
pdfPassword[0] = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
}
}
if (!PDF_PASSWORD_MSG.equals(pdfPassword)) { //该文件异常 错误原因非密码原因输出错误
logger.error("Convert pdf exception, pdfFilePath{}", pdfFilePath, e);
}
} finally {
if (pdfReader != null) { //关闭
pdfReader.close();
}
}
if (usePasswordCache || !PDF_PASSWORD_MSG.equals(pdfPassword)) { //加密文件 判断是否启用缓存命令
this.addPdf2jpgCache(pdfFilePath, pageCount);
}
} catch (IOException e) {
if (!e.getMessage().contains(PDF_PASSWORD_MSG)) {
logger.error("Convert pdf to jpg exception, pdfFilePath{}", pdfFilePath, e);
if (!PDF_PASSWORD_MSG.equals(pdfPassword[0])) { //该文件异常 错误原因非密码原因输出错误
logger.error("Convert pdf exception, pdfFilePath{}", pdfFilePath, e);
}
throw new Exception(e);
} finally {
if (doc != null) { //关闭
}
Callable <List<String>> call = () -> {
try {
String imageFilePath;
BufferedImage image = null;
PDFRenderer pdfRenderer = new PDFRenderer(doc);
pdfRenderer.setSubsamplingAllowed(true);
for (int pageIndex = 0; pageIndex < pageCount[0]; pageIndex++) {
imageFilePath = folder + File.separator + pageIndex + PDF2JPG_IMAGE_FORMAT;
image = pdfRenderer.renderImageWithDPI(pageIndex, ConfigConstants.getPdf2JpgDpi(), ImageType.RGB);
ImageIOUtil.writeImage(image, imageFilePath, ConfigConstants.getPdf2JpgDpi());
String imageUrl = this.getPdf2jpgUrl(pdfFilePath, pageIndex);
imageUrls.add(imageUrl);
}
image.flush();
} catch (IOException e) {
throw new Exception(e);
} finally {
doc.close();
}
return imageUrls;
};
Future<List<String>> result = pool.submit(call);
int pdftimeout;
if(pageCount[0] <=50){
pdftimeout = ConfigConstants.getPdfTimeout();
}else if(pageCount[0] <=200){
pdftimeout = ConfigConstants.getPdfTimeout80();
}else {
pdftimeout = ConfigConstants.getPdfTimeout200();
}
try {
result.get(pdftimeout, TimeUnit.SECONDS);
// 如果在超时时间内没有数据返回则抛出TimeoutException异常
} catch (InterruptedException | ExecutionException e) {
throw new Exception(e);
} catch (TimeoutException e) {
throw new Exception("overtime");
} finally {
//关闭
doc.close();
}
if (usePasswordCache || ObjectUtils.isEmpty(filePassword)) { //加密文件 判断是否启用缓存命令
this.addPdf2jpgCache(pdfFilePath, pageCount[0]);
}
return imageUrls;
}
@@ -458,21 +474,17 @@ public class FileHandlerService implements InitializingBean {
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
if (isCompressFile) { //判断是否使用特定压缩包符号
try {
// http://127.0.0.1:8012/各类型文件1 - 副本.zip_/各类型文件/正常预览/PPT转的PDF.pdf?kkCompressfileKey=各类型文件1 - 副本.zip_
// http://127.0.0.1:8012/preview/各类型文件1 - 副本.zip_/各类型文件/正常预览/PPT转的PDF.pdf?kkCompressfileKey=各类型文件1 - 副本.zip_ 获取路径就会错误 需要下面的方法
String urlStrr = getSubString(compressFilePath, compressFileKey); //反代情况下添加前缀,只获取有压缩包字符的路径
originFileName = compressFileKey + urlStrr.trim(); //拼接完整路径
originFileName = URLDecoder.decode(originFileName, uriEncoding); //压缩包文件中文编码问题
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
attribute.setSkipDownLoad(true);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("Failed to decode file name: {}", originFileName, e);
}
}
if (UrlEncoderUtils.hasUrlEncoded(originFileName)) { //判断文件名是否转义
try {
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("Failed to decode file name: {}", originFileName, e);
}
}else {
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义

View File

@@ -26,6 +26,7 @@ public interface FilePreview {
String CODE_FILE_PREVIEW_PAGE = "code";
String EXEL_FILE_PREVIEW_PAGE = "html";
String XML_FILE_PREVIEW_PAGE = "xml";
String JSON_FILE_PREVIEW_PAGE = "json";
String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
String BPMN_FILE_PREVIEW_PAGE = "bpmn";
String DCM_FILE_PREVIEW_PAGE = "dcm";

View File

@@ -15,8 +15,8 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

View File

@@ -2,9 +2,12 @@ package cn.keking.service.cache;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.pdmodel.DefaultResourceCache;
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
import org.apache.pdfbox.pdmodel.graphics.shading.PDShading;
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
/**
* @author: Sawyer.Yong
@@ -14,7 +17,21 @@ import java.io.IOException;
public class NotResourceCache extends DefaultResourceCache {
@Override
public void put(COSObject indirect, PDXObject xobject) throws IOException {
// do nothing
public void put(COSObject indirect, PDColorSpace colorSpace) {
}
@Override
public void put(COSObject indirect, PDExtendedGraphicsState extGState) {
}
@Override
public void put(COSObject indirect, PDShading shading) {
}
@Override
public void put(COSObject indirect, PDAbstractPattern pattern) {
}
@Override
public void put(COSObject indirect, PDPropertyList propertyList) {
}
@Override
public void put(COSObject indirect, PDXObject xobject) {
}
}

View File

@@ -7,7 +7,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

View File

@@ -9,6 +9,8 @@ import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
import cn.keking.web.filter.BaseUrlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
@@ -22,6 +24,7 @@ import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
@Service
public class CadFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(CadFilePreviewImpl.class);
private static final String OFFICE_PREVIEW_TYPE_IMAGE = "image";
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
@@ -55,7 +58,7 @@ public class CadFilePreviewImpl implements FilePreview {
try {
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
} catch (Exception e) {
e.printStackTrace();
logger.error("Failed to convert CAD file: {}", filePath, e);
}
if (imageUrls == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常请联系管理员");

View File

@@ -10,6 +10,7 @@ import cn.keking.service.CompressFileReader;
import cn.keking.utils.KkFileUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.slf4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
@@ -28,6 +29,9 @@ public class CompressFilePreviewImpl implements FilePreview {
private final CompressFileReader compressFileReader;
private final OtherFilePreviewImpl otherFilePreview;
private static final String Rar_PASSWORD_MSG = "password";
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(CompressFileReader.class);
public CompressFilePreviewImpl(FileHandlerService fileHandlerService, CompressFileReader compressFileReader, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService;
this.compressFileReader = compressFileReader;
@@ -36,28 +40,25 @@ public class CompressFilePreviewImpl implements FilePreview {
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String fileName=fileAttribute.getName();
String fileName = fileAttribute.getName();
String filePassword = fileAttribute.getFilePassword();
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String fileTree = null;
// 判断文件名是否存在(redis缓存读取)
if (forceUpdatedCache || !StringUtils.hasText(fileHandlerService.getConvertedFile(fileName)) || !ConfigConstants.isCacheEnabled()) {
if (forceUpdatedCache || !StringUtils.hasText(fileHandlerService.getConvertedFile(fileName)) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
String filePath = response.getContent();
try {
fileTree = compressFileReader.unRar(filePath, filePassword,fileName, fileAttribute);
fileTree = compressFileReader.unRar(filePath, filePassword, fileName, fileAttribute);
} catch (Exception e) {
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
for (Throwable throwable : throwableArray) {
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
if (e.getMessage().toLowerCase().contains(Rar_PASSWORD_MSG)) {
model.addAttribute("needFilePassword", true);
return EXEL_FILE_PREVIEW_PAGE;
}
}
if (e.getMessage().toLowerCase().contains(Rar_PASSWORD_MSG)) {
model.addAttribute("needFilePassword", true);
return EXEL_FILE_PREVIEW_PAGE;
}else {
logger.error("Error processing RAR file: " + e.getMessage(), e);
}
}
if (!ObjectUtils.isEmpty(fileTree)) {
@@ -69,14 +70,14 @@ public class CompressFilePreviewImpl implements FilePreview {
// 加入缓存
fileHandlerService.addConvertedFile(fileName, fileTree);
}
}else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "压缩文件密码错误! 压缩文件损坏! 压缩文件类型不受支持!");
} else {
return otherFilePreview.notSupportedFile(model, fileAttribute, "压缩文件无法处理!");
}
} else {
fileTree = fileHandlerService.getConvertedFile(fileName);
}
model.addAttribute("fileName", fileName);
model.addAttribute("fileTree", fileTree);
return COMPRESS_FILE_PREVIEW_PAGE;
model.addAttribute("fileName", fileName);
model.addAttribute("fileTree", fileTree);
return COMPRESS_FILE_PREVIEW_PAGE;
}
}

View File

@@ -0,0 +1,95 @@
package cn.keking.service.impl;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.model.ReturnResponse;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
import cn.keking.utils.DownloadUtils;
import cn.keking.utils.KkFileUtils;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.util.HtmlUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
/**
* @author kl (http://kailing.pub)
* @since 2025/01/11
* JSON 文件预览处理实现
*/
@Service
public class JsonFilePreviewImpl implements FilePreview {
private static final Logger LOGGER = LoggerFactory.getLogger(JsonFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
public JsonFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
}
@Override
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
String fileName = fileAttribute.getName();
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
String filePath = fileAttribute.getOriginFilePath();
if (forceUpdatedCache || !fileHandlerService.listConvertedFiles().containsKey(fileName) || !ConfigConstants.isCacheEnabled()) {
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
if (response.isFailure()) {
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
}
filePath = response.getContent();
if (ConfigConstants.isCacheEnabled()) {
fileHandlerService.addConvertedFile(fileName, filePath);
}
try {
String fileData = readJsonFile(filePath, fileName);
String escapedData = HtmlUtils.htmlEscape(fileData);
String base64Data = Base64.encodeBase64String(escapedData.getBytes(StandardCharsets.UTF_8));
model.addAttribute("textData", base64Data);
} catch (IOException e) {
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
}
return JSON_FILE_PREVIEW_PAGE;
}
String fileData = null;
try {
fileData = HtmlUtils.htmlEscape(readJsonFile(filePath, fileName));
} catch (IOException e) {
LOGGER.error("读取JSON文件失败: {}", filePath, e);
}
String base64Data = Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8));
model.addAttribute("textData", base64Data);
return JSON_FILE_PREVIEW_PAGE;
}
/**
* 读取 JSON 文件,强制使用 UTF-8 编码
* JSON 标准规定必须使用 UTF-8 编码
*/
private String readJsonFile(String filePath, String fileName) throws IOException {
File file = new File(filePath);
if (KkFileUtils.isIllegalFileName(fileName)) {
return null;
}
if (!file.exists() || file.length() == 0) {
return "";
}
// JSON 标准规定使用 UTF-8 编码,不依赖自动检测
byte[] bytes = Files.readAllBytes(Paths.get(filePath));
return new String(bytes, StandardCharsets.UTF_8);
}
}

View File

@@ -11,6 +11,8 @@ import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
@@ -26,6 +28,7 @@ import java.io.File;
@Service
public class MediaFilePreviewImpl implements FilePreview {
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
private static final String mp4 = "mp4";
@@ -66,7 +69,7 @@ public class MediaFilePreviewImpl implements FilePreview {
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Failed to convert media file: {}", filePath, e);
}
if (convertedUrl == null) {
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
@@ -148,7 +151,7 @@ public class MediaFilePreviewImpl implements FilePreview {
recorder.record(captured_frame);
}
} catch (Exception e) {
e.printStackTrace();
logger.error("Failed to convert video file to mp4: {}", filePath, e);
return null;
} finally {
if (recorder != null) { //关闭

View File

@@ -9,11 +9,17 @@ import cn.keking.utils.DownloadUtils;
import cn.keking.utils.EncodingDetects;
import cn.keking.utils.KkFileUtils;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.util.HtmlUtils;
import java.io.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
@@ -23,10 +29,12 @@ import java.nio.charset.StandardCharsets;
@Service
public class SimTextFilePreviewImpl implements FilePreview {
private static final Logger LOGGER = LoggerFactory.getLogger(SimTextFilePreviewImpl.class);
private final FileHandlerService fileHandlerService;
private final OtherFilePreviewImpl otherFilePreview;
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
this.fileHandlerService = fileHandlerService;
this.otherFilePreview = otherFilePreview;
}
@@ -47,7 +55,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
}
try {
String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
} catch (IOException e) {
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
}
@@ -57,9 +65,9 @@ public class SimTextFilePreviewImpl implements FilePreview {
try {
fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("读取文本文件失败: {}", filePath, e);
}
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes()));
model.addAttribute("textData", Base64.encodeBase64String(fileData.getBytes(StandardCharsets.UTF_8)));
return TXT_FILE_PREVIEW_PAGE;
}

View File

@@ -6,9 +6,9 @@ import cn.keking.model.ReturnResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;

View File

@@ -86,6 +86,9 @@ public class LocalOfficeUtils {
"/opt/libreoffice7.4",
"/opt/libreoffice7.5",
"/opt/libreoffice7.6",
"/opt/libreoffice24.2",
"/opt/libreoffice24.8",
"/opt/libreoffice25.2",
"/usr/lib64/libreoffice",
"/usr/lib/libreoffice",
"/usr/local/lib64/libreoffice",

View File

@@ -4,6 +4,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.poi.EncryptedDocumentException;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
@@ -18,6 +20,7 @@ import java.nio.file.Paths;
*/
public class OfficeUtils {
private static final Logger logger = LoggerFactory.getLogger(OfficeUtils.class);
private static final String POI_INVALID_PASSWORD_MSG = "password";
/**
@@ -49,7 +52,7 @@ public class OfficeUtils {
try {
propStream.close();//关闭文件输入流
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to close input stream for file: {}", path, e);
}
}
}
@@ -76,7 +79,7 @@ public class OfficeUtils {
try {
propStream.close();//关闭文件输入流
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to close input stream for file: {}", path, e);
}
}
}

View File

@@ -1,6 +1,9 @@
package cn.keking.utils;
import cn.keking.config.ConfigConstants;
import cn.keking.service.ZtreeNodeVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
@@ -15,6 +18,7 @@ import java.util.regex.Pattern;
* create : 2023-04-08
**/
public class RarUtils {
private static final Logger logger = LoggerFactory.getLogger(RarUtils.class);
private static final String fileDir = ConfigConstants.getFileDir();
public static byte[] getUTF8BytesFromGBKString(String gbkStr) {
@@ -55,7 +59,7 @@ public class RarUtils {
str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
logger.error("Failed to convert string encoding: {}", str, e);
}
}
return str;
@@ -77,7 +81,7 @@ public class RarUtils {
}
public static String specialSymbols(String str) {
//去除压缩包文件字符串中特殊符号
Pattern p = Pattern.compile("\\s|\t|\r|\n|\\+|#|&|=|\\p{P}");
Pattern p = Pattern.compile("\\s|\t|\r|\n|\\+|#|&|=|<EFBFBD>|\\p{P}");
// Pattern p = Pattern.compile("\\s|\\+|#|&|=|\\p{P}");
Matcher m = p.matcher(str);
return m.replaceAll("");

View File

@@ -1,5 +1,8 @@
package cn.keking.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
@@ -31,6 +34,8 @@ import java.net.URL;
*/
public class SimpleEncodingDetects {
private static final Logger logger = LoggerFactory.getLogger(SimpleEncodingDetects.class);
/**
* 得到文件的编码
* @param content 文件内容
@@ -65,10 +70,10 @@ public class SimpleEncodingDetects {
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.error("File not found: {}", file, e);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.error("Failed to read file: {}", file, e);
}
}

View File

@@ -1,16 +1,16 @@
package cn.keking.utils;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -79,13 +79,15 @@ public class WebUtils {
urlStr = clearFullfilenameParam(urlStr);
} else {
fullFileName = getFileNameFromURL(urlStr); //获取文件名
}
if (KkFileUtils.isIllegalFileName(fullFileName)) { //判断文件名是否带有穿越漏洞
return null;
}
if (!UrlEncoderUtils.hasUrlEncoded(fullFileName)) { //判断文件名是否转义
try {
urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "=");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
LOGGER.error("Failed to encode URL: {}", urlStr, e);
}
}
return urlStr;
@@ -153,7 +155,7 @@ public class WebUtils {
URL urlObj = new URL(url);
url = urlObj.getPath().substring(1);
} catch (MalformedURLException e) {
e.printStackTrace();
LOGGER.error("Failed to parse file URL: {}", url, e);
}
}
// 因为url的参数中可能会存在/的情况所以直接url.lastIndexOf("/")会有问题
@@ -288,7 +290,7 @@ public class WebUtils {
* https://github.com/kekingcn/kkFileView/pull/340
*/
try {
return new String(Base64Utils.decodeFromString(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
return new String(Base64.decodeBase64(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
} catch (Exception e) {
if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
LOGGER.error("url解码异常接入方法错误未使用BASE64");

View File

@@ -19,9 +19,9 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -213,6 +213,7 @@ public class FileController {
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
return ReturnResponse.failure(errorMsg);
}
fileUrl = fileUrl.replaceAll("http://", "");
if (KkFileUtils.isIllegalFileName(fileUrl)) {
return ReturnResponse.failure("不允许访问的路径:");
}

View File

@@ -11,9 +11,9 @@ import cn.keking.utils.WebUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.opensagres.xdocreport.core.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
@@ -21,14 +21,15 @@ import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@@ -76,7 +77,11 @@ public class OnlinePreviewController {
model.addAttribute("file", fileAttribute);
FilePreview filePreview = previewFactory.get(fileAttribute);
logger.info("预览文件url{}previewType{}", fileUrl, fileAttribute.getType());
return filePreview.filePreviewHandle(WebUtils.urlEncoderencode(fileUrl), model, fileAttribute); //统一在这里处理 url
fileUrl =WebUtils.urlEncoderencode(fileUrl);
if (ObjectUtils.isEmpty(fileUrl)) {
return otherFilePreview.notSupportedFile(model, "非法路径,不允许访问");
}
return filePreview.filePreviewHandle(fileUrl, model, fileAttribute); //统一在这里处理 url
}
@GetMapping( "/picturesPreview")

View File

@@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
import cn.keking.config.WatermarkConfigConstants;
import cn.keking.utils.KkFileUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
/**

View File

@@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
/**

View File

@@ -1,7 +1,7 @@
package cn.keking.web.filter;
import javax.servlet.*;
import jakarta.servlet.*;
import java.io.IOException;
/**

View File

@@ -3,10 +3,10 @@ package cn.keking.web.filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

View File

@@ -10,7 +10,7 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import jakarta.servlet.*;
import java.io.IOException;
import java.net.URL;
import java.net.URLDecoder;
@@ -35,7 +35,7 @@ public class TrustDirFilter implements Filter {
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
logger.error("加载notTrustDir.html失败", e);
}
}

View File

@@ -4,15 +4,23 @@ import cn.keking.config.ConfigConstants;
import cn.keking.utils.WebUtils;
import java.io.IOException;
import java.util.Map;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.nio.charset.StandardCharsets;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.Set;
import java.util.regex.Pattern;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
@@ -22,6 +30,8 @@ import org.springframework.util.FileCopyUtils;
*/
public class TrustHostFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(TrustHostFilter.class);
private final Map<String, Pattern> wildcardPatternCache = new ConcurrentHashMap<>();
private String notTrustHostHtmlView;
@Override
@@ -32,7 +42,7 @@ public class TrustHostFilter implements Filter {
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
this.notTrustHostHtmlView = new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
logger.error("Failed to load notTrustHost.html file", e);
}
}
@@ -40,9 +50,16 @@ public class TrustHostFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = WebUtils.getSourceUrl(request);
String host = WebUtils.getHost(url);
assert host != null;
if (isNotTrustHost(host)) {
String html = this.notTrustHostHtmlView.replace("${current_host}", host);
String currentHost = host == null ? "UNKNOWN" : host;
if (response instanceof HttpServletResponse httpServletResponse) {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("text/html;charset=UTF-8");
String html = this.notTrustHostHtmlView == null
? "<html><head><meta charset=\"utf-8\"></head><body>当前预览文件来自不受信任的站点:" + currentHost + "</body></html>"
: this.notTrustHostHtmlView.replace("${current_host}", currentHost);
response.getWriter().write(html);
response.getWriter().close();
} else {
@@ -51,15 +68,163 @@ public class TrustHostFilter implements Filter {
}
public boolean isNotTrustHost(String host) {
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) {
return ConfigConstants.getNotTrustHostSet().contains(host);
if (host == null || host.trim().isEmpty()) {
logger.warn("主机名为空或无效,拒绝访问");
return true;
}
// 如果配置了黑名单,优先检查黑名单
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())
&& matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) {
return true;
}
// 如果配置了白名单,检查是否在白名单中
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
return !ConfigConstants.getTrustHostSet().contains(host);
// 支持通配符 * 表示允许所有主机
if (ConfigConstants.getTrustHostSet().contains("*")) {
logger.debug("允许所有主机访问(通配符模式): {}", host);
return false;
}
return !matchAnyPattern(host, ConfigConstants.getTrustHostSet());
}
// 安全加固默认拒绝所有未配置的主机防止SSRF攻击
// 如果需要允许所有主机,请在配置文件中明确设置 trust.host = *
logger.warn("未配置信任主机列表,拒绝访问主机: {},请在配置文件中设置 trust.host 或 KK_TRUST_HOST 环境变量", host);
return true;
}
private boolean matchAnyPattern(String host, Set<String> hostPatterns) {
String normalizedHost = host.toLowerCase(Locale.ROOT);
for (String hostPattern : hostPatterns) {
if (matchHostPattern(normalizedHost, hostPattern)) {
return true;
}
}
return false;
}
/**
* 支持三种匹配方式:
* 1. 精确匹配example.com
* 2. 通配符匹配:*.example.com、192.168.*
* 3. IPv4 CIDR192.168.0.0/16
*/
private boolean matchHostPattern(String host, String hostPattern) {
if (hostPattern == null || hostPattern.trim().isEmpty()) {
return false;
}
String pattern = hostPattern.trim().toLowerCase(Locale.ROOT);
if ("*".equals(pattern)) {
return true;
}
if (pattern.contains("/")) {
return matchIpv4Cidr(host, pattern);
}
if (pattern.contains("*")) {
if (isIpv4WildcardPattern(pattern)) {
return matchIpv4Wildcard(host, pattern);
}
Pattern compiledPattern = wildcardPatternCache.computeIfAbsent(pattern, key -> Pattern.compile(wildcardToRegex(key)));
return compiledPattern.matcher(host).matches();
}
return host.equals(pattern);
}
private boolean isIpv4WildcardPattern(String pattern) {
return pattern.matches("^[0-9.*]+$") && pattern.contains(".");
}
private boolean matchIpv4Wildcard(String host, String pattern) {
if (parseLiteralIpv4(host) == null) {
return false;
}
String[] hostParts = host.split("\\.");
String[] patternParts = pattern.split("\\.");
if (hostParts.length != 4 || patternParts.length < 1 || patternParts.length > 4) {
return false;
}
for (int i = 0; i < patternParts.length; i++) {
String p = patternParts[i];
if ("*".equals(p)) {
continue;
}
if (!p.equals(hostParts[i])) {
return false;
}
}
return true;
}
private String wildcardToRegex(String wildcard) {
StringBuilder regexBuilder = new StringBuilder("^");
String[] parts = wildcard.split("\\*", -1);
for (int i = 0; i < parts.length; i++) {
regexBuilder.append(Pattern.quote(parts[i]));
if (i < parts.length - 1) {
regexBuilder.append(".*");
}
}
regexBuilder.append("$");
return regexBuilder.toString();
}
private boolean matchIpv4Cidr(String host, String cidr) {
try {
String[] parts = cidr.split("/");
if (parts.length != 2) {
return false;
}
Long hostInt = parseLiteralIpv4(host);
Long networkInt = parseLiteralIpv4(parts[0]);
int prefixLength = Integer.parseInt(parts[1]);
if (hostInt == null || networkInt == null || prefixLength < 0 || prefixLength > 32) {
return false;
}
long mask = prefixLength == 0 ? 0L : (0xFFFFFFFFL << (32 - prefixLength)) & 0xFFFFFFFFL;
return (hostInt & mask) == (networkInt & mask);
} catch (NumberFormatException e) {
return false;
}
}
/**
* 仅解析字面量 IPv4 地址(不做 DNS 解析),防止 DNS rebinding/TOCTOU 风险。
*/
private Long parseLiteralIpv4(String input) {
if (input == null || input.trim().isEmpty()) {
return null;
}
String[] parts = input.trim().split("\\.");
if (parts.length != 4) {
return null;
}
long result = 0L;
for (String part : parts) {
if (part.isEmpty() || part.length() > 3) {
return null;
}
int value;
try {
value = Integer.parseInt(part);
} catch (NumberFormatException e) {
return null;
}
if (value < 0 || value > 255) {
return null;
}
result = (result << 8) | value;
}
return result;
}
@Override
public void destroy() {

View File

@@ -2,9 +2,9 @@ package cn.keking.web.filter;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**

View File

@@ -6,8 +6,9 @@
| < | < | | | | | | | __/ \ / | | | __/ \ V V /
|_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/
=> Java Version :: ${java.version}
=> Spring Boot :: ${spring-boot.version}
=> kkFileView :: 4.4.0-beta
=> kkFileView :: 4.4.0
=> Home site :: https://kkview.cn
=> Github :: https://github.com/kekingcn/kkFileView
=> Gitee :: https://gitee.com/kekingcn/file-online-preview

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,19 @@
*/
function initWaterMark() {
let watermarkTxt = '${watermarkTxt}';
if (watermarkTxt !== '') {
if (watermarkTxt === '') {
return;
}
let lastWidth = 0;
let lastHeight = 0;
const checkResize = () => {
const currentWidth = document.documentElement.scrollWidth;
const currentHeight = document.documentElement.scrollHeight;
// 检测尺寸是否变化
if (currentWidth === lastWidth && currentHeight === lastHeight) {
return;
}
// 如果变化了, 重新初始化水印
watermark.init({
watermark_txt: '${watermarkTxt}',
watermark_x: 0,
@@ -25,7 +37,11 @@
watermark_height: ${watermarkHeight},
watermark_angle: ${watermarkAngle},
});
}
// 更新存储的宽口大小
lastWidth = currentWidth;
lastHeight = currentHeight;
};
setInterval(checkResize, 1000);
}
</script>

View File

@@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8"/>
<title>压缩包预览</title>
<title>${file.name}压缩包预览</title>
<script src="js/jquery-3.6.1.min.js"></script>
<#include "*/commonHeader.ftl">
<script src="js/base64.min.js" type="text/javascript"></script>
@@ -49,14 +49,28 @@
onClick: chooseNode,
}
};
function isNotEmpty(value) {
return value !== null && value !== undefined && value !== '' && value !== 0 && !(value instanceof Array && value.length === 0) && !isNaN(value);
}
function getQueryParam(url, param) {
var urlObj = new URL(url);
return urlObj.searchParams.get(param);
}
var currentUrl = window.location.href;
var keyword = getQueryParam(currentUrl, 'watermarkTxt');
function chooseNode(event, treeId, treeNode) {
if (!treeNode.isParent) {
var path = '${baseUrl}' + treeNode.id + "?kkCompressfileKey=" + encodeURIComponent('${fileTree}')+"&kkCompressfilepath=" + encodeURIComponent(treeNode.id)+"&fullfilename="+encodeURIComponent(treeNode.name);
location.href = "${baseUrl}onlinePreview?url=" + encodeURIComponent(Base64.encode(path));
var path = '${baseUrl}'+treeNode.id+"?kkCompressfileKey="+'${fileTree}'+"&kkCompressfilepath="+encodeURIComponent(treeNode.id)+"&fullfilename="+encodeURIComponent(treeNode.name);
if (isNotEmpty(keyword)){
location.href = "${baseUrl}onlinePreview?url=" + encodeURIComponent(Base64.encode(path))+"&watermarkTxt="+keyword;
}else{
location.href = "${baseUrl}onlinePreview?url=" + encodeURIComponent(Base64.encode(path));}
}
}
$(document).ready(function () {
var url = '${fileTree}';
var url = "http://"+'${fileTree}'; //添加http协议方法
$.ajax({
type: "get",
url: "${baseUrl}directory?urls="+encodeURIComponent(Base64.encode(url)),
@@ -66,6 +80,9 @@
}
});
});
window.onload = function () {
initWaterMark();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,384 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1.0">
<title>JSON文件预览</title>
<#include "*/commonHeader.ftl">
<script src="js/jquery-3.6.1.min.js" type="text/javascript"></script>
<link rel="stylesheet" href="bootstrap/css/bootstrap.min.css"/>
<script src="bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
<script src="js/base64.min.js" type="text/javascript"></script>
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
.container {
max-width: 100%;
padding: 20px;
}
#json {
padding: 0;
overflow-x: auto;
}
pre {
padding: 20px;
padding-left: 65px; /* 为行号留出空间 */
white-space: pre-wrap;
word-wrap: break-word;
font-size: 14px;
line-height: 1.6;
position: relative;
}
.json-key {
color: #881391;
font-weight: bold;
}
.json-string {
color: #1A1AA6;
}
.json-number {
color: #1C00CF;
}
.json-boolean {
color: #0D22FF;
font-weight: bold;
}
.json-null {
color: #808080;
font-weight: bold;
}
.json-toggle {
cursor: pointer;
color: #666;
user-select: none;
display: inline-block;
width: 16px;
font-weight: bold;
}
.json-toggle:hover {
color: #333;
}
.json-node {
display: block;
}
.line-number {
position: absolute;
left: 0;
width: 55px;
color: #999;
font-size: 12px;
user-select: none;
text-align: right;
padding-right: 10px;
border-right: 1px solid #ddd;
background-color: #f8f9fa;
}
</style>
</head>
<body>
<input hidden id="textData" value="${textData}"/>
<div class="container">
<div class="panel panel-default">
<div id="formatted_btn" class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
${file.name}
</a>
</h4>
</div>
<div id="raw_btn" class="panel-heading">
<h4 class="panel-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
${file.name}
</a>
</h4>
</div>
<div id="json" class="panel-body">
</div>
</div>
</div>
<script>
/**
* 初始化
*/
window.onload = function () {
$("#formatted_btn").hide();
initWaterMark();
loadJsonData();
}
/**
* HTML 反转义(用于还原后端转义的内容)
* 使用浏览器的 DOM 来正确解码所有 HTML 实体
*/
function htmlUnescape(str) {
if (!str || str.length === 0) return "";
var textarea = document.createElement('textarea');
textarea.innerHTML = str;
return textarea.value;
}
/**
* HTML 转义(用于安全显示)
*/
function htmlEscape(str) {
if (!str || str.length === 0) return "";
var s = str;
s = s.replace(/&/g, "&amp;");
s = s.replace(/</g, "&lt;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&#39;");
return s;
}
/**
* 移除 BOM (Byte Order Mark)
*/
function removeBOM(str) {
if (str.charCodeAt(0) === 0xFEFF) {
return str.substring(1);
}
return str;
}
// 全局行号计数器
var lineNumber = 1;
/**
* 构建可展开/收起的 JSON 树形结构
*/
function buildJsonTree(obj, indent, skipLineNumber) {
indent = indent || 0;
skipLineNumber = skipLineNumber || false;
var html = '';
var indentStr = ' '.repeat(indent);
if (obj === null) {
return '<span class="json-null">null</span>';
}
if (typeof obj !== 'object') {
if (typeof obj === 'string') {
// 转义特殊字符,避免换行和制表符破坏布局
var escapedStr = obj
.replace(/\\/g, '\\\\')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/"/g, '\\"');
return '<span class="json-string">"' + htmlEscape(escapedStr) + '"</span>';
} else if (typeof obj === 'number') {
return '<span class="json-number">' + obj + '</span>';
} else if (typeof obj === 'boolean') {
return '<span class="json-boolean">' + obj + '</span>';
}
return htmlEscape(String(obj));
}
var isArray = Array.isArray(obj);
var entries = isArray ? obj : Object.keys(obj);
var openBracket = isArray ? '[' : '{';
var closeBracket = isArray ? ']' : '}';
if (entries.length === 0) {
return openBracket + closeBracket;
}
var nodeId = 'node_' + Math.random().toString(36).substr(2, 9);
// 如果不跳过行号,说明是新的一行
if (!skipLineNumber) {
html += '<span class="line-number">' + lineNumber++ + '</span>';
}
html += '<span class="json-toggle" onclick="toggleJsonNode(\'' + nodeId + '\')">▼</span> ';
html += openBracket + '\n';
html += '<div id="' + nodeId + '" class="json-node">';
for (var i = 0; i < entries.length; i++) {
var key = isArray ? i : entries[i];
var value = isArray ? entries[i] : obj[entries[i]];
html += '<span class="line-number">' + lineNumber++ + '</span>';
html += indentStr + ' ';
if (!isArray) {
html += '<span class="json-key">"' + htmlEscape(key) + '"</span>: ';
}
// 如果值是对象或数组,跳过它的行号(因为已经在上面添加了)
html += buildJsonTree(value, indent + 1, true);
if (i < entries.length - 1) {
html += ',';
}
html += '\n';
}
html += '</div>';
html += '<span class="line-number">' + lineNumber++ + '</span>';
html += indentStr + closeBracket;
return html;
}
/**
* 切换 JSON 节点展开/收起
*/
function toggleJsonNode(nodeId) {
var node = document.getElementById(nodeId);
var toggle = event.target;
if (node.style.display === 'none') {
node.style.display = 'block';
toggle.textContent = '▼';
} else {
node.style.display = 'none';
toggle.textContent = '▶';
}
}
/**
* 全部展开
*/
function expandAll() {
var nodes = document.querySelectorAll('.json-node');
var toggles = document.querySelectorAll('.json-toggle');
nodes.forEach(function(node) {
node.style.display = 'block';
});
toggles.forEach(function(toggle) {
toggle.textContent = '▼';
});
}
/**
* 全部收起
*/
function collapseAll() {
var nodes = document.querySelectorAll('.json-node');
var toggles = document.querySelectorAll('.json-toggle');
nodes.forEach(function(node) {
node.style.display = 'none';
});
toggles.forEach(function(toggle) {
toggle.textContent = '▶';
});
}
/**
* JSON 语法高亮(简单版本,用于原始文本视图)
*/
function syntaxHighlight(json) {
json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
var cls = 'json-number';
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = 'json-key';
} else {
cls = 'json-string';
}
} else if (/true|false/.test(match)) {
cls = 'json-boolean';
} else if (/null/.test(match)) {
cls = 'json-null';
}
return '<span class="' + cls + '">' + match + '</span>';
});
}
/**
* UTF-8 解码 Base64正确处理中文等 UTF-8 字符)
*/
function decodeBase64UTF8(base64Str) {
try {
// 方法1使用现代浏览器的 TextDecoder API推荐
if (typeof TextDecoder !== 'undefined') {
var binaryString = window.atob(base64Str);
var bytes = new Uint8Array(binaryString.length);
for (var i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new TextDecoder('utf-8').decode(bytes);
}
// 方法2降级方案
return decodeURIComponent(escape(window.atob(base64Str)));
} catch (e) {
console.error('Base64 decode error:', e);
// 最后降级到 Base64.js 库
return Base64.decode(base64Str);
}
}
/**
* 加载 JSON 数据
*/
function loadJsonData() {
try {
var textData = decodeBase64UTF8($("#textData").val());
// 1. 先反转义 HTML 实体(因为后端已经转义过)
textData = htmlUnescape(textData);
// 2. 移除 BOM
textData = removeBOM(textData);
// 保存原始文本(用于显示时再次转义以保证安全)
window.rawText = "<pre style='background-color: #FFFFFF; border: none; margin: 0;'>" + htmlEscape(textData) + "</pre>";
// 尝试解析并格式化 JSON
try {
var jsonObj = JSON.parse(textData);
// 重置行号计数器
lineNumber = 1;
// 构建树形视图
var treeHtml = '<div style="padding: 20px;">';
treeHtml += '<div style="margin-bottom: 10px;">';
treeHtml += '<button onclick="expandAll()" class="btn btn-sm btn-default" style="margin-right: 5px;">全部展开</button>';
treeHtml += '<button onclick="collapseAll()" class="btn btn-sm btn-default">全部收起</button>';
treeHtml += '</div>';
treeHtml += '<pre style="background-color: #f8f9fa; border: none; margin: 0;">';
treeHtml += buildJsonTree(jsonObj, 0);
treeHtml += '</pre></div>';
window.formattedJson = treeHtml;
// 默认显示树形视图
$("#json").html(window.formattedJson);
} catch (e) {
// 如果不是有效的 JSON显示错误并回退到原始文本
window.formattedJson = "<div class='alert alert-warning'>JSON 解析失败: " + htmlEscape(e.message) + "</div>" + window.rawText;
$("#json").html(window.formattedJson);
}
} catch (e) {
$("#json").html("<div class='alert alert-danger'>文件加载失败: " + htmlEscape(e.message) + "</div>");
}
}
/**
* 按钮点击事件
*/
$(function () {
$("#formatted_btn").click(function () {
$("#json").html(window.formattedJson);
$("#raw_btn").show();
$("#formatted_btn").hide();
});
$("#raw_btn").click(function () {
$("#json").html(window.rawText);
$("#formatted_btn").show();
$("#raw_btn").hide();
});
});
</script>
</body>
</html>

View File

@@ -148,6 +148,18 @@
<input type="file" id="file" name="file" style="float: left; margin: 0 auto; font-size:22px;" placeholder="请选择文件"/>
<input type="button" id="fileUploadBtn" class="btn btn-success" value=" "/>
</form>
<#else>
<div style="padding: 20px; margin: 10px 0; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">
<p style="margin: 0; color: #6c757d; font-size: 16px;">
文件上传功能默认已禁用。如需开启,请通过以下方式配置:
<br/>
• 配置文件:<code>file.upload.disable=false</code>
<br/>
• 环境变量:<code>KK_FILE_UPLOAD_DISABLE=false</code>
<br/>
<strong style="color: #dc3545;">请注意:文件上传限开发环境调试使用,生产环境建议保持关闭状态,避免非法上传导致的安全隐患。</strong>
</p>
</div>
</#if>
<table id="table" data-pagination="true"></table>
</div>

View File

@@ -33,6 +33,63 @@
<div class="page-header">
<h1>版本发布记录</h1>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">2025年01月16日v4.4.0版本</h3>
</div>
<div class="panel-body">
<div>
<h4>优化</h4>
1. 优化 OFD 移动端预览 页面不自适应 <br>
2. 更新 xlsx 前端解析组件,加速解析速度 <br>
3. 升级 CAD 组件 <br>
4. office 功能调整,支持批注、转换页码限制、生成水印等功能 <br>
5. 升级 markdown 组件 <br>
6. 升级 dcm 解析组件 <br>
7. 升级 PDF.JS 解析组件 <br>
8. 更换视频播放插件为 ckplayer <br>
9. tif 解析更加智能化,支持被修改的图片格式 <br>
10. 针对大小文本文件检测字符编码的正确率,处理并发隐患 <br>
11. 重构下载文件的代码,新增通用的文件服务器认证访问的设计 <br>
12. 更新 bootstrap 组件,并精简掉不需要的文件 <br>
13. 更新 epub 版本,优化 epub 显示效果 <br>
14. 解决定时清除缓存时,对于多媒体类型文件,只删除了磁盘缓存文件 <br>
15. 自动检测已安装 Office 组件,增加 LibreOffice 7.5 & 7.6 版本默认路径 <br>
16. 修改 drawio 默认为预览模式 <br>
17. 新增 PDF 线程管理、超时管理、内存缓存管理,更新 PDF 解析组件版本 <br>
18. 优化 Dockerfile支持真正的跨平台构建镜像 <br>
<br>
<h4>新增</h4>
1. xlsx 新增支持打印功能 <br>
2. 配置文件新增启用 GZIP 压缩 <br>
3. CAD 格式新增支持转换成 SVG 和 TIF 格式,新增超时结束、线程管理 <br>
4. 新增删除文件使用验证码校验 <br>
5. 新增 xbrl 格式预览支持 <br>
6. PDF 预览新增控制签名、绘图、插图控制、搜索定位页码、定义显示内容等功能 <br>
7. 新增 CSV 格式前端解析支持 <br>
8. 新增 ARM64 下的 Docker 镜像支持 <br>
9. 新增 Office 预览支持转换超时属性设置功能 <br>
10. 新增预览文件 host 黑名单机制 <br>
<br>
<h4>修复</h4>
1. 修复 forceUpdatedCache 属性设置,但本地缓存文件不更新的问题 <br>
2. 修复 PDF 解密加密文件转换成功后后台报错的问题 <br>
3. 修复 BPMN 不支持跨域的问题 <br>
4. 修复压缩包二级反代特殊符号错误问题 <br>
5. 修复视频跨域配置导致视频无法预览的问题 <br>
6. 修复 TXT 文本类分页二次加载问题 <br>
7. 修复 Drawio 缺少 Base64 组件的问题 <br>
8. 修复 Markdown 被转义问题 <br>
9. 修复 EPUB 跨域报错问题 <br>
10. 修复 URL 特殊符号问题 <br>
11. 修复压缩包穿越漏洞 <br>
12. 修复压缩获取路径错误、图片合集路径错误、水印问题等 BUG <br>
13. 修复前端解析 XLSX 包含 EMF 格式文件错误问题 <br>
</div>
</div>
</div>
<div class="panel panel-success">
<div class="panel-heading">
<h3 class="panel-title">2024年04月15日v4.4.0-beta版本</h3>
@@ -44,8 +101,8 @@
3. 修复 forceUpdatedCache 属性设置,但是本地缓存文件不更新缺陷 <br>
4. 配置文件新增启用 GZIP压缩 <br>
5. 升级CAD组件,CAD格式新增支持 转换成svg tif 格式 新增 超时结束 新增线程管理 <br>
6. 删除功能 新增验证码方法 <br>
7. office 功能调整 支持批注 转换页码限制 生成水印等等 <br>
6. 删除功能 新增验证码方法 <br>
7. office 功能调整 支持批注 转换页码限制 生成水印等等 <br>
8. 新增xbrl格式 <br>
9. 修复PDF解密加密文件 转换成功后台报错问题 <br>
10. 升级markdown组件 修复markdown被转义问题 <br>

View File

@@ -123,7 +123,7 @@
title: exportJson.info.name,
userInfo: exportJson.info.name.creator,
});
});
}, 1000);
}
loadText();
// 打印时获取luckysheet指定区域html内容拼接至div隐藏luckysheet容器并显示打印区域html

View File

@@ -21,12 +21,4 @@ public class WebUtilsTests {
String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0&param1=1";
assert WebUtils.encodeUrlFileName(in).equals(out);
}
@Test
void encodeUrlFullFileNameTestWithParams() {
// 测试对URL中使用fullfilename参数的文件名部分进行UTF-8编码
String in = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello#0.txt";
String out = "https://file.keking.cn/demo/download?param0=0&fullfilename=hello%230.txt";
assert WebUtils.encodeUrlFileName(in).equals(out);
}
}

View File

@@ -0,0 +1,92 @@
package cn.keking.web.filter;
import cn.keking.config.ConfigConstants;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
public class TrustHostFilterTests {
private final TrustHostFilter trustHostFilter = new TrustHostFilter();
@AfterEach
void tearDown() {
ConfigConstants.setTrustHostValue("default");
ConfigConstants.setNotTrustHostValue("default");
}
@Test
void shouldBlockWildcardNotTrustHostPattern() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("192.168.*");
assert trustHostFilter.isNotTrustHost("192.168.1.10");
assert !trustHostFilter.isNotTrustHost("8.8.8.8");
assert !trustHostFilter.isNotTrustHost("192.168.evil.com");
}
@Test
void shouldBlockCidrNotTrustHostPattern() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("10.0.0.0/8");
assert trustHostFilter.isNotTrustHost("10.1.2.3");
assert !trustHostFilter.isNotTrustHost("11.1.2.3");
// Ensure hostnames are not matched by CIDR-based not-trust rules (no DNS resolution)
assert !trustHostFilter.isNotTrustHost("localhost");
}
@Test
void shouldSupportHighBitIpv4InCidrMatching() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("200.0.0.0/8");
assert trustHostFilter.isNotTrustHost("200.1.2.3");
assert !trustHostFilter.isNotTrustHost("199.1.2.3");
}
@Test
void shouldSupportIpv4UpperBoundaryCidrMatching() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("255.255.255.255/32");
assert trustHostFilter.isNotTrustHost("255.255.255.255");
assert !trustHostFilter.isNotTrustHost("255.255.255.254");
}
@Test
void shouldDenyWhenHostIsBlankOrNull() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("default");
assert trustHostFilter.isNotTrustHost(null);
assert trustHostFilter.isNotTrustHost(" ");
}
@Test
void shouldAllowWildcardTrustHostPattern() {
ConfigConstants.setTrustHostValue("*.trusted.com");
ConfigConstants.setNotTrustHostValue("default");
assert !trustHostFilter.isNotTrustHost("api.trusted.com");
assert trustHostFilter.isNotTrustHost("api.evil.com");
}
@Test
void shouldKeepBlacklistHigherPriorityThanWhitelist() {
ConfigConstants.setTrustHostValue("*");
ConfigConstants.setNotTrustHostValue("127.0.0.1,10.*");
assert trustHostFilter.isNotTrustHost("127.0.0.1");
assert trustHostFilter.isNotTrustHost("10.1.2.3");
assert !trustHostFilter.isNotTrustHost("8.8.8.8");
}
@Test
void shouldStillEnforceWhitelistWhenBlacklistConfigured() {
ConfigConstants.setTrustHostValue("internal.example.com");
ConfigConstants.setNotTrustHostValue("127.0.0.1");
assert !trustHostFilter.isNotTrustHost("internal.example.com");
assert trustHostFilter.isNotTrustHost("8.8.8.8");
}
}

9
tests/e2e/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
node_modules/
playwright-report/
test-results/
__pycache__/
fixtures/archive-tmp/
fixtures/sample.docx
fixtures/sample.xlsx
fixtures/sample.pptx

69
tests/e2e/README.md Normal file
View File

@@ -0,0 +1,69 @@
# kkFileView E2E MVP
This folder contains a first MVP of end-to-end automated tests.
## What is covered
- Basic preview smoke checks for common file types (txt/md/json/xml/csv/html/png)
- Office Phase-2 smoke checks (docx/xlsx/pptx)
- Archive smoke checks (zip/tar/tgz/7z/rar)
- Basic endpoint reachability
- Security regression checks for blocked internal-network hosts (`10.*`) on:
- `/onlinePreview`
- `/getCorsFile`
- Basic performance smoke checks (configurable threshold): txt/docx/xlsx preview response time
- CI combined run command available via `npm run test:ci`
## Local run
1. Build server jar:
```bash
mvn -q -pl server -DskipTests package
```
2. Install deps + browser:
```bash
cd tests/e2e
npm install
npx playwright install --with-deps chromium
pip3 install -r requirements.txt
```
> Prerequisite: ensure `python3`, `zip`, and `7z` (or `bsdtar` as a fallback) are available in PATH for archive fixtures.
3. Generate fixtures and start fixture server:
```bash
cd /path/to/kkFileView
npm run gen:all
cd tests/e2e/fixtures && python3 -m http.server 18080
```
4. Start kkFileView in another terminal:
```bash
JAR_PATH=$(ls server/target/kkFileView-*.jar | head -n 1)
KK_TRUST_HOST='*' KK_NOT_TRUST_HOST='10.*,172.16.*,192.168.*' java -jar "$JAR_PATH"
```
5. Run tests:
```bash
cd tests/e2e
KK_BASE_URL=http://127.0.0.1:8012 FIXTURE_BASE_URL=http://127.0.0.1:18080 npm test
```
Optional:
```bash
# smoke only (self-contained: will auto-generate fixtures)
npm run test:smoke
# perf smoke (self-contained; default threshold 15000ms)
E2E_MAX_PREVIEW_MS=15000 npm run test:perf
# CI-style combined run (single fixture generation)
E2E_MAX_PREVIEW_MS=20000 npm run test:ci
```

Binary file not shown.

View File

@@ -0,0 +1,3 @@
name,value
kkFileView,1
e2e,1
1 name value
2 kkFileView 1
3 e2e 1

View File

@@ -0,0 +1 @@
<!doctype html><html><body><h1>kkFileView fixture</h1></body></html>

View File

@@ -0,0 +1,4 @@
{
"app": "kkFileView",
"e2e": true
}

View File

@@ -0,0 +1,3 @@
# kkFileView
This is a markdown fixture.

View File

@@ -0,0 +1,19 @@
%PDF-1.1
1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj
2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj
3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Contents 4 0 R >>endobj
4 0 obj<< /Length 44 >>stream
BT /F1 12 Tf 72 120 Td (kkFileView e2e pdf) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000010 00000 n
0000000060 00000 n
0000000117 00000 n
0000000212 00000 n
trailer<< /Root 1 0 R /Size 5 >>
startxref
306
%%EOF

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
kkFileView e2e sample text

View File

@@ -0,0 +1 @@
<root><name>kkFileView</name><e2e>true</e2e></root>

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