Compare commits
47 Commits
v4.4.0-bet
...
copilot/su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d55f811ab | ||
|
|
30127aae7c | ||
|
|
3b0f7af382 | ||
|
|
7f6ad472c4 | ||
|
|
8f9dda5a8d | ||
|
|
a0d78c57e3 | ||
|
|
7f16243270 | ||
|
|
36a75e86ac | ||
|
|
7c41200028 | ||
|
|
3da0c523e8 | ||
|
|
8c3bc81e08 | ||
|
|
92ca92bee6 | ||
|
|
64c82a2406 | ||
|
|
e44ef813a1 | ||
|
|
9f3b45a4c7 | ||
|
|
b1af0c7d72 | ||
|
|
421640221b | ||
|
|
f6c6e22b0d | ||
|
|
51653483b9 | ||
|
|
a9787b0add | ||
|
|
6cdbf92fb0 | ||
|
|
fdb40680d3 | ||
|
|
6746325bf2 | ||
|
|
05a8bff1e0 | ||
|
|
874ff5b3f6 | ||
|
|
2230cfa52b | ||
|
|
83c581364d | ||
|
|
1d39360c60 | ||
|
|
8c763599fe | ||
|
|
9632f6070c | ||
|
|
cc63659650 | ||
|
|
177f389814 | ||
|
|
406e9ea6ee | ||
|
|
bb461cd74a | ||
|
|
782509376c | ||
|
|
63dc58d088 | ||
|
|
77f5adb19f | ||
|
|
7abfb67451 | ||
|
|
c8dc638c29 | ||
|
|
48ac926289 | ||
|
|
0a4ae41b0c | ||
|
|
bb0139bee6 | ||
|
|
7bf07cb64c | ||
|
|
ab370e66a5 | ||
|
|
421a2760d5 | ||
|
|
9150346926 | ||
|
|
b65a04857c |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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. / 涉及敏感安全问题请使用私密安全报告。"
|
||||||
76
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal 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
@@ -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
|
||||||
78
.github/workflows/auto-close-old-issues.yml
vendored
Normal 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.`);
|
||||||
34
.github/workflows/copilot-issue-auto-comment.yml
vendored
Normal 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}`);
|
||||||
42
.github/workflows/maven.yml
vendored
@@ -12,13 +12,41 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout repository
|
||||||
- name: Set up JDK 8
|
uses: actions/checkout@v4
|
||||||
uses: actions/setup-java@v2
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: '8'
|
java-version: '21'
|
||||||
distribution: 'adopt'
|
distribution: 'temurin' # 使用 Eclipse Temurin (AdoptOpenJDK 的继任者)
|
||||||
cache: maven
|
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
|
- 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
|
||||||
|
|||||||
100
.github/workflows/pr-e2e-mvp.yml
vendored
Normal 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 + zip
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libreoffice zip
|
||||||
|
|
||||||
|
- 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
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
FROM keking/kkfileview-jdk:latest
|
FROM keking/kkfileview-base:4.4.0
|
||||||
MAINTAINER chenjh "842761733@qq.com"
|
|
||||||
ADD server/target/kkFileView-*.tar.gz /opt/
|
ADD server/target/kkFileView-*.tar.gz /opt/
|
||||||
ENV KKFILEVIEW_BIN_FOLDER /opt/kkFileView-4.4.0-beta/bin
|
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-4.4.0/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"]
|
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"]
|
||||||
|
|||||||
94
README.cn.md
@@ -53,80 +53,81 @@
|
|||||||
#### 1. 文本预览
|
#### 1. 文本预览
|
||||||
支持所有类型的文本文档预览, 由于文本文档类型过多,无法全部枚举,默认开启的类型如下 txt,html,htm,asp,jsp,xml,xbrl,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
|
支持所有类型的文本文档预览, 由于文本文档类型过多,无法全部枚举,默认开启的类型如下 txt,html,htm,asp,jsp,xml,xbrl,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
|
||||||
文本预览效果如下
|
文本预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 2. 图片预览
|
#### 2. 图片预览
|
||||||
支持jpg,jpeg,png,gif等图片预览(翻转,缩放,镜像),预览效果如下
|
支持jpg,jpeg,png,gif等图片预览(翻转,缩放,镜像),预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 3. word文档预览
|
#### 3. word文档预览
|
||||||
支持doc,docx文档预览,word预览有两种模式:一种是每页word转为图片预览,另一种是整个word文档转成pdf,再预览pdf。两种模式的适用场景如下
|
支持doc,docx文档预览,word预览有两种模式:一种是每页word转为图片预览,另一种是整个word文档转成pdf,再预览pdf。两种模式的适用场景如下
|
||||||
* 图片预览:word文件大,前台加载整个pdf过慢
|
* 图片预览:word文件大,前台加载整个pdf过慢
|
||||||
* pdf预览:内网访问,加载pdf快
|
* pdf预览:内网访问,加载pdf快
|
||||||
图片预览模式预览效果如下
|
图片预览模式预览效果如下
|
||||||

|

|
||||||
pdf预览模式预览效果如下
|
pdf预览模式预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 4. ppt文档预览
|
#### 4. ppt文档预览
|
||||||
支持ppt,pptx文档预览,和word文档一样,有两种预览模式
|
支持ppt,pptx文档预览,和word文档一样,有两种预览模式
|
||||||
图片预览模式预览效果如下
|
图片预览模式预览效果如下
|
||||||

|

|
||||||
pdf预览模式预览效果如下
|
pdf预览模式预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 5. pdf文档预览
|
#### 5. pdf文档预览
|
||||||
支持pdf文档预览,和word文档一样,有两种预览模式
|
支持pdf文档预览,和word文档一样,有两种预览模式
|
||||||
图片预览模式预览效果如下
|
图片预览模式预览效果如下
|
||||||

|

|
||||||
pdf预览模式预览效果如下
|
pdf预览模式预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 6. excel文档预览
|
#### 6. excel文档预览
|
||||||
支持xls,xlsx文档预览,预览效果如下
|
支持xls,xlsx文档预览,预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 7. 压缩文件预览
|
#### 7. 压缩文件预览
|
||||||
支持zip,rar,jar,tar,gzip等压缩包,预览效果如下
|
支持zip,rar,jar,tar,gzip等压缩包,预览效果如下
|
||||||

|

|
||||||
可点击压缩包中的文件名,直接预览文件,预览效果如下
|
可点击压缩包中的文件名,直接预览文件,预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 8. 多媒体文件预览
|
#### 8. 多媒体文件预览
|
||||||
理论上支持所有的视频、音频文件,由于无法枚举所有文件格式,默认开启的类型如下
|
理论上支持所有的视频、音频文件,由于无法枚举所有文件格式,默认开启的类型如下
|
||||||
mp3,wav,mp4,flv
|
mp3,wav,mp4,flv
|
||||||
视频预览效果如下
|
视频预览效果如下
|
||||||

|

|
||||||
音频预览效果如下
|
音频预览效果如下
|
||||||

|

|
||||||
|
|
||||||
#### 9. CAD文档预览
|
#### 9. CAD文档预览
|
||||||
支持CAD dwg文档预览,和word文档一样,有两种预览模式
|
支持CAD dwg文档预览,和word文档一样,有两种预览模式
|
||||||
图片预览模式预览效果如下
|
图片预览模式预览效果如下
|
||||||

|

|
||||||
pdf预览模式预览效果如下
|
pdf预览模式预览效果如下
|
||||||

|

|
||||||
考虑说明篇幅原因,就不贴其他格式文件的预览效果了,感兴趣的可以参考下面的实例搭建下
|
|
||||||
|
|
||||||
#### 10. Excel文件纯前端渲染效果
|
#### 10. Excel文件纯前端渲染效果
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 11. 流程图bpmn文件预览效果
|
#### 11. 流程图bpmn文件预览效果
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 12. 3D模型文件预览效果:
|
#### 12. 3D模型文件预览效果:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 13. dcm医疗数位影像文件预览效果:
|
#### 13. dcm医疗数位影像文件预览效果:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 14. drawio流程图预览效果:
|
#### 14. drawio流程图预览效果:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
考虑说明篇幅原因,就不贴其他格式文件的预览效果了,感兴趣的可以参考下面的实例搭建下
|
||||||
|
|
||||||
### 快速开始
|
### 快速开始
|
||||||
> 项目使用技术
|
> 项目使用技术
|
||||||
@@ -148,7 +149,56 @@ pdf预览模式预览效果如下
|
|||||||
|
|
||||||
### 历史更新记录
|
### 历史更新记录
|
||||||
|
|
||||||
#### > 2023年07月05日,v4.3 版本发布 :
|
#### > 2025年01月16日,v4.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等医疗数位影像预
|
1. 新增dcm等医疗数位影像预
|
||||||
|
|||||||
174
SECURITY_CONFIG.md
Normal 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**安全提示**:定期检查和更新信任主机列表,遵循最小权限原则。
|
||||||
BIN
doc/img/preview/preview-3ds.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
doc/img/preview/preview-audio.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
doc/img/preview/preview-bpmn.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
doc/img/preview/preview-cad-image.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
doc/img/preview/preview-cad-pdf.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
doc/img/preview/preview-dcm.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
doc/img/preview/preview-doc-image.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
doc/img/preview/preview-doc-pdf.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
doc/img/preview/preview-drawio.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
doc/img/preview/preview-image.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
doc/img/preview/preview-pdf-image.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
doc/img/preview/preview-pdf-pdf.png
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
doc/img/preview/preview-ppt-image.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
doc/img/preview/preview-ppt-pdf.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
doc/img/preview/preview-text.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/img/preview/preview-video.png
Normal file
|
After Width: | Height: | Size: 469 KiB |
BIN
doc/img/preview/preview-xls.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
doc/img/preview/preview-xlsx-web.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
doc/img/preview/preview-xmind.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
doc/img/preview/preview-zip-inner.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
doc/img/preview/preview-zip.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
28
docker/kkfileview-base/Dockerfile
Normal 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
|
||||||
50
docker/kkfileview-base/README.cn.md
Normal 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 .
|
||||||
|
```
|
||||||
53
docker/kkfileview-base/README.md
Normal 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. Here’s an example build command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker buildx build --platform=linux/amd64,linux/arm64 -t keking/kkfileview-base:4.4.0 --push .
|
||||||
|
```
|
||||||
@@ -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"]
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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
@@ -6,23 +6,23 @@
|
|||||||
|
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<version>4.4.0-beta</version>
|
<version>4.4.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>1.8</java.version>
|
<java.version>21</java.version>
|
||||||
<jodconverter.version>4.4.6</jodconverter.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>
|
<poi.version>5.2.2</poi.version>
|
||||||
<xdocreport.version>1.0.6</xdocreport.version>
|
<xdocreport.version>1.0.6</xdocreport.version>
|
||||||
<xstream.version>1.4.20</xstream.version>
|
<xstream.version>1.4.20</xstream.version>
|
||||||
<junrar.version>7.5.5</junrar.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>
|
<sevenzipjbinding.version>16.02-2.01</sevenzipjbinding.version>
|
||||||
<jchardet.version>1.0</jchardet.version>
|
<jchardet.version>1.0</jchardet.version>
|
||||||
<antlr.version>2.7.7</antlr.version>
|
<antlr.version>2.7.7</antlr.version>
|
||||||
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
|
<concurrentlinkedhashmap.version>1.4.2</concurrentlinkedhashmap.version>
|
||||||
<rocksdb.version>5.17.2</rocksdb.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>
|
<jai-imageio.version>1.4.0</jai-imageio.version>
|
||||||
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
|
<jbig2-imageio.version>3.0.4</jbig2-imageio.version>
|
||||||
<galimatias.version>0.2.1</galimatias.version>
|
<galimatias.version>0.2.1</galimatias.version>
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
|
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<version>4.4.0-beta</version>
|
<version>4.4.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>kkFileView</artifactId>
|
<artifactId>kkFileView</artifactId>
|
||||||
@@ -44,21 +44,16 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<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>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
<!-- web end -->
|
<!-- web end -->
|
||||||
|
|
||||||
<!-- poi start -->
|
<!-- poi start -->
|
||||||
@@ -100,9 +95,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<!-- poi start -->
|
<!-- poi start -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents</groupId>
|
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||||
<artifactId>httpclient</artifactId>
|
<artifactId>httpclient5</artifactId>
|
||||||
<version>${httpcomponents.version}</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 对 rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
|
<!-- 对 rar5 的支持 和其他众多压缩支持 可参考 package net.sf.sevenzipjbinding.ArchiveFormat; -->
|
||||||
@@ -182,6 +176,12 @@
|
|||||||
<groupId>org.apache.pdfbox</groupId>
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
<artifactId>pdfbox</artifactId>
|
<artifactId>pdfbox</artifactId>
|
||||||
<version>${pdfbox.version}</version>
|
<version>${pdfbox.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>commons-logging</artifactId>
|
||||||
|
<groupId>commons-logging</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.pdfbox</groupId>
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
@@ -327,6 +327,14 @@
|
|||||||
</resource>
|
</resource>
|
||||||
</resources>
|
</resources>
|
||||||
<plugins>
|
<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>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ install_redhat() {
|
|||||||
yum install -y libSM.x86_64 libXrender.x86_64 libXext.x86_64
|
yum install -y libSM.x86_64 libXrender.x86_64 libXext.x86_64
|
||||||
yum groupinstall -y "X Window System"
|
yum groupinstall -y "X Window System"
|
||||||
yum localinstall -y *.rpm
|
yum localinstall -y *.rpm
|
||||||
echo 'install finshed...'
|
echo 'install finished...'
|
||||||
else
|
else
|
||||||
echo 'download package error...'
|
echo 'download package error...'
|
||||||
fi
|
fi
|
||||||
@@ -20,7 +20,7 @@ install_ubuntu() {
|
|||||||
if [ $? -eq 0 ];then
|
if [ $? -eq 0 ];then
|
||||||
apt-get install -y libxinerama1 libcairo2 libcups2 libx11-xcb1
|
apt-get install -y libxinerama1 libcairo2 libcups2 libx11-xcb1
|
||||||
dpkg -i *.deb
|
dpkg -i *.deb
|
||||||
echo 'install finshed...'
|
echo 'install finished...'
|
||||||
else
|
else
|
||||||
echo 'download package error...'
|
echo 'download package error...'
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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 You can get help in our official home site: https://kkview.cn
|
||||||
echo If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ
|
echo If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ
|
||||||
echo If this project is helpful to you, please star it on https://gitee.com/kekingcn/file-online-preview/stargazers
|
echo If this project is helpful to you, please star it on https://gitee.com/kekingcn/file-online-preview/stargazers
|
||||||
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0-beta.jar -> ..\log\kkFileView.log
|
java -Dspring.config.location=..\config\application.properties -jar kkFileView-4.4.0.jar -> ..\log\kkFileView.log
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# Description: v1.1:修改进程启动机制为pid形式。
|
# 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=
|
FLAG=
|
||||||
OFFICE_HOME=
|
OFFICE_HOME=
|
||||||
KKFILEVIEW_BIN_FOLDER=$(cd "$(dirname "$0")" || exit 1 ;pwd)
|
KKFILEVIEW_BIN_FOLDER=$(cd "$(dirname "$0")" || exit 1 ;pwd)
|
||||||
@@ -29,7 +29,7 @@ if [ -s "${PID_FILE}" ]; then
|
|||||||
else
|
else
|
||||||
cd "$KKFILEVIEW_BIN_FOLDER" || exit 1
|
cd "$KKFILEVIEW_BIN_FOLDER" || exit 1
|
||||||
echo "Using KKFILEVIEW_BIN_FOLDER $KKFILEVIEW_BIN_FOLDER"
|
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
|
if [ $? -eq 0 ]; then
|
||||||
echo "Using customized office.home"
|
echo "Using customized office.home"
|
||||||
else
|
else
|
||||||
@@ -51,7 +51,7 @@ else
|
|||||||
|
|
||||||
## 启动kkFileView
|
## 启动kkFileView
|
||||||
echo "Starting kkFileView..."
|
echo "Starting kkFileView..."
|
||||||
nohup java -Dfile.encoding=UTF-8 -Dspring.config.location=../config/application.properties -jar kkFileView-4.4.0-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 "Please execute ./showlog.sh to check log for more information"
|
||||||
echo "You can get help in our official home site: https://kkview.cn"
|
echo "You can get help in our official home site: https://kkview.cn"
|
||||||
echo "If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ"
|
echo "If you need further help, please join our kk opensource community: https://t.zsxq.com/09ZHSXbsQ"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ server.port = ${KK_SERVER_PORT:8012}
|
|||||||
server.servlet.context-path= ${KK_CONTEXT_PATH:/}
|
server.servlet.context-path= ${KK_CONTEXT_PATH:/}
|
||||||
server.servlet.encoding.charset = utf-8
|
server.servlet.encoding.charset = utf-8
|
||||||
#启用GZIP压缩功能
|
#启用GZIP压缩功能
|
||||||
server.compression.enable= true
|
server.compression.enabled = true
|
||||||
#允许压缩的响应缓冲区最小字节数,默认2048
|
#允许压缩的响应缓冲区最小字节数,默认2048
|
||||||
server.compression.min-response-size = 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.expose-session-attributes = true
|
||||||
spring.freemarker.request-context-attribute = request
|
spring.freemarker.request-context-attribute = request
|
||||||
spring.freemarker.suffix = .ftl
|
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设置
|
# office设置
|
||||||
#openoffice或LibreOffice home路径
|
#openoffice或LibreOffice home路径
|
||||||
@@ -78,11 +86,25 @@ cache.clean.cron = ${KK_CACHE_CLEAN_CRON:0 0 3 * * ?}
|
|||||||
#提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置
|
#提供预览服务的地址,默认从请求url读,如果使用nginx等反向代理,需要手动设置
|
||||||
#base.url = https://file.keking.cn
|
#base.url = https://file.keking.cn
|
||||||
base.url = ${KK_BASE_URL:default}
|
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}
|
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}
|
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}
|
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预览模块设置
|
||||||
#配置PDF文件生成图片的像素大小,dpi 越高,图片质量越清晰,同时也会消耗更多的计算资源。
|
#配置PDF文件生成图片的像素大小,dpi 越高,图片质量越清晰,同时也会消耗更多的计算资源。
|
||||||
pdf2jpg.dpi = ${KK_PDF2JPG_DPI:144}
|
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}
|
pdf.presentationMode.disable = ${KK_PDF_PRESENTATION_MODE_DISABLE:true}
|
||||||
#是否禁止打开文件
|
#是否禁止打开文件
|
||||||
@@ -151,10 +181,9 @@ watermark.height = ${WATERMARK_HEIGHT:80}
|
|||||||
#水印倾斜度数,要求设置在大于等于0,小于90
|
#水印倾斜度数,要求设置在大于等于0,小于90
|
||||||
watermark.angle = ${WATERMARK_ANGLE:10}
|
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}
|
beian = ${KK_BEIAN:default}
|
||||||
#禁止上传类型
|
#禁止上传类型
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ public class ConfigConstants {
|
|||||||
private static String homePagination;
|
private static String homePagination;
|
||||||
private static String homePageSize;
|
private static String homePageSize;
|
||||||
private static String homeSearch;
|
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_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";
|
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_PAGINATION = "true";
|
||||||
public static final String DEFAULT_HOME_PAGSIZE = "15";
|
public static final String DEFAULT_HOME_PAGSIZE = "15";
|
||||||
public static final String DEFAULT_HOME_SEARCH = "true";
|
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() {
|
public static Boolean isCacheEnabled() {
|
||||||
return cacheEnabled;
|
return cacheEnabled;
|
||||||
@@ -300,7 +308,8 @@ public class ConfigConstants {
|
|||||||
if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) {
|
if (DEFAULT_VALUE.equalsIgnoreCase(trustHost)) {
|
||||||
return new CopyOnWriteArraySet<>();
|
return new CopyOnWriteArraySet<>();
|
||||||
} else {
|
} else {
|
||||||
String[] trustHostArray = trustHost.toLowerCase().split(",");
|
// 去除空格并转小写
|
||||||
|
String[] trustHostArray = trustHost.toLowerCase().replaceAll("\\s+", "").split(",");
|
||||||
return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray));
|
return new CopyOnWriteArraySet<>(Arrays.asList(trustHostArray));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +427,7 @@ public class ConfigConstants {
|
|||||||
return fileUploadDisable;
|
return fileUploadDisable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value("${file.upload.disable:false}")
|
@Value("${file.upload.disable:true}")
|
||||||
public void setFileUploadDisable(Boolean fileUploadDisable) {
|
public void setFileUploadDisable(Boolean fileUploadDisable) {
|
||||||
setFileUploadDisableValue(fileUploadDisable);
|
setFileUploadDisableValue(fileUploadDisable);
|
||||||
}
|
}
|
||||||
@@ -580,6 +589,65 @@ public class ConfigConstants {
|
|||||||
ConfigConstants.cadThread = cadThread;
|
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转换模块设置
|
* 以下为OFFICE转换模块设置
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -78,6 +78,10 @@ public class ConfigRefreshComponent {
|
|||||||
String homePagination;
|
String homePagination;
|
||||||
String homePageSize;
|
String homePageSize;
|
||||||
String homeSearch;
|
String homeSearch;
|
||||||
|
int pdfTimeout;
|
||||||
|
int pdfTimeout80;
|
||||||
|
int pdfTimeout200;
|
||||||
|
int pdfThread;
|
||||||
while (true) {
|
while (true) {
|
||||||
FileReader fileReader = new FileReader(configFilePath);
|
FileReader fileReader = new FileReader(configFilePath);
|
||||||
BufferedReader bufferedReader = new BufferedReader(fileReader);
|
BufferedReader bufferedReader = new BufferedReader(fileReader);
|
||||||
@@ -126,6 +130,10 @@ public class ConfigRefreshComponent {
|
|||||||
homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
|
homePageSize = properties.getProperty("home.pagesize", ConfigConstants.DEFAULT_HOME_PAGSIZE);
|
||||||
homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
|
homeSearch = properties.getProperty("home.search", ConfigConstants.DEFAULT_HOME_SEARCH);
|
||||||
cadThread = Integer.parseInt(properties.getProperty("cad.thread", ConfigConstants.DEFAULT_CAD_THREAD));
|
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(",");
|
prohibitArray = prohibit.split(",");
|
||||||
|
|
||||||
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
|
ConfigConstants.setCacheEnabledValueValue(cacheEnabled);
|
||||||
@@ -169,6 +177,10 @@ public class ConfigRefreshComponent {
|
|||||||
ConfigConstants.setHomePaginationValue(homePagination);
|
ConfigConstants.setHomePaginationValue(homePagination);
|
||||||
ConfigConstants.setHomePageSizeValue(homePageSize);
|
ConfigConstants.setHomePageSizeValue(homePageSize);
|
||||||
ConfigConstants.setHomeSearchValue(homeSearch);
|
ConfigConstants.setHomeSearchValue(homeSearch);
|
||||||
|
ConfigConstants.setPdfTimeoutValue(pdfTimeout);
|
||||||
|
ConfigConstants.setPdfTimeout80Value(pdfTimeout80);
|
||||||
|
ConfigConstants.setPdfTimeout200Value(pdfTimeout200);
|
||||||
|
ConfigConstants.setPdfThreadValue(pdfThread);
|
||||||
setWatermarkConfig(properties);
|
setWatermarkConfig(properties);
|
||||||
bufferedReader.close();
|
bufferedReader.close();
|
||||||
fileReader.close();
|
fileReader.close();
|
||||||
|
|||||||
@@ -50,26 +50,21 @@ public class RedissonConfig {
|
|||||||
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
.setConnectionMinimumIdleSize(connectionMinimumIdleSize)
|
||||||
.setConnectionPoolSize(connectionPoolSize)
|
.setConnectionPoolSize(connectionPoolSize)
|
||||||
.setDatabase(database)
|
.setDatabase(database)
|
||||||
.setDnsMonitoring(dnsMonitoring)
|
|
||||||
.setDnsMonitoringInterval(dnsMonitoringInterval)
|
.setDnsMonitoringInterval(dnsMonitoringInterval)
|
||||||
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
.setSubscriptionConnectionMinimumIdleSize(subscriptionConnectionMinimumIdleSize)
|
||||||
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
.setSubscriptionConnectionPoolSize(subscriptionConnectionPoolSize)
|
||||||
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
.setSubscriptionsPerConnection(subscriptionsPerConnection)
|
||||||
.setClientName(clientName)
|
.setClientName(clientName)
|
||||||
.setFailedAttempts(failedAttempts)
|
|
||||||
.setRetryAttempts(retryAttempts)
|
.setRetryAttempts(retryAttempts)
|
||||||
.setRetryInterval(retryInterval)
|
.setRetryInterval(retryInterval)
|
||||||
.setReconnectionTimeout(reconnectionTimeout)
|
|
||||||
.setTimeout(timeout)
|
.setTimeout(timeout)
|
||||||
.setConnectTimeout(connectTimeout)
|
.setConnectTimeout(connectTimeout)
|
||||||
.setIdleConnectionTimeout(idleConnectionTimeout)
|
.setIdleConnectionTimeout(idleConnectionTimeout)
|
||||||
.setPingTimeout(pingTimeout)
|
|
||||||
.setPassword(StringUtils.trimToNull(password));
|
.setPassword(StringUtils.trimToNull(password));
|
||||||
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
|
Codec codec=(Codec) ClassUtils.forName(getCodec(), ClassUtils.getDefaultClassLoader()).newInstance();
|
||||||
config.setCodec(codec);
|
config.setCodec(codec);
|
||||||
config.setThreads(thread);
|
config.setThreads(thread);
|
||||||
config.setEventLoopGroup(new NioEventLoopGroup());
|
config.setEventLoopGroup(new NioEventLoopGroup());
|
||||||
config.setUseLinuxNativeEpoll(false);
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public enum FileType {
|
|||||||
MEDIACONVERT("mediaFilePreviewImpl"),
|
MEDIACONVERT("mediaFilePreviewImpl"),
|
||||||
MARKDOWN("markdownFilePreviewImpl"),
|
MARKDOWN("markdownFilePreviewImpl"),
|
||||||
XML("xmlFilePreviewImpl"),
|
XML("xmlFilePreviewImpl"),
|
||||||
|
JSON("jsonFilePreviewImpl"),
|
||||||
CAD("cadFilePreviewImpl"),
|
CAD("cadFilePreviewImpl"),
|
||||||
TIFF("tiffFilePreviewImpl"),
|
TIFF("tiffFilePreviewImpl"),
|
||||||
OFD("ofdFilePreviewImpl"),
|
OFD("ofdFilePreviewImpl"),
|
||||||
@@ -44,12 +45,13 @@ public enum FileType {
|
|||||||
private static final String[] DCM_TYPES = {"dcm"};
|
private static final String[] DCM_TYPES = {"dcm"};
|
||||||
private static final String[] DRAWIO_TYPES = {"drawio"};
|
private static final String[] DRAWIO_TYPES = {"drawio"};
|
||||||
private static final String[] XML_TYPES = {"xml","xbrl"};
|
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[] TIFF_TYPES = {"tif", "tiff"};
|
||||||
private static final String[] OFD_TYPES = {"ofd"};
|
private static final String[] OFD_TYPES = {"ofd"};
|
||||||
private static final String[] SVG_TYPES = {"svg"};
|
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[] 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[] 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();
|
private static final String[] MEDIA_TYPES = ConfigConstants.getMedia();
|
||||||
public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias();
|
public static final String[] MEDIA_CONVERT_TYPES = ConfigConstants.getConvertMedias();
|
||||||
private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
|
private static final Map<String, FileType> FILE_TYPE_MAPPER = new HashMap<>();
|
||||||
@@ -109,6 +111,9 @@ public enum FileType {
|
|||||||
for (String xml : XML_TYPES) {
|
for (String xml : XML_TYPES) {
|
||||||
FILE_TYPE_MAPPER.put(xml, FileType.XML);
|
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("md", FileType.MARKDOWN);
|
||||||
FILE_TYPE_MAPPER.put("pdf", FileType.PDF);
|
FILE_TYPE_MAPPER.put("pdf", FileType.PDF);
|
||||||
FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN);
|
FILE_TYPE_MAPPER.put("bpmn", FileType.BPMN);
|
||||||
|
|||||||
@@ -12,12 +12,19 @@ import net.sf.sevenzipjbinding.SevenZipException;
|
|||||||
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;
|
||||||
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
|
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
|
||||||
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
|
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.springframework.stereotype.Component;
|
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.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -37,73 +44,70 @@ public class CompressFileReader {
|
|||||||
public String unRar(String filePath, String filePassword, String fileName, FileAttribute fileAttribute) throws Exception {
|
public String unRar(String filePath, String filePassword, String fileName, FileAttribute fileAttribute) throws Exception {
|
||||||
List<String> imgUrls = new ArrayList<>();
|
List<String> imgUrls = new ArrayList<>();
|
||||||
String baseUrl = BaseUrlFilter.getBaseUrl();
|
String baseUrl = BaseUrlFilter.getBaseUrl();
|
||||||
String packagePath = "_"; //防止文件名重复 压缩包统一生成文件添加_符号
|
String packagePath = "_";
|
||||||
String folderName = filePath.replace(fileDir, ""); //修复压缩包 多重目录获取路径错误
|
String folderName = filePath.replace(fileDir, ""); //修复压缩包 多重目录获取路径错误
|
||||||
if (fileAttribute.isCompressFile()) { //压缩包文件 直接赋予路径 不予下载
|
if (fileAttribute.isCompressFile()) {
|
||||||
folderName = "_decompression" + folderName; //重新修改多重压缩包 生成文件路径
|
folderName = "_decompression" + folderName;
|
||||||
}
|
}
|
||||||
RandomAccessFile randomAccessFile = null;
|
Path folderPath = Paths.get(fileDir, folderName + packagePath);
|
||||||
IInArchive inArchive = null;
|
Files.createDirectories(folderPath);
|
||||||
try {
|
|
||||||
randomAccessFile = new RandomAccessFile(filePath, "r");
|
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
|
||||||
inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile));
|
IInArchive inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile))) {
|
||||||
|
|
||||||
ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface();
|
ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface();
|
||||||
final String[] str = {null};
|
|
||||||
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
|
for (final ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
|
||||||
if (!item.isFolder()) {
|
if (!item.isFolder()) {
|
||||||
ExtractOperationResult result;
|
final Path filePathInsideArchive = getFilePathInsideArchive(item, folderPath);
|
||||||
String finalFolderName = folderName;
|
ExtractOperationResult result = item.extractSlow(data -> {
|
||||||
result = item.extractSlow(data -> {
|
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(filePathInsideArchive.toFile(), true))) {
|
||||||
try {
|
out.write(data);
|
||||||
str[0] = RarUtils.getUtf8String(item.getPath());
|
} catch (IOException e) {
|
||||||
if (RarUtils.isMessyCode(str[0])) {
|
throw new RuntimeException(e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
return data.length;
|
return data.length;
|
||||||
}, filePassword);
|
}, filePassword);
|
||||||
if (result == ExtractOperationResult.OK) {
|
if (result != ExtractOperationResult.OK) {
|
||||||
FileType type = FileType.typeFromUrl(str[0]);
|
ExtractOperationResult result1 = ExtractOperationResult.valueOf("WRONG_PASSWORD");
|
||||||
if (type.equals(FileType.PICTURE)) {
|
if (result1.equals(result)) {
|
||||||
imgUrls.add(baseUrl + folderName + packagePath + "/" + str[0].replace("\\", "/"));
|
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) {
|
} catch (Exception e) {
|
||||||
throw new Exception(e);
|
throw new Exception("Error processing RAR file: " + e.getMessage(), 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.ExtendedModelMap;
|
import org.springframework.ui.ExtendedModelMap;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +73,7 @@ public class FileConvertQueueTask {
|
|||||||
TimeUnit.SECONDS.sleep(10);
|
TimeUnit.SECONDS.sleep(10);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
ex.printStackTrace();
|
logger.error("Failed to sleep after exception", ex);
|
||||||
}
|
}
|
||||||
logger.info("处理预览转换任务异常,url:{}", url, e);
|
logger.info("处理预览转换任务异常,url:{}", url, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import com.aspose.cad.*;
|
|||||||
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
|
import com.aspose.cad.fileformats.cad.CadDrawTypeMode;
|
||||||
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
|
import com.aspose.cad.fileformats.tiff.enums.TiffExpectedFormat;
|
||||||
import com.aspose.cad.imageoptions.*;
|
import com.aspose.cad.imageoptions.*;
|
||||||
import com.itextpdf.text.pdf.PdfReader;
|
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
@@ -31,13 +31,16 @@ import org.springframework.util.CollectionUtils;
|
|||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.concurrent.*;
|
||||||
import java.util.stream.IntStream;
|
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("<script src=\"excel/excel.header.js\" type=\"text/javascript\"></script>");
|
||||||
sb.append("<link rel=\"stylesheet\" href=\"excel/excel.css\">");
|
sb.append("<link rel=\"stylesheet\" href=\"excel/excel.css\">");
|
||||||
} catch (IOException e) {
|
} 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))) {
|
try (FileOutputStream fos = new FileOutputStream(outFilePath); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) {
|
||||||
writer.write(sb.toString());
|
writer.write(sb.toString());
|
||||||
} catch (IOException e) {
|
} 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 forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
|
boolean usePasswordCache = fileAttribute.getUsePasswordCache();
|
||||||
String filePassword = fileAttribute.getFilePassword();
|
String filePassword = fileAttribute.getFilePassword();
|
||||||
String pdfPassword = null;
|
PDDocument doc;
|
||||||
PDDocument doc = null;
|
final String[] pdfPassword = {null};
|
||||||
PdfReader pdfReader = null;
|
final int[] pageCount = new int[1];
|
||||||
if (!forceUpdatedCache) {
|
if (!forceUpdatedCache) {
|
||||||
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
|
List<String> cacheResult = this.loadPdf2jpgCache(pdfFilePath);
|
||||||
if (!CollectionUtils.isEmpty(cacheResult)) {
|
if (!CollectionUtils.isEmpty(cacheResult)) {
|
||||||
@@ -246,64 +249,77 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> imageUrls = new ArrayList<>();
|
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 {
|
try {
|
||||||
File pdfFile = new File(fileNameFilePath);
|
doc = Loader.loadPDF(pdfFile, filePassword);
|
||||||
if (!pdfFile.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
doc = PDDocument.load(pdfFile, filePassword);
|
|
||||||
doc.setResourceCache(new NotResourceCache());
|
doc.setResourceCache(new NotResourceCache());
|
||||||
int pageCount = doc.getNumberOfPages();
|
pageCount[0] = doc.getNumberOfPages();
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(doc);
|
} catch (IOException e) {
|
||||||
int index = pdfFilePath.lastIndexOf(".");
|
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
||||||
String folder = pdfFilePath.substring(0, index);
|
for (Throwable throwable : throwableArray) {
|
||||||
File path = new File(folder);
|
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
||||||
if (!path.exists() && !path.mkdirs()) {
|
if (e.getMessage().toLowerCase().contains(PDF_PASSWORD_MSG)) {
|
||||||
logger.error("创建转换文件【{}】目录失败,请检查目录权限!", folder);
|
pdfPassword[0] = PDF_PASSWORD_MSG; //查询到该文件是密码文件 输出带密码的值
|
||||||
}
|
|
||||||
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; //查询到该文件是密码文件 输出带密码的值
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!PDF_PASSWORD_MSG.equals(pdfPassword)) { //该文件异常 错误原因非密码原因输出错误
|
|
||||||
logger.error("Convert pdf exception, pdfFilePath:{}", pdfFilePath, e);
|
|
||||||
}
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (pdfReader != null) { //关闭
|
|
||||||
pdfReader.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!PDF_PASSWORD_MSG.equals(pdfPassword[0])) { //该文件异常 错误原因非密码原因输出错误
|
||||||
if (usePasswordCache || !PDF_PASSWORD_MSG.equals(pdfPassword)) { //加密文件 判断是否启用缓存命令
|
logger.error("Convert pdf exception, pdfFilePath:{}", pdfFilePath, e);
|
||||||
this.addPdf2jpgCache(pdfFilePath, pageCount);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (!e.getMessage().contains(PDF_PASSWORD_MSG)) {
|
|
||||||
logger.error("Convert pdf to jpg exception, pdfFilePath:{}", pdfFilePath, e);
|
|
||||||
}
|
}
|
||||||
throw new Exception(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();
|
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;
|
return imageUrls;
|
||||||
}
|
}
|
||||||
@@ -458,21 +474,17 @@ public class FileHandlerService implements InitializingBean {
|
|||||||
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
|
boolean isCompressFile = !ObjectUtils.isEmpty(compressFileKey);
|
||||||
if (isCompressFile) { //判断是否使用特定压缩包符号
|
if (isCompressFile) { //判断是否使用特定压缩包符号
|
||||||
try {
|
try {
|
||||||
// http://127.0.0.1:8012/各类型文件1 - 副本.zip_/各类型文件/正常预览/PPT转的PDF.pdf?kkCompressfileKey=各类型文件1 - 副本.zip_
|
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
||||||
// 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); //压缩包文件中文编码问题
|
|
||||||
attribute.setSkipDownLoad(true);
|
attribute.setSkipDownLoad(true);
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to decode file name: {}", originFileName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (UrlEncoderUtils.hasUrlEncoded(originFileName)) { //判断文件名是否转义
|
if (UrlEncoderUtils.hasUrlEncoded(originFileName)) { //判断文件名是否转义
|
||||||
try {
|
try {
|
||||||
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
originFileName = URLDecoder.decode(originFileName, uriEncoding); //转义的文件名 解下出原始文件名
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to decode file name: {}", originFileName, e);
|
||||||
}
|
}
|
||||||
}else {
|
}else {
|
||||||
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义
|
url = WebUtils.encodeUrlFileName(url); //对未转义的url进行转义
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public interface FilePreview {
|
|||||||
String CODE_FILE_PREVIEW_PAGE = "code";
|
String CODE_FILE_PREVIEW_PAGE = "code";
|
||||||
String EXEL_FILE_PREVIEW_PAGE = "html";
|
String EXEL_FILE_PREVIEW_PAGE = "html";
|
||||||
String XML_FILE_PREVIEW_PAGE = "xml";
|
String XML_FILE_PREVIEW_PAGE = "xml";
|
||||||
|
String JSON_FILE_PREVIEW_PAGE = "json";
|
||||||
String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
|
String MARKDOWN_FILE_PREVIEW_PAGE = "markdown";
|
||||||
String BPMN_FILE_PREVIEW_PAGE = "bpmn";
|
String BPMN_FILE_PREVIEW_PAGE = "bpmn";
|
||||||
String DCM_FILE_PREVIEW_PAGE = "dcm";
|
String DCM_FILE_PREVIEW_PAGE = "dcm";
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import org.springframework.core.Ordered;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import javax.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package cn.keking.service.cache;
|
|||||||
|
|
||||||
import org.apache.pdfbox.cos.COSObject;
|
import org.apache.pdfbox.cos.COSObject;
|
||||||
import org.apache.pdfbox.pdmodel.DefaultResourceCache;
|
import org.apache.pdfbox.pdmodel.DefaultResourceCache;
|
||||||
|
import org.apache.pdfbox.pdmodel.documentinterchange.markedcontent.PDPropertyList;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
|
||||||
import java.io.IOException;
|
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
|
* @author: Sawyer.Yong
|
||||||
@@ -14,7 +17,21 @@ import java.io.IOException;
|
|||||||
public class NotResourceCache extends DefaultResourceCache {
|
public class NotResourceCache extends DefaultResourceCache {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void put(COSObject indirect, PDXObject xobject) throws IOException {
|
public void put(COSObject indirect, PDColorSpace colorSpace) {
|
||||||
// do nothing
|
}
|
||||||
|
@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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import cn.keking.utils.DownloadUtils;
|
|||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import cn.keking.utils.WebUtils;
|
import cn.keking.utils.WebUtils;
|
||||||
import cn.keking.web.filter.BaseUrlFilter;
|
import cn.keking.web.filter.BaseUrlFilter;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
@@ -22,6 +24,7 @@ import static cn.keking.service.impl.OfficeFilePreviewImpl.getPreviewType;
|
|||||||
@Service
|
@Service
|
||||||
public class CadFilePreviewImpl implements FilePreview {
|
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_IMAGE = "image";
|
||||||
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
private static final String OFFICE_PREVIEW_TYPE_ALL_IMAGES = "allImages";
|
||||||
|
|
||||||
@@ -55,7 +58,7 @@ public class CadFilePreviewImpl implements FilePreview {
|
|||||||
try {
|
try {
|
||||||
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
|
imageUrls = fileHandlerService.cadToPdf(filePath, outFilePath, cadPreviewType, fileAttribute);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to convert CAD file: {}", filePath, e);
|
||||||
}
|
}
|
||||||
if (imageUrls == null) {
|
if (imageUrls == null) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "CAD转换异常,请联系管理员");
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import cn.keking.service.CompressFileReader;
|
|||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
@@ -28,6 +29,9 @@ public class CompressFilePreviewImpl implements FilePreview {
|
|||||||
private final CompressFileReader compressFileReader;
|
private final CompressFileReader compressFileReader;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
private static final String Rar_PASSWORD_MSG = "password";
|
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) {
|
public CompressFilePreviewImpl(FileHandlerService fileHandlerService, CompressFileReader compressFileReader, OtherFilePreviewImpl otherFilePreview) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.compressFileReader = compressFileReader;
|
this.compressFileReader = compressFileReader;
|
||||||
@@ -36,28 +40,25 @@ public class CompressFilePreviewImpl implements FilePreview {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
public String filePreviewHandle(String url, Model model, FileAttribute fileAttribute) {
|
||||||
String fileName=fileAttribute.getName();
|
String fileName = fileAttribute.getName();
|
||||||
String filePassword = fileAttribute.getFilePassword();
|
String filePassword = fileAttribute.getFilePassword();
|
||||||
boolean forceUpdatedCache=fileAttribute.forceUpdatedCache();
|
boolean forceUpdatedCache = fileAttribute.forceUpdatedCache();
|
||||||
String fileTree = null;
|
String fileTree = null;
|
||||||
// 判断文件名是否存在(redis缓存读取)
|
// 判断文件名是否存在(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);
|
ReturnResponse<String> response = DownloadUtils.downLoad(fileAttribute, fileName);
|
||||||
if (response.isFailure()) {
|
if (response.isFailure()) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, response.getMsg());
|
||||||
}
|
}
|
||||||
String filePath = response.getContent();
|
String filePath = response.getContent();
|
||||||
try {
|
try {
|
||||||
fileTree = compressFileReader.unRar(filePath, filePassword,fileName, fileAttribute);
|
fileTree = compressFileReader.unRar(filePath, filePassword, fileName, fileAttribute);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Throwable[] throwableArray = ExceptionUtils.getThrowables(e);
|
if (e.getMessage().toLowerCase().contains(Rar_PASSWORD_MSG)) {
|
||||||
for (Throwable throwable : throwableArray) {
|
model.addAttribute("needFilePassword", true);
|
||||||
if (throwable instanceof IOException || throwable instanceof EncryptedDocumentException) {
|
return EXEL_FILE_PREVIEW_PAGE;
|
||||||
if (e.getMessage().toLowerCase().contains(Rar_PASSWORD_MSG)) {
|
}else {
|
||||||
model.addAttribute("needFilePassword", true);
|
logger.error("Error processing RAR file: " + e.getMessage(), e);
|
||||||
return EXEL_FILE_PREVIEW_PAGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ObjectUtils.isEmpty(fileTree)) {
|
if (!ObjectUtils.isEmpty(fileTree)) {
|
||||||
@@ -69,14 +70,14 @@ public class CompressFilePreviewImpl implements FilePreview {
|
|||||||
// 加入缓存
|
// 加入缓存
|
||||||
fileHandlerService.addConvertedFile(fileName, fileTree);
|
fileHandlerService.addConvertedFile(fileName, fileTree);
|
||||||
}
|
}
|
||||||
}else {
|
} else {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "压缩文件密码错误! 压缩文件损坏! 压缩文件类型不受支持!");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "该压缩包文件无法处理!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileTree = fileHandlerService.getConvertedFile(fileName);
|
fileTree = fileHandlerService.getConvertedFile(fileName);
|
||||||
}
|
}
|
||||||
model.addAttribute("fileName", fileName);
|
model.addAttribute("fileName", fileName);
|
||||||
model.addAttribute("fileTree", fileTree);
|
model.addAttribute("fileTree", fileTree);
|
||||||
return COMPRESS_FILE_PREVIEW_PAGE;
|
return COMPRESS_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import org.bytedeco.ffmpeg.global.avcodec;
|
|||||||
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
import org.bytedeco.javacv.FFmpegFrameGrabber;
|
||||||
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||||
import org.bytedeco.javacv.Frame;
|
import org.bytedeco.javacv.Frame;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
@@ -26,6 +28,7 @@ import java.io.File;
|
|||||||
@Service
|
@Service
|
||||||
public class MediaFilePreviewImpl implements FilePreview {
|
public class MediaFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MediaFilePreviewImpl.class);
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
private static final String mp4 = "mp4";
|
private static final String mp4 = "mp4";
|
||||||
@@ -66,7 +69,7 @@ public class MediaFilePreviewImpl implements FilePreview {
|
|||||||
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
|
convertedUrl = outFilePath; //其他协议的 不需要转换方式的文件 直接输出
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to convert media file: {}", filePath, e);
|
||||||
}
|
}
|
||||||
if (convertedUrl == null) {
|
if (convertedUrl == null) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "视频转换异常,请联系管理员");
|
||||||
@@ -148,7 +151,7 @@ public class MediaFilePreviewImpl implements FilePreview {
|
|||||||
recorder.record(captured_frame);
|
recorder.record(captured_frame);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to convert video file to mp4: {}", filePath, e);
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (recorder != null) { //关闭
|
if (recorder != null) { //关闭
|
||||||
|
|||||||
@@ -9,11 +9,17 @@ import cn.keking.utils.DownloadUtils;
|
|||||||
import cn.keking.utils.EncodingDetects;
|
import cn.keking.utils.EncodingDetects;
|
||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.util.HtmlUtils;
|
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;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,10 +29,12 @@ import java.nio.charset.StandardCharsets;
|
|||||||
@Service
|
@Service
|
||||||
public class SimTextFilePreviewImpl implements FilePreview {
|
public class SimTextFilePreviewImpl implements FilePreview {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(SimTextFilePreviewImpl.class);
|
||||||
|
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
|
|
||||||
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService,OtherFilePreviewImpl otherFilePreview) {
|
public SimTextFilePreviewImpl(FileHandlerService fileHandlerService, OtherFilePreviewImpl otherFilePreview) {
|
||||||
this.fileHandlerService = fileHandlerService;
|
this.fileHandlerService = fileHandlerService;
|
||||||
this.otherFilePreview = otherFilePreview;
|
this.otherFilePreview = otherFilePreview;
|
||||||
}
|
}
|
||||||
@@ -47,7 +55,7 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
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) {
|
} catch (IOException e) {
|
||||||
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
return otherFilePreview.notSupportedFile(model, fileAttribute, e.getLocalizedMessage());
|
||||||
}
|
}
|
||||||
@@ -57,9 +65,9 @@ public class SimTextFilePreviewImpl implements FilePreview {
|
|||||||
try {
|
try {
|
||||||
fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
fileData = HtmlUtils.htmlEscape(textData(filePath,fileName));
|
||||||
} catch (IOException e) {
|
} 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;
|
return TXT_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import cn.keking.model.ReturnResponse;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.mola.galimatias.GalimatiasParseException;
|
import io.mola.galimatias.GalimatiasParseException;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.hc.client5.http.classic.HttpClient;
|
||||||
import org.apache.http.impl.client.DefaultRedirectStrategy;
|
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ public class LocalOfficeUtils {
|
|||||||
"/opt/libreoffice7.4",
|
"/opt/libreoffice7.4",
|
||||||
"/opt/libreoffice7.5",
|
"/opt/libreoffice7.5",
|
||||||
"/opt/libreoffice7.6",
|
"/opt/libreoffice7.6",
|
||||||
|
"/opt/libreoffice24.2",
|
||||||
|
"/opt/libreoffice24.8",
|
||||||
|
"/opt/libreoffice25.2",
|
||||||
"/usr/lib64/libreoffice",
|
"/usr/lib64/libreoffice",
|
||||||
"/usr/lib/libreoffice",
|
"/usr/lib/libreoffice",
|
||||||
"/usr/local/lib64/libreoffice",
|
"/usr/local/lib64/libreoffice",
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
|
|||||||
import org.apache.poi.EncryptedDocumentException;
|
import org.apache.poi.EncryptedDocumentException;
|
||||||
import org.apache.poi.extractor.ExtractorFactory;
|
import org.apache.poi.extractor.ExtractorFactory;
|
||||||
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -18,6 +20,7 @@ import java.nio.file.Paths;
|
|||||||
*/
|
*/
|
||||||
public class OfficeUtils {
|
public class OfficeUtils {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OfficeUtils.class);
|
||||||
private static final String POI_INVALID_PASSWORD_MSG = "password";
|
private static final String POI_INVALID_PASSWORD_MSG = "password";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +52,7 @@ public class OfficeUtils {
|
|||||||
try {
|
try {
|
||||||
propStream.close();//关闭文件输入流
|
propStream.close();//关闭文件输入流
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to close input stream for file: {}", path, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +79,7 @@ public class OfficeUtils {
|
|||||||
try {
|
try {
|
||||||
propStream.close();//关闭文件输入流
|
propStream.close();//关闭文件输入流
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to close input stream for file: {}", path, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package cn.keking.utils;
|
package cn.keking.utils;
|
||||||
import cn.keking.config.ConfigConstants;
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.service.ZtreeNodeVo;
|
import cn.keking.service.ZtreeNodeVo;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
@@ -15,6 +18,7 @@ import java.util.regex.Pattern;
|
|||||||
* create : 2023-04-08
|
* create : 2023-04-08
|
||||||
**/
|
**/
|
||||||
public class RarUtils {
|
public class RarUtils {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RarUtils.class);
|
||||||
private static final String fileDir = ConfigConstants.getFileDir();
|
private static final String fileDir = ConfigConstants.getFileDir();
|
||||||
|
|
||||||
public static byte[] getUTF8BytesFromGBKString(String gbkStr) {
|
public static byte[] getUTF8BytesFromGBKString(String gbkStr) {
|
||||||
@@ -55,7 +59,7 @@ public class RarUtils {
|
|||||||
str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8);
|
str = new String(getUTF8BytesFromGBKString(str), StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
e.printStackTrace();
|
logger.error("Failed to convert string encoding: {}", str, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return str;
|
return str;
|
||||||
@@ -77,7 +81,7 @@ public class RarUtils {
|
|||||||
}
|
}
|
||||||
public static String specialSymbols(String str) {
|
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}");
|
// Pattern p = Pattern.compile("\\s|\\+|#|&|=|\\p{P}");
|
||||||
Matcher m = p.matcher(str);
|
Matcher m = p.matcher(str);
|
||||||
return m.replaceAll("");
|
return m.replaceAll("");
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package cn.keking.utils;
|
package cn.keking.utils;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
@@ -31,6 +34,8 @@ import java.net.URL;
|
|||||||
*/
|
*/
|
||||||
public class SimpleEncodingDetects {
|
public class SimpleEncodingDetects {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SimpleEncodingDetects.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 得到文件的编码
|
* 得到文件的编码
|
||||||
* @param content 文件内容
|
* @param content 文件内容
|
||||||
@@ -65,10 +70,10 @@ public class SimpleEncodingDetects {
|
|||||||
|
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
logger.error("File not found: {}", file, e);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
logger.error("Failed to read file: {}", file, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package cn.keking.utils;
|
package cn.keking.utils;
|
||||||
|
|
||||||
import io.mola.galimatias.GalimatiasParseException;
|
import io.mola.galimatias.GalimatiasParseException;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.util.Base64Utils;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.util.HtmlUtils;
|
import org.springframework.web.util.HtmlUtils;
|
||||||
|
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
@@ -79,13 +79,15 @@ public class WebUtils {
|
|||||||
urlStr = clearFullfilenameParam(urlStr);
|
urlStr = clearFullfilenameParam(urlStr);
|
||||||
} else {
|
} else {
|
||||||
fullFileName = getFileNameFromURL(urlStr); //获取文件名
|
fullFileName = getFileNameFromURL(urlStr); //获取文件名
|
||||||
|
}
|
||||||
|
if (KkFileUtils.isIllegalFileName(fullFileName)) { //判断文件名是否带有穿越漏洞
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
if (!UrlEncoderUtils.hasUrlEncoded(fullFileName)) { //判断文件名是否转义
|
if (!UrlEncoderUtils.hasUrlEncoded(fullFileName)) { //判断文件名是否转义
|
||||||
try {
|
try {
|
||||||
urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "=");
|
urlStr = URLEncoder.encode(urlStr, "UTF-8").replaceAll("\\+", "%20").replaceAll("%3A", ":").replaceAll("%2F", "/").replaceAll("%3F", "?").replaceAll("%26", "&").replaceAll("%3D", "=");
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("Failed to encode URL: {}", urlStr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return urlStr;
|
return urlStr;
|
||||||
@@ -153,7 +155,7 @@ public class WebUtils {
|
|||||||
URL urlObj = new URL(url);
|
URL urlObj = new URL(url);
|
||||||
url = urlObj.getPath().substring(1);
|
url = urlObj.getPath().substring(1);
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("Failed to parse file URL: {}", url, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 因为url的参数中可能会存在/的情况,所以直接url.lastIndexOf("/")会有问题
|
// 因为url的参数中可能会存在/的情况,所以直接url.lastIndexOf("/")会有问题
|
||||||
@@ -288,7 +290,7 @@ public class WebUtils {
|
|||||||
* https://github.com/kekingcn/kkFileView/pull/340
|
* https://github.com/kekingcn/kkFileView/pull/340
|
||||||
*/
|
*/
|
||||||
try {
|
try {
|
||||||
return new String(Base64Utils.decodeFromString(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
|
return new String(Base64.decodeBase64(source.replaceAll(" ", "+").replaceAll("\n", "")), charsets);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
|
if (e.getMessage().toLowerCase().contains(BASE64_MSG)) {
|
||||||
LOGGER.error("url解码异常,接入方法错误未使用BASE64");
|
LOGGER.error("url解码异常,接入方法错误未使用BASE64");
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.servlet.ServletOutputStream;
|
import jakarta.servlet.ServletOutputStream;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -213,6 +213,7 @@ public class FileController {
|
|||||||
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
|
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
|
||||||
return ReturnResponse.failure(errorMsg);
|
return ReturnResponse.failure(errorMsg);
|
||||||
}
|
}
|
||||||
|
fileUrl = fileUrl.replaceAll("http://", "");
|
||||||
if (KkFileUtils.isIllegalFileName(fileUrl)) {
|
if (KkFileUtils.isIllegalFileName(fileUrl)) {
|
||||||
return ReturnResponse.failure("不允许访问的路径:");
|
return ReturnResponse.failure("不允许访问的路径:");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import cn.keking.utils.WebUtils;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import fr.opensagres.xdocreport.core.io.IOUtils;
|
import fr.opensagres.xdocreport.core.io.IOUtils;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.hc.client5.http.classic.HttpClient;
|
||||||
import org.apache.http.impl.client.DefaultRedirectStrategy;
|
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@@ -21,14 +21,15 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.client.RequestCallback;
|
import org.springframework.web.client.RequestCallback;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
@@ -76,7 +77,11 @@ public class OnlinePreviewController {
|
|||||||
model.addAttribute("file", fileAttribute);
|
model.addAttribute("file", fileAttribute);
|
||||||
FilePreview filePreview = previewFactory.get(fileAttribute);
|
FilePreview filePreview = previewFactory.get(fileAttribute);
|
||||||
logger.info("预览文件url:{},previewType:{}", fileUrl, fileAttribute.getType());
|
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")
|
@GetMapping( "/picturesPreview")
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
|
|||||||
import cn.keking.config.WatermarkConfigConstants;
|
import cn.keking.config.WatermarkConfigConstants;
|
||||||
import cn.keking.utils.KkFileUtils;
|
import cn.keking.utils.KkFileUtils;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import cn.keking.config.ConfigConstants;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package cn.keking.web.filter;
|
package cn.keking.web.filter;
|
||||||
|
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package cn.keking.web.filter;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
import javax.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import javax.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.springframework.core.io.ClassPathResource;
|
|||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
@@ -35,7 +35,7 @@ public class TrustDirFilter implements Filter {
|
|||||||
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
||||||
this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8);
|
this.notTrustDirView = new String(bytes, StandardCharsets.UTF_8);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("加载notTrustDir.html失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import cn.keking.config.ConfigConstants;
|
|||||||
import cn.keking.utils.WebUtils;
|
import cn.keking.utils.WebUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import javax.servlet.Filter;
|
import java.util.Set;
|
||||||
import javax.servlet.FilterChain;
|
import java.util.regex.Pattern;
|
||||||
import javax.servlet.FilterConfig;
|
import jakarta.servlet.Filter;
|
||||||
import javax.servlet.ServletException;
|
import jakarta.servlet.FilterChain;
|
||||||
import javax.servlet.ServletRequest;
|
import jakarta.servlet.FilterConfig;
|
||||||
import javax.servlet.ServletResponse;
|
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.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.util.FileCopyUtils;
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
@@ -22,6 +30,8 @@ import org.springframework.util.FileCopyUtils;
|
|||||||
*/
|
*/
|
||||||
public class TrustHostFilter implements Filter {
|
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;
|
private String notTrustHostHtmlView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -32,7 +42,7 @@ public class TrustHostFilter implements Filter {
|
|||||||
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
byte[] bytes = FileCopyUtils.copyToByteArray(classPathResource.getInputStream());
|
||||||
this.notTrustHostHtmlView = new String(bytes, StandardCharsets.UTF_8);
|
this.notTrustHostHtmlView = new String(bytes, StandardCharsets.UTF_8);
|
||||||
} catch (IOException e) {
|
} 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 {
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
String url = WebUtils.getSourceUrl(request);
|
String url = WebUtils.getSourceUrl(request);
|
||||||
String host = WebUtils.getHost(url);
|
String host = WebUtils.getHost(url);
|
||||||
assert host != null;
|
|
||||||
if (isNotTrustHost(host)) {
|
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().write(html);
|
||||||
response.getWriter().close();
|
response.getWriter().close();
|
||||||
} else {
|
} else {
|
||||||
@@ -51,15 +68,163 @@ public class TrustHostFilter implements Filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isNotTrustHost(String host) {
|
public boolean isNotTrustHost(String host) {
|
||||||
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())) {
|
if (host == null || host.trim().isEmpty()) {
|
||||||
return ConfigConstants.getNotTrustHostSet().contains(host);
|
logger.warn("主机名为空或无效,拒绝访问");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果配置了黑名单,优先检查黑名单
|
||||||
|
if (CollectionUtils.isNotEmpty(ConfigConstants.getNotTrustHostSet())
|
||||||
|
&& matchAnyPattern(host, ConfigConstants.getNotTrustHostSet())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果配置了白名单,检查是否在白名单中
|
||||||
if (CollectionUtils.isNotEmpty(ConfigConstants.getTrustHostSet())) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持三种匹配方式:
|
||||||
|
* 1. 精确匹配:example.com
|
||||||
|
* 2. 通配符匹配:*.example.com、192.168.*
|
||||||
|
* 3. IPv4 CIDR:192.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
|
@Override
|
||||||
public void destroy() {
|
public void destroy() {
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package cn.keking.web.filter;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import javax.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
| < | < | | | | | | | __/ \ / | | | __/ \ V V /
|
| < | < | | | | | | | __/ \ / | | | __/ \ V V /
|
||||||
|_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/
|
|_|\_\ |_|\_\ |_| |_| |_| \___| \/ |_| \___| \_/\_/
|
||||||
|
|
||||||
|
=> Java Version :: ${java.version}
|
||||||
=> Spring Boot :: ${spring-boot.version}
|
=> Spring Boot :: ${spring-boot.version}
|
||||||
=> kkFileView :: 4.4.0-beta
|
=> kkFileView :: 4.4.0
|
||||||
=> Home site :: https://kkview.cn
|
=> Home site :: https://kkview.cn
|
||||||
=> Github :: https://github.com/kekingcn/kkFileView
|
=> Github :: https://github.com/kekingcn/kkFileView
|
||||||
=> Gitee :: https://gitee.com/kekingcn/file-online-preview
|
=> Gitee :: https://gitee.com/kekingcn/file-online-preview
|
||||||
|
|||||||
@@ -8,7 +8,19 @@
|
|||||||
*/
|
*/
|
||||||
function initWaterMark() {
|
function initWaterMark() {
|
||||||
let watermarkTxt = '${watermarkTxt}';
|
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.init({
|
||||||
watermark_txt: '${watermarkTxt}',
|
watermark_txt: '${watermarkTxt}',
|
||||||
watermark_x: 0,
|
watermark_x: 0,
|
||||||
@@ -25,7 +37,11 @@
|
|||||||
watermark_height: ${watermarkHeight},
|
watermark_height: ${watermarkHeight},
|
||||||
watermark_angle: ${watermarkAngle},
|
watermark_angle: ${watermarkAngle},
|
||||||
});
|
});
|
||||||
}
|
// 更新存储的宽口大小
|
||||||
|
lastWidth = currentWidth;
|
||||||
|
lastHeight = currentHeight;
|
||||||
|
};
|
||||||
|
setInterval(checkResize, 1000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>压缩包预览</title>
|
<title>${file.name}压缩包预览</title>
|
||||||
<script src="js/jquery-3.6.1.min.js"></script>
|
<script src="js/jquery-3.6.1.min.js"></script>
|
||||||
<#include "*/commonHeader.ftl">
|
<#include "*/commonHeader.ftl">
|
||||||
<script src="js/base64.min.js" type="text/javascript"></script>
|
<script src="js/base64.min.js" type="text/javascript"></script>
|
||||||
@@ -49,14 +49,28 @@
|
|||||||
onClick: chooseNode,
|
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) {
|
function chooseNode(event, treeId, treeNode) {
|
||||||
if (!treeNode.isParent) {
|
if (!treeNode.isParent) {
|
||||||
var path = '${baseUrl}' + treeNode.id + "?kkCompressfileKey=" + encodeURIComponent('${fileTree}')+"&kkCompressfilepath=" + encodeURIComponent(treeNode.id)+"&fullfilename="+encodeURIComponent(treeNode.name);
|
var path = '${baseUrl}'+treeNode.id+"?kkCompressfileKey="+'${fileTree}'+"&kkCompressfilepath="+encodeURIComponent(treeNode.id)+"&fullfilename="+encodeURIComponent(treeNode.name);
|
||||||
location.href = "${baseUrl}onlinePreview?url=" + encodeURIComponent(Base64.encode(path));
|
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 () {
|
$(document).ready(function () {
|
||||||
var url = '${fileTree}';
|
var url = "http://"+'${fileTree}'; //添加http协议方法
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "get",
|
type: "get",
|
||||||
url: "${baseUrl}directory?urls="+encodeURIComponent(Base64.encode(url)),
|
url: "${baseUrl}directory?urls="+encodeURIComponent(Base64.encode(url)),
|
||||||
@@ -66,6 +80,9 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
window.onload = function () {
|
||||||
|
initWaterMark();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
384
server/src/main/resources/web/json.ftl
Normal 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, "&");
|
||||||
|
s = s.replace(/</g, "<");
|
||||||
|
s = s.replace(/>/g, ">");
|
||||||
|
s = s.replace(/"/g, """);
|
||||||
|
s = s.replace(/'/g, "'");
|
||||||
|
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
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>
|
||||||
@@ -148,6 +148,18 @@
|
|||||||
<input type="file" id="file" name="file" style="float: left; margin: 0 auto; font-size:22px;" placeholder="请选择文件"/>
|
<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=" 上 传 "/>
|
<input type="button" id="fileUploadBtn" class="btn btn-success" value=" 上 传 "/>
|
||||||
</form>
|
</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>
|
</#if>
|
||||||
<table id="table" data-pagination="true"></table>
|
<table id="table" data-pagination="true"></table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,6 +33,63 @@
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>版本发布记录</h1>
|
<h1>版本发布记录</h1>
|
||||||
</div>
|
</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 panel-success">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h3 class="panel-title">2024年04月15日,v4.4.0-beta版本</h3>
|
<h3 class="panel-title">2024年04月15日,v4.4.0-beta版本</h3>
|
||||||
@@ -44,8 +101,8 @@
|
|||||||
3. 修复 forceUpdatedCache 属性设置,但是本地缓存文件不更新缺陷 <br>
|
3. 修复 forceUpdatedCache 属性设置,但是本地缓存文件不更新缺陷 <br>
|
||||||
4. 配置文件新增启用 GZIP压缩 <br>
|
4. 配置文件新增启用 GZIP压缩 <br>
|
||||||
5. 升级CAD组件,CAD格式新增支持 转换成svg tif 格式 新增 超时结束 新增线程管理 <br>
|
5. 升级CAD组件,CAD格式新增支持 转换成svg tif 格式 新增 超时结束 新增线程管理 <br>
|
||||||
6. 删除功能 新增验证码方法 <br>
|
6. 删除功能 新增验证码方法 <br>
|
||||||
7. office 功能调整 支持批注 转换页码限制 生成水印等等 <br>
|
7. office 功能调整 支持批注 转换页码限制 生成水印等等 <br>
|
||||||
8. 新增xbrl格式 <br>
|
8. 新增xbrl格式 <br>
|
||||||
9. 修复PDF解密加密文件 转换成功后台报错问题 <br>
|
9. 修复PDF解密加密文件 转换成功后台报错问题 <br>
|
||||||
10. 升级markdown组件 修复markdown被转义问题 <br>
|
10. 升级markdown组件 修复markdown被转义问题 <br>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
title: exportJson.info.name,
|
title: exportJson.info.name,
|
||||||
userInfo: exportJson.info.name.creator,
|
userInfo: exportJson.info.name.creator,
|
||||||
});
|
});
|
||||||
});
|
}, 1000);
|
||||||
}
|
}
|
||||||
loadText();
|
loadText();
|
||||||
// 打印时,获取luckysheet指定区域html内容,拼接至div,隐藏luckysheet容器并显示打印区域html
|
// 打印时,获取luckysheet指定区域html内容,拼接至div,隐藏luckysheet容器并显示打印区域html
|
||||||
|
|||||||
@@ -21,12 +21,4 @@ public class WebUtilsTests {
|
|||||||
String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0¶m1=1";
|
String out = "https://file.keking.cn/demo/%23hello%26world.txt?param0=0¶m1=1";
|
||||||
assert WebUtils.encodeUrlFileName(in).equals(out);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
fixtures/zip-tmp/
|
||||||
|
fixtures/sample.docx
|
||||||
|
fixtures/sample.xlsx
|
||||||
|
fixtures/sample.pptx
|
||||||
53
tests/e2e/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 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 check (zip)
|
||||||
|
- Basic endpoint reachability
|
||||||
|
- Security regression checks for blocked internal-network hosts (`10.*`) on:
|
||||||
|
- `/onlinePreview`
|
||||||
|
- `/getCorsFile`
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Generate fixtures and start fixture server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/kkFileView
|
||||||
|
node tests/e2e/scripts/generate-fixtures.mjs
|
||||||
|
python3 tests/e2e/scripts/generate-office-fixtures.py
|
||||||
|
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
|
||||||
|
```
|
||||||
3
tests/e2e/fixtures/sample.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
name,value
|
||||||
|
kkFileView,1
|
||||||
|
e2e,1
|
||||||
|
1
tests/e2e/fixtures/sample.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html><body><h1>kkFileView fixture</h1></body></html>
|
||||||
4
tests/e2e/fixtures/sample.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"app": "kkFileView",
|
||||||
|
"e2e": true
|
||||||
|
}
|
||||||
3
tests/e2e/fixtures/sample.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# kkFileView
|
||||||
|
|
||||||
|
This is a markdown fixture.
|
||||||
19
tests/e2e/fixtures/sample.pdf
Normal 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
|
||||||
BIN
tests/e2e/fixtures/sample.png
Normal file
|
After Width: | Height: | Size: 68 B |
1
tests/e2e/fixtures/sample.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
kkFileView e2e sample text
|
||||||
1
tests/e2e/fixtures/sample.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<root><name>kkFileView</name><e2e>true</e2e></root>
|
||||||
BIN
tests/e2e/fixtures/sample.zip
Normal file
78
tests/e2e/package-lock.json
generated
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"name": "kkfileview-e2e",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "kkfileview-e2e",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.55.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
tests/e2e/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "kkfileview-e2e",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"gen:fixtures": "node ./scripts/generate-fixtures.mjs",
|
||||||
|
"gen:office": "python3 ./scripts/generate-office-fixtures.py",
|
||||||
|
"gen:all": "npm run gen:fixtures && npm run gen:office",
|
||||||
|
"pretest": "npm run gen:all",
|
||||||
|
"test": "playwright test",
|
||||||
|
"test:headed": "playwright test --headed"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.55.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
tests/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './specs',
|
||||||
|
timeout: 30_000,
|
||||||
|
expect: { timeout: 10_000 },
|
||||||
|
reporter: [['list'], ['html', { outputFolder: 'playwright-report', open: 'never' }]],
|
||||||
|
use: {
|
||||||
|
baseURL: process.env.KK_BASE_URL || 'http://127.0.0.1:8012',
|
||||||
|
},
|
||||||
|
});
|
||||||
3
tests/e2e/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
python-docx==1.1.2
|
||||||
|
openpyxl==3.1.5
|
||||||
|
python-pptx==1.0.2
|
||||||