mirror of
https://github.com/zfile-dev/zfile.git
synced 2025-04-19 05:34:52 +00:00
Compare commits
358 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5b6f8da4e | ||
|
|
6da72845e4 | ||
|
|
81b9ac5923 | ||
|
|
8f8dc86365 | ||
|
|
f9d46edbcc | ||
|
|
e3991be3b7 | ||
|
|
d42853abe2 | ||
|
|
e5c6a1aca4 | ||
|
|
b5d46912d2 | ||
|
|
dfcbaf7158 | ||
|
|
17257dccda | ||
|
|
1c1248306c | ||
|
|
d5873a7f97 | ||
|
|
7437dc8936 | ||
|
|
549a599a5b | ||
|
|
38effb0bb7 | ||
|
|
1e25c23b0b | ||
|
|
c870d32777 | ||
|
|
b86e487a47 | ||
|
|
330713509e | ||
|
|
ecf85dfe9e | ||
|
|
ac3b4283a3 | ||
|
|
141f9dee5e | ||
|
|
ce9f809ab5 | ||
|
|
b83af8dc5e | ||
|
|
ce0a7bd6ef | ||
|
|
008425734c | ||
|
|
e078366395 | ||
|
|
69ec12ab99 | ||
|
|
8e93874c69 | ||
|
|
2a6f0f94cc | ||
|
|
d501d96ad6 | ||
|
|
73569a63f8 | ||
|
|
71e6ba4d8b | ||
|
|
72a627be74 | ||
|
|
0344f687b6 | ||
|
|
1b0789cdd0 | ||
|
|
7cf16754f8 | ||
|
|
6aefc107e7 | ||
|
|
e082043f99 | ||
|
|
a55e8ae2ad | ||
|
|
188431b64d | ||
|
|
7f23dcb7c4 | ||
|
|
8dcb64a60d | ||
|
|
d9c64ff369 | ||
|
|
6963b1d593 | ||
|
|
0a61e1047d | ||
|
|
3f02cd9832 | ||
|
|
3aa42c00fa | ||
|
|
77b2253ff6 | ||
|
|
71a4fdfbaf | ||
|
|
2ecd69dc51 | ||
|
|
ebae9ba5c8 | ||
|
|
b2fd722443 | ||
|
|
e2ce404e87 | ||
|
|
6cfbe7689e | ||
|
|
b95c1d890b | ||
|
|
461c77012a | ||
|
|
9328e0ea9d | ||
|
|
aefa928a19 | ||
|
|
89c6c515f1 | ||
|
|
dcadffa265 | ||
|
|
300e58e92c | ||
|
|
96b71e4f8d | ||
|
|
456aabb893 | ||
|
|
b4d2ca238f | ||
|
|
2afb841fd9 | ||
|
|
2b09812153 | ||
|
|
972099a598 | ||
|
|
9335d78d60 | ||
|
|
f332cb929b | ||
|
|
a190a2ec6e | ||
|
|
5bfa7037cb | ||
|
|
4a94c879b8 | ||
|
|
38161f96e1 | ||
|
|
33c751ab33 | ||
|
|
d12cbd2383 | ||
|
|
5f84becf08 | ||
|
|
432fd89c0f | ||
|
|
84c8adc9d2 | ||
|
|
ba2523ac8a | ||
|
|
dcdd25c01f | ||
|
|
278e320550 | ||
|
|
1bfa66cc49 | ||
|
|
20fb2b3baa | ||
|
|
e9a85a4e88 | ||
|
|
63edbc11d3 | ||
|
|
c98c6af0f9 | ||
|
|
47f6066733 | ||
|
|
f4fb471da3 | ||
|
|
301b6bdf70 | ||
|
|
a98c4f4c48 | ||
|
|
f4a625dac6 | ||
|
|
1c2f3de55c | ||
|
|
ed375768f5 | ||
|
|
b0c4ed1fad | ||
|
|
8d30ca7eee | ||
|
|
43aba8e20e | ||
|
|
a2fa8b3eeb | ||
|
|
0ef77ee11b | ||
|
|
6608c0b456 | ||
|
|
3ceb5c8c1b | ||
|
|
7399c89a8e | ||
|
|
477c9dbdd2 | ||
|
|
e8117c7d3b | ||
|
|
b29ff1e646 | ||
|
|
774a8e184a | ||
|
|
7da1405798 | ||
|
|
564770ba3c | ||
|
|
ac07de4e73 | ||
|
|
3933eba99a | ||
|
|
27e8f7961e | ||
|
|
55843cbef6 | ||
|
|
ebbb33409f | ||
|
|
b39360791f | ||
|
|
9833378e25 | ||
|
|
31df2d16e2 | ||
|
|
f8f07912a1 | ||
|
|
edf43954c6 | ||
|
|
5a816b1dfb | ||
|
|
910406c33a | ||
|
|
543f76ad1d | ||
|
|
1cc2d874a1 | ||
|
|
18de372bf9 | ||
|
|
5748644814 | ||
|
|
fb8060d316 | ||
|
|
a4005defe2 | ||
|
|
4c90d5bdda | ||
|
|
fa46850bb4 | ||
|
|
a7551fce53 | ||
|
|
0591669ec9 | ||
|
|
6286a9e9aa | ||
|
|
c67ee1e89d | ||
|
|
c75695c63a | ||
|
|
8f771c652d | ||
|
|
d6e13d6115 | ||
|
|
8460d17b07 | ||
|
|
49806221b4 | ||
|
|
409af8409d | ||
|
|
6647efeb03 | ||
|
|
c8bd52e26a | ||
|
|
27db6ed14a | ||
|
|
b079d03753 | ||
|
|
edf8e114ad | ||
|
|
67a84edd4d | ||
|
|
d798750ee6 | ||
|
|
fea1da86fc | ||
|
|
96a0c90600 | ||
|
|
a054e82740 | ||
|
|
dc0e84e1e3 | ||
|
|
ae31005959 | ||
|
|
0f167a304d | ||
|
|
43d8143c74 | ||
|
|
b2fb1af99b | ||
|
|
a4f4d654a3 | ||
|
|
04d9c24b43 | ||
|
|
91a7092190 | ||
|
|
f82155f157 | ||
|
|
a99218dfa5 | ||
|
|
3e9fe27890 | ||
|
|
7524f58928 | ||
|
|
6499f2d220 | ||
|
|
40b1f1bc2d | ||
|
|
3bd859d705 | ||
|
|
6156d85ba3 | ||
|
|
e1df2f886b | ||
|
|
cea42a57bb | ||
|
|
c123d2a671 | ||
|
|
02e00b765b | ||
|
|
1992c1f52d | ||
|
|
4eb9a721fc | ||
|
|
f1bac40af4 | ||
|
|
0a7fb41e81 | ||
|
|
4fd0b8d442 | ||
|
|
28150b0c1a | ||
|
|
a7813ff7bc | ||
|
|
ae500ef9dc | ||
|
|
29d5b84df2 | ||
|
|
28882b198c | ||
|
|
d624c886fe | ||
|
|
e08654a59e | ||
|
|
8fa6a5c9b8 | ||
|
|
7620a578fb | ||
|
|
9c9bf93f00 | ||
|
|
912eb694cb | ||
|
|
78d5ba90cf | ||
|
|
056f9d4449 | ||
|
|
9a6020dcc8 | ||
|
|
6b3025f379 | ||
|
|
9dffaaee25 | ||
|
|
bda654a012 | ||
|
|
52271b5117 | ||
|
|
223a6c1970 | ||
|
|
60c272b714 | ||
|
|
3b6ed1c78d | ||
|
|
5d888bb68f | ||
|
|
082bb07213 | ||
|
|
803b8cdf71 | ||
|
|
eca5f7e48b | ||
|
|
b70c37f3f0 | ||
|
|
5cb2844141 | ||
|
|
4dd6cdb4b3 | ||
|
|
c6127c029f | ||
|
|
e149039ecb | ||
|
|
37688d83cf | ||
|
|
6692016642 | ||
|
|
3f41aeda9a | ||
|
|
de86d5c47d | ||
|
|
c89e072005 | ||
|
|
b533b5e959 | ||
|
|
d35cf27d47 | ||
|
|
796c4c1fb0 | ||
|
|
1033d6c1a9 | ||
|
|
e09c6b4e58 | ||
|
|
168b0b08f3 | ||
|
|
60a6a5348c | ||
|
|
2ec8a5df1f | ||
|
|
47b5f6ac12 | ||
|
|
c64c8465f2 | ||
|
|
f636681dd8 | ||
|
|
eadd2434e0 | ||
|
|
b84c0bff42 | ||
|
|
4cb5b84bfe | ||
|
|
0351b4401c | ||
|
|
48cb14be8a | ||
|
|
eea2ff11f9 | ||
|
|
325ec1a348 | ||
|
|
93205266d3 | ||
|
|
6849a4347f | ||
|
|
cb5c6a5945 | ||
|
|
5ed45c3bb3 | ||
|
|
4442ec3165 | ||
|
|
b268a24333 | ||
|
|
84f9354d4e | ||
|
|
8dfc4f8004 | ||
|
|
1158f5c2b9 | ||
|
|
c5f0e15207 | ||
|
|
68a842ce75 | ||
|
|
3bd4f74dae | ||
|
|
4a0bdc3baf | ||
|
|
603c1b4654 | ||
|
|
1136d582df | ||
|
|
187544fc06 | ||
|
|
aa3cde8f59 | ||
|
|
e29a702b6e | ||
|
|
e4f663c9f0 | ||
|
|
fb08ef6e78 | ||
|
|
10c465d159 | ||
|
|
d15d1203b7 | ||
|
|
de48ed3b61 | ||
|
|
d22e2e872a | ||
|
|
7da1b454dc | ||
|
|
463f311dd3 | ||
|
|
73b42cf654 | ||
|
|
1fac59c4cd | ||
|
|
6ed6b4a019 | ||
|
|
7bd02437f0 | ||
|
|
57aeb5771c | ||
|
|
695c03a530 | ||
|
|
6ebc403572 | ||
|
|
7ff6fe43b5 | ||
|
|
b34f141181 | ||
|
|
87dd7b58d1 | ||
|
|
2a949db5d2 | ||
|
|
b889e91fb5 | ||
|
|
b5c757f9f0 | ||
|
|
a465f48b94 | ||
|
|
797a0a0e4c | ||
|
|
e7ff159b6d | ||
|
|
a9fbf54bb2 | ||
|
|
81f9e262f5 | ||
|
|
23bb3960ab | ||
|
|
c4a17985a4 | ||
|
|
75ddcd47f4 | ||
|
|
2dd03ae490 | ||
|
|
5b383c8741 | ||
|
|
73198d7852 | ||
|
|
fb0d9721aa | ||
|
|
b24c663fd6 | ||
|
|
6e62cfc84d | ||
|
|
eee22e9dc9 | ||
|
|
5109c51ffc | ||
|
|
66d6d311ea | ||
|
|
ef7cbdcbd7 | ||
|
|
63bcbebb4b | ||
|
|
50ce1bb6db | ||
|
|
3c88659679 | ||
|
|
d79a993eea | ||
|
|
afafb311b8 | ||
|
|
2e280e4931 | ||
|
|
ed6efac8b7 | ||
|
|
7409df85d7 | ||
|
|
4d42529c4d | ||
|
|
65224685c8 | ||
|
|
080a84986e | ||
|
|
3f8beb2f0b | ||
|
|
410a87c426 | ||
|
|
c14b8343d2 | ||
|
|
bea440f6c3 | ||
|
|
6a5fe15121 | ||
|
|
e920ab0ec0 | ||
|
|
537e3e0563 | ||
|
|
e208dc7c4c | ||
|
|
ed64910a53 | ||
|
|
5b075c3505 | ||
|
|
a8e6d9af6a | ||
|
|
2b21d8a73c | ||
|
|
e30289d21b | ||
|
|
3b6e2be7fe | ||
|
|
43c12aa8a7 | ||
|
|
c03a7710c0 | ||
|
|
1833b23d84 | ||
|
|
f3e393972d | ||
|
|
4f46c13369 | ||
|
|
f181959218 | ||
|
|
11effc0ae7 | ||
|
|
c8397e17bf | ||
|
|
ed32b9f1d4 | ||
|
|
4e184936db | ||
|
|
fe6aebfdee | ||
|
|
d65e1a442d | ||
|
|
34647793c8 | ||
|
|
e8c249b9ea | ||
|
|
d1e613dc10 | ||
|
|
1adcfee96f | ||
|
|
75f5de6b9a | ||
|
|
499942ef70 | ||
|
|
e11277ce26 | ||
|
|
5edd9e38a7 | ||
|
|
f4ffee706b | ||
|
|
bb65750278 | ||
|
|
e09167c0d0 | ||
|
|
ee6c04fa11 | ||
|
|
b31982b788 | ||
|
|
544a3d3eb2 | ||
|
|
1987bc97a9 | ||
|
|
7e878af06c | ||
|
|
766a047ee1 | ||
|
|
c1d29a46f5 | ||
|
|
08e39b3d15 | ||
|
|
e7790ac256 | ||
|
|
499f3e108c | ||
|
|
19144b653e | ||
|
|
17a87648fa | ||
|
|
ac4cef0980 | ||
|
|
71978f8003 | ||
|
|
0b3a67ec6e | ||
|
|
ee5fb54ebb | ||
|
|
4585f22817 | ||
|
|
b523453588 | ||
|
|
a8cc03c911 | ||
|
|
949c437653 | ||
|
|
84e9cce60f | ||
|
|
5720bd93ec | ||
|
|
bcae9713bc | ||
|
|
04e3023071 | ||
|
|
a09ef84629 | ||
|
|
1d29395191 |
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: BUG 反馈
|
||||
about: 事情不像预期的那样工作吗?
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
你好!感谢你正在考虑为 ZFile 提交一个 bug。请花一点点时间尽量详细地回答以下基础问题。
|
||||
|
||||
谢谢!
|
||||
-->
|
||||
|
||||
<!--
|
||||
请确认你已经做了下面这些事情,若 bug 还是未解决,那么请尽可详细地描述你的问题。
|
||||
|
||||
- 我已经安装了最新版的 ZFile
|
||||
- 我已经阅读了 ZFile 的文档:http://docs.zhaojun.im/zfile
|
||||
- 我已经搜索了已有的 Issues 列表中有关的信息
|
||||
- 我已经清理过浏览器缓存并重试
|
||||
-->
|
||||
|
||||
## 我的环境
|
||||
|
||||
<!--
|
||||
请登录到管理后台,点击左侧系统监控, 复制或截图此页内容.
|
||||
-->
|
||||
|
||||
## 错误日志
|
||||
|
||||
<!--
|
||||
请登录到管理后台,点击左侧系统监控, 点击右上角诊断日志下载, 然后上传到此 Issue 中.
|
||||
-->
|
||||
|
||||
## 期望行为
|
||||
|
||||
<!--
|
||||
你期望会发生什么?
|
||||
-->
|
||||
|
||||
## 当前行为
|
||||
|
||||
<!--
|
||||
描述 bug 细节,确认出现此问题的复现步骤,例如点击了哪里,发生了什么情况?
|
||||
|
||||
你可以粘贴截图或附件。
|
||||
-->
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 创建 Issue
|
||||
url: https://issue.zfile.vip/
|
||||
about: 未通过 https://issue.zfile.vip/ 创建的问题可能会被立即关闭。
|
||||
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
34
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: 功能建议
|
||||
about: 想让我们为 ZFile 增加什么功能吗?
|
||||
title: 'feat: '
|
||||
labels: 'Feature Request'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
你好!感谢你愿意考虑希望 ZFile 增加某个新功能。请花一点点时间尽量详细地回答以下基础问题。
|
||||
|
||||
谢谢!
|
||||
-->
|
||||
|
||||
## 概述
|
||||
|
||||
<!--
|
||||
对这个新功能的一段描述
|
||||
-->
|
||||
|
||||
## 动机
|
||||
|
||||
<!--
|
||||
为什么你希望在 ZFile 中使用这个功能?
|
||||
-->
|
||||
|
||||
## 详细解释
|
||||
|
||||
<!--
|
||||
详细描述这个新功能。
|
||||
|
||||
如果这是一个小功能,你可以忽略这部分。
|
||||
-->
|
||||
14
.github/ISSUE_TEMPLATE/question.md
vendored
14
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Question
|
||||
about: 对 ZFile 有任何问题吗?
|
||||
title: ''
|
||||
labels: 'question'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
如果你有任何问题也可以通过此渠道来向我们反馈。
|
||||
|
||||
谢谢!
|
||||
-->
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,11 +1,8 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/**
|
||||
!**/src/main/**
|
||||
**/src/test/**
|
||||
|
||||
mvnw
|
||||
mvnw.cmd
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
@@ -21,6 +18,8 @@ mvnw.cmd
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
.fastRequest
|
||||
.murphy.yml
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
@@ -29,6 +28,12 @@ mvnw.cmd
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/.mvn/wrapper/
|
||||
/mvnw
|
||||
/mvnw.cmd
|
||||
/result/
|
||||
2
.package/script/log.sh
Normal file
2
.package/script/log.sh
Normal file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
tail -fn100 ~/.zfile-v4/logs/zfile.log
|
||||
6
.package/script/restart.sh
Normal file
6
.package/script/restart.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
$DIR/stop.sh
|
||||
$DIR/start.sh
|
||||
22
.package/script/start.sh
Normal file
22
.package/script/start.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 检测是否已启动
|
||||
pid=`ps -ef | grep -n zfile | grep -v grep | grep -v launch | grep -v .sh | awk '{print $2}'`
|
||||
if [ -n "${pid}" ]
|
||||
then
|
||||
echo "已运行在 pid:${pid},无需重复启动!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 获取当前脚本所在路径
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
ZFILE_DIR=$(dirname "$DIR")
|
||||
|
||||
# 启动 zfile
|
||||
nohup $ZFILE_DIR/zfile/zfile --spring.config.location=$ZFILE_DIR/application.properties --spring.web.resources.static-locations=file:$ZFILE_DIR/static/ >/dev/null 2>&1 &
|
||||
echo '启动中...'
|
||||
sleep 3s
|
||||
|
||||
# 输出 pid
|
||||
pid=`ps -ef | grep -n zfile | grep -v grep | grep -v .sh | awk '{print $2}'`
|
||||
echo "目前 PID 为: ${pid}"
|
||||
12
.package/script/status.sh
Normal file
12
.package/script/status.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "------------------ 检测状态 START --------------"
|
||||
pid=`ps -ef | grep -n zfile | grep -v grep | grep -v launch | grep -v .sh | awk '{print $2}'`
|
||||
if [ -z "${pid}" ]
|
||||
then
|
||||
echo "未运行, 无需停止!"
|
||||
else
|
||||
echo "运行pid:${pid}"
|
||||
fi
|
||||
|
||||
echo "------------------ 检测状态 END --------------"
|
||||
14
.package/script/stop.sh
Normal file
14
.package/script/stop.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "------------------ 检测状态 START --------------"
|
||||
pid=`ps -ef | grep -n zfile | grep -v grep | grep -v .sh | awk '{print $2}'`
|
||||
if [ -z "${pid}" ]
|
||||
then
|
||||
echo "未运行, 无需停止!"
|
||||
else
|
||||
echo "运行pid:${pid}"
|
||||
kill -9 ${pid}
|
||||
echo "已停止进程: ${pid}"
|
||||
fi
|
||||
|
||||
echo "------------------ 检测状态 END --------------"
|
||||
BIN
.package/script/vcruntime140_1.dll
Normal file
BIN
.package/script/vcruntime140_1.dll
Normal file
Binary file not shown.
7
.package/script/双击我启动.bat
Normal file
7
.package/script/双击我启动.bat
Normal file
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
if not exist %windir%\system32\cmd.exe (
|
||||
"%CD%\zfile\zfile.exe"
|
||||
) else (
|
||||
cmd /k "%CD%\zfile\zfile.exe"
|
||||
exit
|
||||
)
|
||||
164
API.md
164
API.md
@@ -1,164 +0,0 @@
|
||||
## API 标准
|
||||
|
||||
所有 API 均返回 `msg`, `code`, `data` 三个属性.
|
||||
|
||||
| code | 描述 |
|
||||
| :---: | :------------: |
|
||||
| 0 | 请求成功 |
|
||||
| -1 | 请求失败 |
|
||||
| -2 | 文件夹需要密码 |
|
||||
|
||||
当 `code == 0` 时, `data` 中为请求所需数据.
|
||||
|
||||
当 `code != 0` 时, 应当将 `msg` 中的属性作为参考值.
|
||||
|
||||
|
||||
## 获取文件列表
|
||||
|
||||
### 请求 URL
|
||||
|
||||
`/api/list` `GET`
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 描述 | 是否必填 | 参考值 |
|
||||
| :------: | :--------: | :------: | :--------------------------: |
|
||||
| path | 路径 | 是 | `/`, `/文件夹名称` |
|
||||
| password | 文件夹密码 | 否 | 当文件夹需要密码时, |
|
||||
| page | 页数 | 否 | 默认取第一页, 每页固定 30 条 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"name": "密码文件夹",
|
||||
"time": "2020-01-28 13:17",
|
||||
"size": 4096,
|
||||
"type": "FOLDER",
|
||||
"path": "/",
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"name": "新建 文本文档.txt",
|
||||
"time": "2020-01-28 13:16",
|
||||
"size": 3,
|
||||
"type": "FILE",
|
||||
"path": "/",
|
||||
"url": "http://127.0.0.1:8080/file/新建 文本文档.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 搜索
|
||||
|
||||
|
||||
### 请求 URL
|
||||
|
||||
`/api/search` `GET`
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 描述 | 是否必填 | 参考值 |
|
||||
| :----: | :----: | :------: | :--------------------------: |
|
||||
| name | 搜索值 | 是 | 模糊匹配 |
|
||||
| page | 页数 | 否 | 默认取第一页, 每页固定 30 条 |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 0,
|
||||
"data": [
|
||||
{
|
||||
"name": "密码文件夹",
|
||||
"time": "2020-01-28 13:17",
|
||||
"size": 4096,
|
||||
"type": "FOLDER",
|
||||
"path": "/",
|
||||
"url": null
|
||||
},
|
||||
{
|
||||
"name": "新建 文本文档.txt",
|
||||
"time": "2020-01-28 13:16",
|
||||
"size": 3,
|
||||
"type": "FILE",
|
||||
"path": "/",
|
||||
"url": "http://127.0.0.1:8080/file/新建 文本文档.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 获取单个文件信息
|
||||
|
||||
### 请求 URL
|
||||
|
||||
`/api/directlink` `GET`
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 描述 | 是否必填 | 参考值 |
|
||||
| :----: | :--------: | :------: | :------------------: |
|
||||
| path | 文件全路径 | 是 | `/新建 文本文档.txt` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"name": "新建 文本文档.txt",
|
||||
"time": "2020-01-28 13:16",
|
||||
"size": 3,
|
||||
"type": "FILE",
|
||||
"path": "d:/test",
|
||||
"url": "http://127.0.0.1:8080/file/新建 文本文档.txt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 获取系统配置
|
||||
|
||||
|
||||
### 请求 URL
|
||||
|
||||
`/api/config` `GET`
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数名 | 描述 | 是否必填 | 参考值 |
|
||||
| :----: | :--------: | :------: | :-----------: |
|
||||
| path | 文件夹名称 | 是 | `/文件夹名称` |
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 0,
|
||||
"data": {
|
||||
"readme": null, # 文档文件名称
|
||||
"viewConfig": {
|
||||
"siteName": "站点名称", # 站点名称
|
||||
"infoEnable": false, # 是否开启右侧信息框
|
||||
"searchEnable": false, # 是否开启搜索
|
||||
"searchIgnoreCase": true, # 搜索是否忽略大小写
|
||||
"storageStrategy": "local", # 当前启用存储引擎
|
||||
"username": "2", # 用户名
|
||||
"domain": "http://127.0.0.1:8080", # 域名
|
||||
"enableCache": false, # 是否开启缓存
|
||||
"searchContainEncryptedFile": false, # 搜索是否包含加密文件夹
|
||||
"customJs": "", # 自定义 js 片段
|
||||
"customCss": "" # 自定义 css 片段
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM debian:10-slim
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /root
|
||||
EXPOSE 8080
|
||||
|
||||
RUN apt update -y && apt install --no-install-recommends fontconfig zstd -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY zfile-artifacts/zfile-linux-${TARGETARCH}/zfile/* /root/
|
||||
COPY zfile-artifacts/zfile-linux-${TARGETARCH}/static/ /root/static/
|
||||
COPY zfile-artifacts/zfile-linux-${TARGETARCH}/application.properties /root/
|
||||
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' >/etc/timezone
|
||||
|
||||
# 设置编码为 UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
CMD if [ -f /root/zfile.zst ]; then zstd --no-progress -d /root/zfile.zst && rm -rf /root/zfile.zst && chmod +x /root/zfile && /root/zfile --spring.config.location=file:/root/application.properties; else chmod +x /root/zfile && /root/zfile --spring.config.location=file:/root/application.properties; fi
|
||||
182
README.md
182
README.md
@@ -1,144 +1,72 @@
|
||||
# Z-File
|
||||
# ZFile
|
||||
|
||||

|
||||
[](https://www.codacy.com/manual/zhaojun1998/zfile?utm_source=github.com&utm_medium=referral&utm_content=zhaojun1998/zfile&utm_campaign=Badge_Grade)
|
||||

|
||||

|
||||
[](https://github.com/zfile-dev/zfile/blob/main/LICENSE)
|
||||
[](https://github.com/zfile-dev/zfile/releases)
|
||||
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a"/>
|
||||
[](https://hub.docker.com/r/zhaojun1998/zfile)
|
||||
[](https://www.bt.cn/u/WYVNdM)
|
||||
|
||||
此项目是一个在线文件目录的程序, 支持各种对象存储和本地存储, 使用定位是个人放常用工具下载, 或做公共的文件库. 不会向多账户方向开发.
|
||||
## ZFile 是什么?
|
||||
|
||||
前端基于 [h5ai](https://larsjung.de/h5ai/) 的原有功能使用 Vue 重新开发了一遍. 后端采用 SpringBoot, 数据库采用内嵌数据库.
|
||||
ZFile 是一个适用于个人的在线网盘(列目录)程序,可以将你各个存储类型的存储源,统一到一个网页中查看、预览、维护,再也不用去登录各种各样的网页登录后管理文件,现在你只需要在 ZFile 中使用。你只需要填写存储源相关信息,其他的令牌刷新,授权都是尽量自动化的,且有完善的文档帮助你使用。
|
||||
|
||||
预览地址: [https://zfile.jun6.net](https://zfile.jun6.net)
|
||||
|
||||
文档地址: [http://docs.zhaojun.im/zfile](http://docs.zhaojun.im/zfile)
|
||||
|
||||
## 系统特色
|
||||
|
||||
* 内存缓存 (免安装)
|
||||
* 内存数据库 (免安装)
|
||||
* 个性化配置
|
||||
* 自定义目录的 readme 说明文件
|
||||
* 自定义 JS, CSS
|
||||
* 文件夹密码
|
||||
* 支持在线浏览文本文件, 视频, 图片, 音乐. (支持 FLV 和 HLS)
|
||||
* 文件/目录二维码
|
||||
* 缓存动态开启, ~~缓存自动刷新 (v2.2 及以前版本支持)~~
|
||||
* ~~全局搜索 (v2.2 及以前版本支持)~~
|
||||
* 同时挂载多个存储策略
|
||||
* 支持 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版, OneDrive 世纪互联版, 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
|
||||
- 支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源
|
||||
- 支持在线浏览图片、播放音视频,文本文件、Office、obj(3d)等文件类型。
|
||||
|
||||
## 快速开始
|
||||
|
||||
安装依赖环境:
|
||||
请参考部署文档: [https://docs.zfile.vip](https://docs.zfile.vip)
|
||||
|
||||
```bash
|
||||
# CentOS系统
|
||||
yum install -y java-1.8.0-openjdk unzip
|
||||
```
|
||||
## 在线体验
|
||||
|
||||
```bash
|
||||
# Debian 9 / Ubuntu 14+
|
||||
apt update
|
||||
apt install -y openjdk-8-jre-headless unzip
|
||||
```
|
||||
[https://demo.zfile.vip](https://demo.zfile.vip)
|
||||
|
||||
```bash
|
||||
# Debian 10 (Buster) 系统
|
||||
apt update && apt install -y apt-transport-https software-properties-common ca-certificates dirmngr gnupg
|
||||
wget -qO - https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public | apt-key add -
|
||||
add-apt-repository --yes https://adoptopenjdk.jfrog.io/adoptopenjdk/deb/
|
||||
apt update && apt install -y adoptopenjdk-8-hotspot-jre
|
||||
```
|
||||
## 功能预览
|
||||
|
||||
### 文件列表
|
||||

|
||||
### 画廊模式
|
||||

|
||||
### 视频预览
|
||||

|
||||
### 文本预览
|
||||

|
||||
### 音频预览
|
||||

|
||||
### PDF 预览
|
||||

|
||||
### Office 预览
|
||||

|
||||
### 3d 文件预览
|
||||

|
||||
### 生成直链
|
||||

|
||||
### 页面设置
|
||||

|
||||
### 后台设置-登录
|
||||

|
||||
### 后台设置-存储源列表
|
||||

|
||||
### 后台设置-存储源权限控制
|
||||

|
||||
### 后台设置-添加存储源(本地存储)
|
||||

|
||||
### 后台设置-添加存储源(世纪互联)
|
||||

|
||||
### 后台设置-显示设置
|
||||

|
||||
|
||||
> 如为更新程序, 则请先执行 `~/zfile/bin/stop.sh && rm -rf ~/zfile` 清理旧程序. 首次安装请忽略此选项.
|
||||
|
||||
|
||||
下载项目:
|
||||
|
||||
```bash
|
||||
cd ~
|
||||
wget https://c.jun6.net/ZFILE/zfile-release.war
|
||||
mkdir zfile && unzip zfile-release.war -d zfile && rm -rf zfile-release.war
|
||||
chmod +x zfile/bin/*.sh
|
||||
```
|
||||
|
||||
> 下载指定版本可以将 `zfile-release.war` 改为 `zfile-x.x.war`,如 `zfile-2.2.war`。
|
||||
|
||||
程序的目录结构为:
|
||||
```
|
||||
├── zfile
|
||||
├── META-INF
|
||||
├── WEB-INF
|
||||
└── bin
|
||||
├── start.sh # 启动脚本
|
||||
└── stop.sh # 停止脚本
|
||||
├── restart.sh # 重启脚本
|
||||
```
|
||||
|
||||
启动项目:
|
||||
|
||||
```bash
|
||||
~/zfile/bin/start.sh
|
||||
```
|
||||
|
||||
篇幅有限, 更详细的安装教程及介绍请参考: [ZFile 文档](http://zhaojun.im/zfile-install)
|
||||
|
||||
访问地址:
|
||||
|
||||
用户前台: http://127.0.0.1:8080/#/main
|
||||
|
||||
初始安装: http://127.0.0.1:8080/#/install
|
||||
|
||||
管理后台: http://127.0.0.1:8080/#/admin
|
||||
|
||||
|
||||
## 预览
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 常见问题
|
||||
|
||||
|
||||
### 默认路径
|
||||
|
||||
默认 H2 数据库文件地址: `~/.zfile/db/`, `~` 表示用户目录
|
||||
|
||||
windows 为 `C:/Users/用户名/`
|
||||
|
||||
linux 为 `/home/用户名/`, root 用户为 `/root/`
|
||||
|
||||
> 2.3 及以后版本路径为 `~/.zfile-new/db/`
|
||||
|
||||
### 文档文件和加密文件
|
||||
|
||||
- 目录文档显示文件名为 `readme.md`
|
||||
- 目录需要密码访问, 添加文件 `password.txt` (无法拦截此文件被下载, 但可以改名文件)
|
||||
|
||||
## 开发计划
|
||||
|
||||
- [x] API 支持 [点击查看文档](https://github.com/zhaojun1998/zfile/blob/master/API.md)
|
||||
- [x] 更方便的部署方式
|
||||
- [x] 布局优化 - 自定义操作按钮 (现为右键实现)
|
||||
- [x] 后台优化 - 设置按照其功能进行分离
|
||||
- [x] 体验优化 - 支持前后端分离部署
|
||||
- [x] 体验优化 - 文本预览更换 vscode 同款编辑器 monaco editor
|
||||
- [x] 新功能 - Docker 支持
|
||||
- [x] 架构调整 - 支持多存储策略
|
||||
- [ ] 新功能 - 后台支持上传、编辑、删除等操作
|
||||
- [ ] 新功能 - WebDav 支持
|
||||
- [ ] 新功能 - 离线下载 (aria2)
|
||||
- [ ] 体验优化 - 忽略文件列表 (正则表达式)
|
||||
- [ ] 体验优化 - 自定义支持预览的文件后缀 (正则表达式)
|
||||
- [ ] 体验优化 - 一键安装脚本
|
||||
|
||||
## 支持作者
|
||||
|
||||
如果本项目对你有帮助,请作者喝杯咖啡吧。
|
||||
|
||||
<img src="http://cdn.jun6.net/alipay.png" width="200" height="312">
|
||||
<img src="http://cdn.jun6.net/wechat.png" width="222" height="300">
|
||||
<img src="https://cdn.jun6.net/2021/03/27/152704e91f13d.png" width="400" alt="赞助我">
|
||||
|
||||
## Status
|
||||
|
||||

|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#zfile-dev/zfile&Date)
|
||||
|
||||
363
pom.xml
363
pom.xml
@@ -1,29 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>im.zhaojun</groupId>
|
||||
<artifactId>zfile</artifactId>
|
||||
<version>4.2.0</version>
|
||||
<name>zfile</name>
|
||||
<packaging>jar</packaging>
|
||||
<description>一个在线的文件浏览系统</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.6.RELEASE</version>
|
||||
<relativePath/>
|
||||
<version>3.3.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>im.zhaojun</groupId>
|
||||
<artifactId>zfile</artifactId>
|
||||
<version>2.4</version>
|
||||
<name>zfile</name>
|
||||
<packaging>war</packaging>
|
||||
<description>一个在线的文件浏览系统</description>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<skipTests>true</skipTests>
|
||||
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
|
||||
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
||||
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
|
||||
<snakeyaml.version>2.0</snakeyaml.version>
|
||||
<jackson-bom.version>2.14.1</jackson-bom.version>
|
||||
<sqlite-jdbc.version>3.46.0.1</sqlite-jdbc.version>
|
||||
<flyway.version>10.12.0</flyway.version>
|
||||
|
||||
<lombok.version>1.18.32</lombok.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>bom</artifactId>
|
||||
<version>2.24.3</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<!-- spring boot 官方相关 -->
|
||||
<dependencies>
|
||||
<!-- spring boot 官方相关-->
|
||||
<dependency>
|
||||
<groupId>org.graalvm.sdk</groupId>
|
||||
<artifactId>graal-sdk</artifactId>
|
||||
<version>24.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
@@ -32,71 +64,140 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据库驱动-->
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据库相关 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
<version>${flyway.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
<version>${flyway.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.1.3</version>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.6</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 存储策略相关 API, 对象存储、FTP、 Rest API-->
|
||||
<dependency>
|
||||
<groupId>com.upyun</groupId>
|
||||
<artifactId>java-sdk</artifactId>
|
||||
<version>4.1.3</version>
|
||||
<version>4.2.3</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/s3 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>1.11.699</version>
|
||||
<groupId>com.qiniu</groupId>
|
||||
<artifactId>qiniu-java-sdk</artifactId>
|
||||
<version>7.12.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.8</version>
|
||||
<groupId>com.github.mwiede</groupId>
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.2.20</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.6</version>
|
||||
<groupId>com.github.lookfirst</groupId>
|
||||
<artifactId>sardine</artifactId>
|
||||
<version>5.12</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.api-client</groupId>
|
||||
<artifactId>google-api-client</artifactId>
|
||||
<version>1.35.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 登陆/权限相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>1.38.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 文档相关 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>4.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>com.hierynomus</groupId>-->
|
||||
<!-- <artifactId>sshj</artifactId>-->
|
||||
<!-- <version>0.38.0</version>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.28</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.26.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- 其他工具类 -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@@ -104,15 +205,89 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mpatric</groupId>
|
||||
<artifactId>mp3agic</artifactId>
|
||||
<version>0.9.1</version>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.29</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.2.0-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-chain</groupId>
|
||||
<artifactId>commons-chain</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.samstevens.totp</groupId>
|
||||
<artifactId>totp</artifactId>
|
||||
<version>1.7.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.beust</groupId>
|
||||
<artifactId>jcommander</artifactId>
|
||||
<version>1.82</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.70</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.61</version>
|
||||
<artifactId>dns-cache-manipulator</artifactId>
|
||||
<version>1.8.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.6.3</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
@@ -124,25 +299,81 @@
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.uyoqu.framework</groupId>
|
||||
<artifactId>maven-plugin-starter</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>bin</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<jvms>
|
||||
<jvm>-Djava.security.egd=file:/dev/./urandom</jvm>
|
||||
<jvm>-Dfile.encoding=utf-8</jvm>
|
||||
</jvms>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.32</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-native</id>
|
||||
<goals>
|
||||
<goal>compile-no-fork</goal>
|
||||
</goals>
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<fallback>false</fallback>
|
||||
<imageName>${project.name}</imageName>
|
||||
<metadataRepository>
|
||||
<enabled>true</enabled>
|
||||
</metadataRepository>
|
||||
<jvmArgs>
|
||||
<jvmArg>--add-opens=java.base/java.net=ALL-UNNAMED</jvmArg>
|
||||
<jvmArg>--add-opens=java.base/sun.net=ALL-UNNAMED</jvmArg>
|
||||
</jvmArgs>
|
||||
<buildArgs>
|
||||
<arg>
|
||||
-march=compatibility
|
||||
-H:+AddAllCharsets
|
||||
--features=im.zhaojun.zfile.aot.LambdaRegistrationFeature
|
||||
--features=im.zhaojun.zfile.aot.BouncyCastleFeature
|
||||
--features=im.zhaojun.zfile.aot.SQLiteNativeConfiguration
|
||||
</arg>
|
||||
</buildArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
</project>
|
||||
@@ -2,19 +2,22 @@ package im.zhaojun.zfile;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@EnableAsync
|
||||
@SpringBootApplication
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
|
||||
@ServletComponentScan(basePackages = {"im.zhaojun.zfile.core.filter", "im.zhaojun.zfile.module.storage.filter"})
|
||||
@ComponentScan(basePackages = "im.zhaojun.zfile.*")
|
||||
public class ZfileApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ZfileApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package im.zhaojun.zfile.aspect;
|
||||
|
||||
import im.zhaojun.zfile.cache.ZFileCache;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
* 缓存切面类, 用于访问文件夹时, 缓存文件列表内容.
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class FileListCacheAop {
|
||||
|
||||
@Resource
|
||||
private ZFileCache zFileCache;
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
|
||||
/**
|
||||
* 缓存切面, 如果此驱动器开启了缓存, 则从缓存中取数据, 没有开启, 则直接调用方法.
|
||||
*/
|
||||
@Around(value = "execution(public * im.zhaojun.zfile.service.base.AbstractBaseFileService.fileList(..))")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
List<FileItemDTO> result;
|
||||
|
||||
// 获取请求路径
|
||||
Object[] args = point.getArgs();
|
||||
String path = String.valueOf(args[0]);
|
||||
|
||||
// 获取当前驱动器
|
||||
AbstractBaseFileService fileService = ((AbstractBaseFileService) point.getTarget());
|
||||
Integer driveId = fileService.driveId;
|
||||
|
||||
// 判断驱动器是否开启了缓存
|
||||
DriveConfig driveConfig = driveConfigService.findById(driveId);
|
||||
boolean enableCache = driveConfig.getEnableCache();
|
||||
|
||||
if (enableCache) {
|
||||
List<FileItemDTO> cacheFileList = zFileCache.get(driveId, path);
|
||||
if (cacheFileList == null) {
|
||||
result = (List<FileItemDTO>) point.proceed();
|
||||
zFileCache.put(driveId, path, result);
|
||||
} else {
|
||||
result = cacheFileList;
|
||||
}
|
||||
} else {
|
||||
result = (List<FileItemDTO>) point.proceed();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
254
src/main/java/im/zhaojun/zfile/cache/ZFileCache.java
vendored
254
src/main/java/im/zhaojun/zfile/cache/ZFileCache.java
vendored
@@ -1,254 +0,0 @@
|
||||
package im.zhaojun.zfile.cache;
|
||||
|
||||
import cn.hutool.cache.CacheUtil;
|
||||
import cn.hutool.cache.impl.CacheObj;
|
||||
import cn.hutool.cache.impl.TimedCache;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import im.zhaojun.zfile.repository.DriverConfigRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* ZFile 缓存类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class ZFileCache {
|
||||
|
||||
@Value("${zfile.cache.timeout}")
|
||||
private long timeout;
|
||||
|
||||
|
||||
/**
|
||||
* 缓存 map 对象.
|
||||
*
|
||||
* ConcurrentMap<Integer, ConcurrentHashMap<String, List<FileItemDTO>>>
|
||||
* ConcurrentMap<driveId, ConcurrentHashMap<key, value>>
|
||||
*
|
||||
* driveId: 驱动器 ID
|
||||
* key: 文件夹路径
|
||||
* value: 文件夹中内容
|
||||
*/
|
||||
private ConcurrentMap<Integer, TimedCache<String, List<FileItemDTO>>> drivesCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 系统设置缓存
|
||||
*/
|
||||
private SystemConfigDTO systemConfigCache;
|
||||
|
||||
@Resource
|
||||
private DriverConfigRepository driverConfigRepository;
|
||||
|
||||
|
||||
/**
|
||||
* 写入缓存
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param key
|
||||
* 文件夹路径
|
||||
*
|
||||
* @param value
|
||||
* 文件夹中列表
|
||||
*/
|
||||
public synchronized void put(Integer driveId, String key, List<FileItemDTO> value) {
|
||||
getCacheByDriveId(driveId).put(key, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器, 某个文件夹的名称
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param key
|
||||
* 文件夹路径
|
||||
*
|
||||
* @return 驱动器中文件夹的内容
|
||||
*/
|
||||
public List<FileItemDTO> get(Integer driveId, String key) {
|
||||
return getCacheByDriveId(driveId).get(key, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清空指定驱动器的缓存.
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*/
|
||||
public void clear(Integer driveId) {
|
||||
getCacheByDriveId(driveId).clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器中已缓存文件夹数量
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 已缓存文件夹数量
|
||||
*/
|
||||
public int cacheCount(Integer driveId) {
|
||||
return getCacheByDriveId(driveId).size();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 指定驱动器, 根据文件及文件名查找相关的文件
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param key
|
||||
* 搜索键, 可匹配文件夹名称和文件名称.
|
||||
*
|
||||
* @return 搜索结果, 包含文件夹和文件.
|
||||
*/
|
||||
public List<FileItemDTO> find(Integer driveId, String key) {
|
||||
List<FileItemDTO> result = new ArrayList<>();
|
||||
|
||||
DriveConfig driveConfig = driverConfigRepository.getOne(driveId);
|
||||
boolean searchContainEncryptedFile = driveConfig.getSearchContainEncryptedFile();
|
||||
boolean ignoreCase = driveConfig.getSearchIgnoreCase();
|
||||
|
||||
for (List<FileItemDTO> fileItemList : getCacheByDriveId(driveId)) {
|
||||
// 过滤加密文件
|
||||
if (!searchContainEncryptedFile && isEncryptedFolder(fileItemList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FileItemDTO fileItemDTO : fileItemList) {
|
||||
boolean testResult;
|
||||
|
||||
// 根据是否需要忽略大小写来匹配文件(夹)名
|
||||
if (ignoreCase) {
|
||||
testResult = StrUtil.containsIgnoreCase(fileItemDTO.getName(), key);
|
||||
} else {
|
||||
testResult = fileItemDTO.getName().contains(key);
|
||||
}
|
||||
|
||||
if (testResult) {
|
||||
result.add(fileItemDTO);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有缓存 key (文件夹名称)
|
||||
*
|
||||
* @return 所有缓存 key
|
||||
*/
|
||||
public Set<String> keySet(Integer driveId) {
|
||||
Iterator<CacheObj<String, List<FileItemDTO>>> cacheObjIterator = getCacheByDriveId(driveId).cacheObjIterator();
|
||||
Set<String> keys = new HashSet<>();
|
||||
while (cacheObjIterator.hasNext()) {
|
||||
keys.add(cacheObjIterator.next().getKey());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从缓存中删除指定存储器的某个路径的缓存
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param key
|
||||
* 文件夹路径
|
||||
*/
|
||||
public void remove(Integer driveId, String key) {
|
||||
getCacheByDriveId(driveId).remove(key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新缓存中的系统设置
|
||||
*
|
||||
* @param systemConfigCache
|
||||
* 系统设置
|
||||
*/
|
||||
public void updateConfig(SystemConfigDTO systemConfigCache) {
|
||||
this.systemConfigCache = systemConfigCache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从缓存中获取系统设置
|
||||
*
|
||||
* @return 系统设置
|
||||
*/
|
||||
public SystemConfigDTO getConfig() {
|
||||
return this.systemConfigCache;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清空系统设置缓存
|
||||
*/
|
||||
public void removeConfig() {
|
||||
this.systemConfigCache = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断此文件夹是否为加密文件夹 (包含)
|
||||
*
|
||||
* @param list
|
||||
* 文件夹中的内容
|
||||
*
|
||||
* @return 返回此文件夹是否是加密的 ().
|
||||
*/
|
||||
private boolean isEncryptedFolder(List<FileItemDTO> list) {
|
||||
// 遍历文件判断是否包含
|
||||
for (FileItemDTO fileItemDTO : list) {
|
||||
if (Objects.equals(ZFileConstant.PASSWORD_FILE_NAME, fileItemDTO.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器对应的缓存
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 驱动器对应的缓存
|
||||
*/
|
||||
private synchronized TimedCache<String, List<FileItemDTO>> getCacheByDriveId(Integer driveId) {
|
||||
TimedCache<String, List<FileItemDTO>> driveCache = drivesCache.get(driveId);
|
||||
if (driveCache == null) {
|
||||
driveCache = CacheUtil.newTimedCache(timeout * 1000);
|
||||
drivesCache.put(driveId, driveCache);
|
||||
}
|
||||
|
||||
return driveCache;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package im.zhaojun.zfile.config;
|
||||
|
||||
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
|
||||
import im.zhaojun.zfile.model.entity.StorageConfig;
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.service.StorageConfigService;
|
||||
import im.zhaojun.zfile.service.impl.OneDriveChinaServiceImpl;
|
||||
import im.zhaojun.zfile.service.impl.OneDriveServiceImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class OneDriveConfig {
|
||||
|
||||
@Resource
|
||||
private StorageConfigService storageConfigService;
|
||||
|
||||
@Resource
|
||||
private OneDriveServiceImpl oneDriveServiceImpl;
|
||||
|
||||
@Resource
|
||||
private OneDriveChinaServiceImpl oneDriveChinaServiceImpl;
|
||||
|
||||
|
||||
/**
|
||||
* OneDrive 请求 RestTemplate, 会在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证.
|
||||
*/
|
||||
@Bean
|
||||
public RestTemplate oneDriveRestTemplate() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, clientHttpRequestExecution) -> {
|
||||
HttpHeaders headers = httpRequest.getHeaders();
|
||||
Integer driveId = Integer.valueOf(((LinkedList)headers.get("driveId")).get(0).toString());
|
||||
|
||||
StorageConfig accessTokenConfig =
|
||||
storageConfigService.findByDriveIdAndKey(driveId, StorageConfigConstant.ACCESS_TOKEN_KEY);
|
||||
|
||||
String tokenValue = String.format("%s %s", "Bearer", accessTokenConfig.getValue());
|
||||
httpRequest.getHeaders().add("Authorization", tokenValue);
|
||||
return clientHttpRequestExecution.execute(httpRequest, bytes);
|
||||
};
|
||||
restTemplate.setInterceptors(Collections.singletonList(interceptor));
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package im.zhaojun.zfile.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class ZFileConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(){
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||
|
||||
restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> {
|
||||
ClientHttpResponse response = execution.execute(request, body);
|
||||
HttpHeaders headers = response.getHeaders();
|
||||
headers.put("Content-Type", Collections.singletonList("application/text"));
|
||||
return response;
|
||||
}));
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package im.zhaojun.zfile.context;
|
||||
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.util.SpringContextHolder;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 驱动器上下文环境
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
@DependsOn("springContextHolder")
|
||||
public class DriveContext implements ApplicationContextAware {
|
||||
|
||||
private static Map<Integer, AbstractBaseFileService> drivesServiceMap = new ConcurrentHashMap<>();
|
||||
|
||||
private static Map<StorageTypeEnum, Class<AbstractBaseFileService>> storageTypeEnumClassMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
|
||||
/**
|
||||
* 初始化指定驱动器的 Service, 添加到上下文环境中.
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID.
|
||||
*/
|
||||
public void initDrive(Integer driveId) {
|
||||
AbstractBaseFileService baseFileService = getBeanByDriveId(driveId);
|
||||
if (baseFileService != null) {
|
||||
baseFileService.init(driveId);
|
||||
drivesServiceMap.put(driveId, baseFileService);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器的 Service.
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 驱动器对应的 Service
|
||||
*/
|
||||
public AbstractBaseFileService getDriveService(Integer driveId) {
|
||||
return drivesServiceMap.get(driveId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 销毁指定驱动器的 Service.
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*/
|
||||
public void destroyDrive(Integer driveId) {
|
||||
drivesServiceMap.remove(driveId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器对应的 Service, 状态为未初始化
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 驱动器对应未初始化的 Service
|
||||
*/
|
||||
private AbstractBaseFileService getBeanByDriveId(Integer driveId) {
|
||||
StorageTypeEnum storageTypeEnum = driveConfigService.findStorageTypeById(driveId);
|
||||
Map<String, AbstractBaseFileService> beansOfType = SpringContextHolder.getBeansOfType(AbstractBaseFileService.class);
|
||||
for (AbstractBaseFileService value : beansOfType.values()) {
|
||||
if (Objects.equals(value.getStorageTypeEnum(), storageTypeEnum)) {
|
||||
return SpringContextHolder.getBean(value.getClass());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 项目启动时, 自动调用所有驱动器进行初始化.
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
List<DriveConfig> list = driveConfigService.list();
|
||||
for (DriveConfig driveConfig : list) {
|
||||
initDrive(driveConfig.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package im.zhaojun.zfile.context;
|
||||
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* 存储类型工厂类
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class StorageTypeContext implements ApplicationContextAware {
|
||||
|
||||
private static Map<String, AbstractBaseFileService> storageTypeEnumFileServiceMap;
|
||||
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
|
||||
/**
|
||||
* 项目启动时执行
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext act) throws BeansException {
|
||||
applicationContext = act;
|
||||
|
||||
// 获取 Spring 容器中所有 FileService 类型的类
|
||||
storageTypeEnumFileServiceMap = act.getBeansOfType(AbstractBaseFileService.class);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定存储类型 Service
|
||||
*/
|
||||
public static AbstractBaseFileService getStorageTypeService(StorageTypeEnum type) {
|
||||
AbstractBaseFileService result = null;
|
||||
for (AbstractBaseFileService fileService : storageTypeEnumFileServiceMap.values()) {
|
||||
if (fileService.getStorageTypeEnum() == type) {
|
||||
result = fileService;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.util.AudioHelper;
|
||||
import im.zhaojun.zfile.util.HttpUtil;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 公共 Controller
|
||||
* @author zhaojun
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/common")
|
||||
public class CommonController {
|
||||
|
||||
|
||||
/**
|
||||
* 返回系统支持的所有存储策略
|
||||
*
|
||||
* @return 存储策略
|
||||
*/
|
||||
@GetMapping("/support-strategy")
|
||||
public ResultBean supportStrategy() {
|
||||
return ResultBean.successData(StorageTypeEnum.values());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取文件内容, 仅限用于 txt, md, ini 等普通文本文件.
|
||||
*
|
||||
* @param url
|
||||
* 文件路径
|
||||
*
|
||||
* @return 文件内容
|
||||
*/
|
||||
@GetMapping("/content")
|
||||
public ResultBean getContent(String url) {
|
||||
return ResultBean.successData(HttpUtil.getTextContent(url));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取音频文件信息
|
||||
*
|
||||
* @param url
|
||||
* 文件 URL
|
||||
*
|
||||
* @return 音频信息, 标题封面等信息
|
||||
*/
|
||||
@GetMapping("/audio-info")
|
||||
public ResultBean getAudioInfo(String url) throws Exception {
|
||||
return ResultBean.success(AudioHelper.getAudioInfo(url));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.model.dto.SystemFrontConfigDTO;
|
||||
import im.zhaojun.zfile.model.support.FilePageModel;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import im.zhaojun.zfile.service.SystemConfigService;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import im.zhaojun.zfile.util.FileComparator;
|
||||
import im.zhaojun.zfile.util.HttpUtil;
|
||||
import im.zhaojun.zfile.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 前台文件管理
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@RequestMapping("/api")
|
||||
@RestController
|
||||
public class FileController {
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
@Resource
|
||||
private DriveContext driveContext;
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
/**
|
||||
* 滚动加载每页条数.
|
||||
*/
|
||||
private static final Integer PAGE_SIZE = 30;
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有驱动器
|
||||
*
|
||||
* @return 所有驱动器
|
||||
*/
|
||||
@GetMapping("/drive/list")
|
||||
public ResultBean drives() {
|
||||
return ResultBean.success(driveConfigService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某个驱动器下, 指定路径的数据, 每页固定 {@link #PAGE_SIZE} 条数据.
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param path
|
||||
* 路径
|
||||
*
|
||||
* @param password
|
||||
* 文件夹密码, 某些文件夹需要密码才能访问, 当不需要密码时, 此参数可以为空
|
||||
*
|
||||
* @param page
|
||||
* 页数
|
||||
*
|
||||
* @return 当前路径下所有文件及文件夹
|
||||
*/
|
||||
@GetMapping("/list/{driveId}")
|
||||
public ResultBean list(@PathVariable(name = "driveId") Integer driveId,
|
||||
@RequestParam(defaultValue = "/") String path,
|
||||
@RequestParam(required = false) String password,
|
||||
@RequestParam(defaultValue = "1") Integer page) throws Exception {
|
||||
AbstractBaseFileService fileService = driveContext.getDriveService(driveId);
|
||||
List<FileItemDTO> fileItemList = fileService.fileList(StringUtils.removeDuplicateSeparator("/" + path + "/"));
|
||||
|
||||
for (FileItemDTO fileItemDTO : fileItemList) {
|
||||
if (ZFileConstant.PASSWORD_FILE_NAME.equals(fileItemDTO.getName())) {
|
||||
String expectedPasswordContent;
|
||||
try {
|
||||
expectedPasswordContent = HttpUtil.getTextContent(fileItemDTO.getUrl());
|
||||
} catch (HttpClientErrorException httpClientErrorException) {
|
||||
log.debug("尝试重新获取密码文件缓存中链接后仍失败", httpClientErrorException);
|
||||
try {
|
||||
String fullPath = StringUtils.removeDuplicateSeparator(fileItemDTO.getPath() + "/" + fileItemDTO.getName());
|
||||
FileItemDTO fileItem = fileService.getFileItem(fullPath);
|
||||
expectedPasswordContent = HttpUtil.getTextContent(fileItem.getUrl());
|
||||
} catch (Exception e) {
|
||||
log.debug("尝试重新获取密码文件链接后仍失败, 已暂时取消密码", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.equals(expectedPasswordContent, password)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (password != null && !"".equals(password)) {
|
||||
return ResultBean.error("密码错误.", ResultBean.INVALID_PASSWORD);
|
||||
}
|
||||
return ResultBean.error("此文件夹需要密码.", ResultBean.REQUIRED_PASSWORD);
|
||||
}
|
||||
}
|
||||
return ResultBean.successData(getSortedPagingData(fileItemList, page));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统配置信息和当前页的标题, 页面文档信息
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 返回指定存储器的系统配置信息
|
||||
*/
|
||||
@GetMapping("/config/{driveId}")
|
||||
public ResultBean getConfig(@PathVariable(name = "driveId") Integer driveId, String path) {
|
||||
SystemFrontConfigDTO systemConfig = systemConfigService.getSystemFrontConfig(driveId);
|
||||
|
||||
AbstractBaseFileService fileService = driveContext.getDriveService(driveId);
|
||||
String fullPath = StringUtils.removeDuplicateSeparator(path + "/" + ZFileConstant.README_FILE_NAME);
|
||||
try {
|
||||
FileItemDTO fileItem = fileService.getFileItem(fullPath);
|
||||
String readme = HttpUtil.getTextContent(fileItem.getUrl());
|
||||
systemConfig.setReadme(readme);
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return ResultBean.successData(systemConfig);
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/search/{driveId}")
|
||||
public ResultBean search(@RequestParam(value = "name", defaultValue = "/") String name,
|
||||
@RequestParam(defaultValue = "name") String sortBy,
|
||||
@RequestParam(defaultValue = "asc") String order,
|
||||
@RequestParam(defaultValue = "1") Integer page,
|
||||
@PathVariable("driveId") Integer driveId) {
|
||||
return ResultBean.error("暂不支持搜索功能");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 过滤文件列表, 去除密码, 文档文件.
|
||||
*
|
||||
* @param fileItemList
|
||||
* 文件列表
|
||||
*/
|
||||
private void filterFileList(List<FileItemDTO> fileItemList) {
|
||||
if (fileItemList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fileItemList.removeIf(fileItem -> ZFileConstant.PASSWORD_FILE_NAME.equals(fileItem.getName())
|
||||
|| ZFileConstant.README_FILE_NAME.equals(fileItem.getName()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 对传入的文件列表, 按照文件名进行排序, 然后取相应页数的文件
|
||||
*
|
||||
* @param fileItemList
|
||||
* 文件列表
|
||||
*
|
||||
* @param page
|
||||
* 要取的页数
|
||||
*
|
||||
* @return 排序及分页后的那段数据
|
||||
*/
|
||||
private FilePageModel getSortedPagingData(List<FileItemDTO> fileItemList, Integer page) {
|
||||
ArrayList<FileItemDTO> copy = new ArrayList<>(Arrays.asList(new FileItemDTO[fileItemList.size()]));
|
||||
Collections.copy(copy, fileItemList);
|
||||
|
||||
// 排序, 先按照文件类型比较, 文件夹在前, 文件在后, 然后根据 sortBy 字段排序, 默认为升序;
|
||||
copy.sort(new FileComparator());
|
||||
filterFileList(copy);
|
||||
|
||||
int total = copy.size();
|
||||
int totalPage = (total + PAGE_SIZE - 1) / PAGE_SIZE;
|
||||
|
||||
if (page > totalPage) {
|
||||
return new FilePageModel(totalPage, Collections.emptyList());
|
||||
}
|
||||
|
||||
int start = (page - 1) * PAGE_SIZE;
|
||||
int end = page * PAGE_SIZE;
|
||||
end = Math.min(end, total);
|
||||
return new FilePageModel(totalPage, copy.subList(start, end));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定路径下的文件信息内容
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param path
|
||||
* 文件全路径
|
||||
*
|
||||
* @return 该文件的名称, 路径, 大小, 下载地址等信息.
|
||||
*/
|
||||
@GetMapping("/directlink/{driveId}")
|
||||
public ResultBean directlink(@PathVariable(name = "driveId") Integer driveId, String path) {
|
||||
AbstractBaseFileService fileService = driveContext.getDriveService(driveId);
|
||||
return ResultBean.successData(fileService.getFileItem(path));
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import im.zhaojun.zfile.controller.admin.AdminController;
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.service.StorageConfigService;
|
||||
import im.zhaojun.zfile.service.SystemConfigService;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 系统安装初始化
|
||||
* @author zhaojun
|
||||
*/
|
||||
@RestController
|
||||
public class InstallController {
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
@Resource
|
||||
private StorageConfigService storageConfigService;
|
||||
|
||||
@Resource
|
||||
private AdminController adminController;
|
||||
|
||||
@GetMapping("/is-installed")
|
||||
public ResultBean isInstall() {
|
||||
if (!StringUtils.isEmpty(systemConfigService.getAdminUsername())) {
|
||||
return ResultBean.error("请勿重复初始化");
|
||||
}
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/install")
|
||||
public ResultBean install(SystemConfigDTO systemConfigDTO) {
|
||||
if (!StringUtils.isEmpty(systemConfigService.getAdminUsername())) {
|
||||
return ResultBean.error("请勿重复初始化.");
|
||||
}
|
||||
|
||||
systemConfigDTO.setPassword(SecureUtil.md5(systemConfigDTO.getPassword()));
|
||||
systemConfigService.updateSystemConfig(systemConfigDTO);
|
||||
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import im.zhaojun.zfile.service.impl.LocalServiceImpl;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import im.zhaojun.zfile.util.FileUtil;
|
||||
import im.zhaojun.zfile.util.StringUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 本地存储 Controller
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Controller
|
||||
public class LocalController {
|
||||
|
||||
@Resource
|
||||
private DriveContext driveContext;
|
||||
|
||||
/**
|
||||
* 本地存储下载指定文件
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 文件
|
||||
*/
|
||||
@GetMapping("/file/{driveId}/**")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Object> downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
|
||||
String path = (String) request.getAttribute(
|
||||
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
AntPathMatcher apm = new AntPathMatcher();
|
||||
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
|
||||
LocalServiceImpl localService = (LocalServiceImpl) driveContext.getDriveService(driveId);
|
||||
return FileUtil.export(new File(StringUtils.concatPath(localService.getFilePath(), filePath)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import im.zhaojun.zfile.model.support.OneDriveToken;
|
||||
import im.zhaojun.zfile.service.impl.OneDriveChinaServiceImpl;
|
||||
import im.zhaojun.zfile.service.impl.OneDriveServiceImpl;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Controller
|
||||
@RequestMapping("/onedrive")
|
||||
public class OneDriveController {
|
||||
|
||||
@Resource
|
||||
private OneDriveServiceImpl oneDriveServiceImpl;
|
||||
|
||||
@Resource
|
||||
private OneDriveChinaServiceImpl oneDriveChinaServiceImpl;
|
||||
|
||||
@GetMapping("/callback")
|
||||
public String oneDriveCallback(String code, Model model) {
|
||||
OneDriveToken oneDriveToken = oneDriveServiceImpl.getToken(code);
|
||||
model.addAttribute("accessToken", oneDriveToken.getAccessToken());
|
||||
model.addAttribute("refreshToken", oneDriveToken.getRefreshToken());
|
||||
return "callback";
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/china-callback")
|
||||
public String oneDriveChinaCallback(String code, Model model) {
|
||||
OneDriveToken oneDriveToken = oneDriveChinaServiceImpl.getToken(code);
|
||||
model.addAttribute("accessToken", oneDriveToken.getAccessToken());
|
||||
model.addAttribute("refreshToken", oneDriveToken.getRefreshToken());
|
||||
return "callback";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package im.zhaojun.zfile.controller;
|
||||
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.enums.FileTypeEnum;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Zhao Jun
|
||||
*/
|
||||
@Controller
|
||||
public class PageController {
|
||||
|
||||
@Resource
|
||||
private DriveContext driveContext;
|
||||
|
||||
/**
|
||||
* 获取指定驱动器, 某个文件的直链, 然后重定向过去.
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 重定向至文件直链
|
||||
*/
|
||||
@GetMapping("/directlink/{driveId}/**")
|
||||
public String directlink(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
|
||||
String path = (String) request.getAttribute(
|
||||
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
AntPathMatcher apm = new AntPathMatcher();
|
||||
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
|
||||
|
||||
if (filePath.length() > 0 && filePath.charAt(0) != ZFileConstant.PATH_SEPARATOR_CHAR) {
|
||||
filePath = "/" + filePath;
|
||||
}
|
||||
|
||||
AbstractBaseFileService fileService = driveContext.getDriveService(driveId);
|
||||
FileItemDTO fileItem = fileService.getFileItem(filePath);
|
||||
|
||||
String url = fileItem.getUrl();
|
||||
|
||||
int queryIndex = url.indexOf('?');
|
||||
|
||||
if (queryIndex != -1) {
|
||||
String origin = url.substring(0, queryIndex);
|
||||
String queryString = url.substring(queryIndex + 1);
|
||||
|
||||
url = URLUtil.encode(origin) + "?" + URLUtil.encode(queryString);
|
||||
} else {
|
||||
url = URLUtil.encode(url);
|
||||
}
|
||||
|
||||
|
||||
if (Objects.equals(fileItem.getType(), FileTypeEnum.FOLDER)) {
|
||||
return "redirect:" + fileItem.getUrl();
|
||||
} else {
|
||||
return "redirect:" + url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package im.zhaojun.zfile.controller.admin;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ZipUtil;
|
||||
import im.zhaojun.zfile.context.StorageTypeContext;
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.model.dto.StorageStrategyDTO;
|
||||
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.model.entity.StorageConfig;
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnum;
|
||||
import im.zhaojun.zfile.model.support.SystemMonitorInfo;
|
||||
import im.zhaojun.zfile.service.SystemConfigService;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.util.FileUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台接口
|
||||
* @author zhaojun
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@Slf4j
|
||||
public class AdminController {
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
*/
|
||||
@GetMapping("/config")
|
||||
public ResultBean getConfig() {
|
||||
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
|
||||
return ResultBean.success(systemConfigDTO);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新系统配置
|
||||
*/
|
||||
@PostMapping("/config")
|
||||
public ResultBean updateConfig(SystemConfigDTO systemConfigDTO) throws Exception {
|
||||
systemConfigDTO.setId(1);
|
||||
systemConfigService.updateSystemConfig(systemConfigDTO);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 修改管理员登陆密码
|
||||
*/
|
||||
@PostMapping("/update-pwd")
|
||||
public ResultBean updatePwd(String username, String password) {
|
||||
systemConfigService.updateUsernameAndPwd(username, password);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定存储策略的表单域
|
||||
*
|
||||
* @param storageType
|
||||
* 存储策略
|
||||
*
|
||||
* @return 所有表单域
|
||||
*/
|
||||
@GetMapping("/strategy-form")
|
||||
public ResultBean getFormByStorageType(StorageTypeEnum storageType) {
|
||||
AbstractBaseFileService storageTypeService = StorageTypeContext.getStorageTypeService(storageType);
|
||||
List<StorageConfig> storageConfigList = storageTypeService.storageStrategyConfigList();
|
||||
return ResultBean.success(storageConfigList);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回支持的存储引擎.
|
||||
*/
|
||||
@GetMapping("/support-strategy")
|
||||
public ResultBean supportStrategy() {
|
||||
List<StorageStrategyDTO> result = new ArrayList<>();
|
||||
StorageTypeEnum[] values = StorageTypeEnum.values();
|
||||
return ResultBean.successData(values);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 系统日志下载
|
||||
*/
|
||||
@GetMapping("/log")
|
||||
public ResponseEntity<Object> downloadLog(HttpServletResponse response) {
|
||||
String userHome = System.getProperty("user.home");
|
||||
File fileZip = ZipUtil.zip(userHome + "/.zfile/logs");
|
||||
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
|
||||
return FileUtil.export(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取系统监控信息
|
||||
*/
|
||||
@GetMapping("monitor")
|
||||
public ResultBean monitor() {
|
||||
return ResultBean.success(new SystemMonitorInfo());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package im.zhaojun.zfile.controller.admin;
|
||||
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 缓存 Controller
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/cache")
|
||||
public class CacheController {
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
|
||||
@PostMapping("/{driveId}/enable")
|
||||
public ResultBean enableCache(@PathVariable("driveId") Integer driveId) {
|
||||
driveConfigService.updateCacheStatus(driveId, true);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
@PostMapping("/{driveId}/disable")
|
||||
public ResultBean disableCache(@PathVariable("driveId") Integer driveId) {
|
||||
driveConfigService.updateCacheStatus(driveId, false);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package im.zhaojun.zfile.controller.admin;
|
||||
|
||||
import im.zhaojun.zfile.model.dto.DriveConfigDTO;
|
||||
import im.zhaojun.zfile.model.dto.ResultBean;
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 驱动器 Controller
|
||||
* @author zhaojun
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@Slf4j
|
||||
public class DriveController {
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有驱动器列表
|
||||
*
|
||||
* @return 驱动器列表
|
||||
*/
|
||||
@GetMapping("drives")
|
||||
public ResultBean driveList() {
|
||||
List<DriveConfig> list = driveConfigService.list();
|
||||
return ResultBean.success(list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定驱动器基本信息及其参数
|
||||
*
|
||||
* @param id
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 驱动器基本信息信息
|
||||
*/
|
||||
@GetMapping("drive/{id}")
|
||||
public ResultBean driveItem(@PathVariable Integer id) {
|
||||
DriveConfigDTO driveConfig = driveConfigService.findDriveConfigDTOById(id);
|
||||
return ResultBean.success(driveConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存驱动器设置
|
||||
*/
|
||||
@PostMapping("drive")
|
||||
public ResultBean saveDriveItem(@RequestBody DriveConfigDTO driveConfigDTO) {
|
||||
driveConfigService.save(driveConfigDTO);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除驱动器设置
|
||||
*
|
||||
* @param id
|
||||
* 驱动器 ID
|
||||
*/
|
||||
@DeleteMapping("drive/{id}")
|
||||
public ResultBean deleteDriveItem(@PathVariable Integer id) {
|
||||
driveConfigService.deleteById(id);
|
||||
return ResultBean.success();
|
||||
}
|
||||
|
||||
}
|
||||
33
src/main/java/im/zhaojun/zfile/core/annotation/ApiLimit.java
Normal file
33
src/main/java/im/zhaojun/zfile/core/annotation/ApiLimit.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package im.zhaojun.zfile.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口限流注解
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiLimit {
|
||||
|
||||
/**
|
||||
* 持续时间
|
||||
*/
|
||||
int timeout();
|
||||
|
||||
/**
|
||||
* 时间单位, 默认为秒
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 单位时间内允许访问的最大次数
|
||||
*/
|
||||
long maxCount();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package im.zhaojun.zfile.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 演示系统禁用功能注解
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DemoDisable {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package im.zhaojun.zfile.core.aspect;
|
||||
|
||||
import cn.hutool.cache.CacheUtil;
|
||||
import cn.hutool.cache.impl.TimedCache;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import im.zhaojun.zfile.core.annotation.ApiLimit;
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.util.RequestHolder;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 接口限流切面, 通过注解 {@link ApiLimit} 进行限流.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ApiLimitAspect {
|
||||
|
||||
private final TimedCache<String, AtomicLong> apiLimitTimedCache = CacheUtil.newTimedCache(1000);
|
||||
|
||||
public static final String API_LIMIT_KEY_PREFIX = "api_limit_";
|
||||
|
||||
/**
|
||||
* 定义一个切点(通过注解)
|
||||
*/
|
||||
@Pointcut("@annotation(im.zhaojun.zfile.core.annotation.ApiLimit)")
|
||||
public void apiLimit() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在标记了 {@link ApiLimit} 注解的方法执行前进行限流校验.
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@Before("apiLimit()")
|
||||
public void before(JoinPoint joinPoint) {
|
||||
// 获取当前请求的方法上的注解中设置的值
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
// 反射获取当前被调用的方法
|
||||
Method method = signature.getMethod();
|
||||
// 获取方法中的注解
|
||||
ApiLimit apiLimit = method.getDeclaredAnnotation(ApiLimit.class);
|
||||
int timeout = apiLimit.timeout();
|
||||
TimeUnit timeUnit = apiLimit.timeUnit();
|
||||
long millis = timeUnit.toMillis(timeout);
|
||||
long maxCount = apiLimit.maxCount();
|
||||
|
||||
// 获取请求相关信息
|
||||
String ip = JakartaServletUtil.getClientIP(RequestHolder.getRequest());
|
||||
|
||||
// 限制访问次数
|
||||
String key = API_LIMIT_KEY_PREFIX.concat(ip).concat(method.getName());
|
||||
AtomicLong atomicLong = apiLimitTimedCache.get(key, false);
|
||||
if (atomicLong == null) {
|
||||
apiLimitTimedCache.put(key, new AtomicLong(1), millis);
|
||||
} else {
|
||||
if (atomicLong.incrementAndGet() > maxCount) {
|
||||
throw new BizException(ErrorCode.BIZ_ACCESS_TOO_FREQUENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package im.zhaojun.zfile.core.aspect;
|
||||
|
||||
import im.zhaojun.zfile.core.constant.MdcConstant;
|
||||
import im.zhaojun.zfile.core.util.AjaxJson;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
/**
|
||||
* Controller 切面, 用于处理返回值统一封装.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public final Object beforeBodyWrite(@Nullable Object body,
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull MediaType contentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType,
|
||||
@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response) {
|
||||
MappingJacksonValue container = getOrCreateContainer(body);
|
||||
// The contain body will never be null
|
||||
beforeBodyWriteInternal(container, contentType, returnType, request, response);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the body in a {@link MappingJacksonValue} value container (for providing
|
||||
* additional serialization instructions) or simply cast it if already wrapped.
|
||||
*/
|
||||
private MappingJacksonValue getOrCreateContainer(Object body) {
|
||||
return body instanceof MappingJacksonValue ? (MappingJacksonValue) body :
|
||||
new MappingJacksonValue(body);
|
||||
}
|
||||
|
||||
private void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
|
||||
MediaType contentType,
|
||||
MethodParameter returnType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
// Get return body
|
||||
Object returnBody = bodyContainer.getValue();
|
||||
|
||||
if (returnBody instanceof AjaxJson<?> baseResponse) {
|
||||
// 将 MDC 中的 TraceId 设置到返回值中
|
||||
baseResponse.setTraceId(MDC.get(MdcConstant.TRACE_ID));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package im.zhaojun.zfile.core.aspect;
|
||||
|
||||
import im.zhaojun.zfile.core.annotation.DemoDisable;
|
||||
import im.zhaojun.zfile.core.config.ZFileProperties;
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 通过注解 {@link DemoDisable} 限制演示系统不可操作的功能.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class DemoDisableAspect {
|
||||
|
||||
@Resource
|
||||
private ZFileProperties zFileProperties;
|
||||
|
||||
/**
|
||||
* 定义一个切点(通过注解)
|
||||
*/
|
||||
@Pointcut("@annotation(im.zhaojun.zfile.core.annotation.DemoDisable)")
|
||||
public void demoDisable() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在标记了 {@link DemoDisable} 注解的方法执行前进行限流校验.
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@Before("demoDisable()")
|
||||
public void before(JoinPoint joinPoint) {
|
||||
if (zFileProperties.isDemoSite()) {
|
||||
throw new BizException(ErrorCode.DEMO_SITE_DISABLE_OPERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package im.zhaojun.zfile.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ZFile 配置类,将配置文件中的 zfile 配置项映射到该类中.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@EnableConfigurationProperties
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "zfile")
|
||||
public class ZFileProperties {
|
||||
|
||||
private boolean debug;
|
||||
|
||||
private String version;
|
||||
|
||||
private boolean isDemoSite;
|
||||
|
||||
private OAuth2Properties onedrive = new OAuth2Properties();
|
||||
private OAuth2Properties onedriveChina = new OAuth2Properties();
|
||||
private OAuth2Properties gd = new OAuth2Properties();
|
||||
|
||||
@Data
|
||||
public static class OAuth2Properties {
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private String redirectUri;
|
||||
private String scope;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package im.zhaojun.zfile.core.config.datasource;
|
||||
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
|
||||
import org.springframework.core.PriorityOrdered;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 在 Spring 容器初始化时, 对数据源进行处理.
|
||||
* <br/>
|
||||
* 1. 针对 DataSource 进行处理,仅针对 sqlite:
|
||||
* <ul>
|
||||
* <li>提前创建 sqlite 数据文件所在目录.</li>
|
||||
* <li>检测到版本更新时(pom.xml -> project.version)自动备份原数据库.</li>
|
||||
* </ul>
|
||||
* <br/>
|
||||
* 2. 针对 Flyway 进行处理,根据数据库类型, 配置不同的 Flyway Migration Location:
|
||||
* <ul>
|
||||
* <li>SQLite 数据库使用 migration-sqlite 目录.</li>
|
||||
* <li>MySQL 数据库使用 migration-mysql 目录.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class DataSourceBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
|
||||
|
||||
public static final String ZFILE_VERSION_PROPERTIES = "zfile.version";
|
||||
|
||||
public static final String DRIVE_CLASS_NAME_PROPERTIES = "spring.datasource.driver-class-name";
|
||||
|
||||
public static final String DATA_SOURCE_BEAN_NAME = "dataSource";
|
||||
|
||||
public static final String SQLITE_DRIVE_CLASS_NAME = "org.sqlite.JDBC";
|
||||
|
||||
public static final String MYSQL_DRIVE_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
// 如果更改了数据源类型这里要修改
|
||||
if (bean instanceof HikariDataSource dataSource && DATA_SOURCE_BEAN_NAME.equals(beanName)) {
|
||||
processSqliteDataSource(dataSource);
|
||||
} else if (bean instanceof FlywayProperties flywayProperties) {
|
||||
processFlywayLocations(flywayProperties);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果是 sqlite 数据库, 提前创建数据库文件所在目录. <br/>
|
||||
*
|
||||
* 如果检测到版本更新, 自动备份原数据库文件.
|
||||
*
|
||||
* @param dataSource
|
||||
* 数据源
|
||||
*/
|
||||
private void processSqliteDataSource(HikariDataSource dataSource) {
|
||||
String driverClassName = dataSource.getDriverClassName();
|
||||
String jdbcUrl = dataSource.getJdbcUrl();
|
||||
if (StringUtils.equals(driverClassName, SQLITE_DRIVE_CLASS_NAME)) {
|
||||
String path = jdbcUrl.replace("jdbc:sqlite:", "");
|
||||
String folderPath = FileUtil.getAbsolutePath(new File(path).getParentFile());
|
||||
log.info("SQLite 数据库文件所在目录: [{}]", folderPath);
|
||||
File file = new File(folderPath);
|
||||
if (!file.exists()) {
|
||||
log.info("检测到 SQLite 数据库文件所在目录不存在, 已自动创建.");
|
||||
if (!file.mkdirs()) {
|
||||
log.error("SQLite 数据库文件创建失败.");
|
||||
}
|
||||
} else {
|
||||
log.info("检测到 SQLite 数据库文件所在目录已存在, 无需自动创建.");
|
||||
|
||||
// 更新版本时, 先自动备份数据库文件
|
||||
String version = SpringUtil.getProperty(ZFILE_VERSION_PROPERTIES);
|
||||
if (StringUtils.isNotEmpty(version)) {
|
||||
String backupPath = folderPath + "/zfile-update-" + version + "-backup.db";
|
||||
if (!FileUtil.exist(path)) {
|
||||
log.error("检测到 SQLite 数据库文件不存在, 一般为初始化状态,无需备份.");
|
||||
return;
|
||||
}
|
||||
if (FileUtil.exist(backupPath)) {
|
||||
log.info("检测到 SQLite 数据库备份文件 [{}] 已存在, 无需再次备份.", backupPath);
|
||||
} else {
|
||||
FileUtil.copy(path, backupPath, false);
|
||||
log.info("自动备份 SQLite 数据库文件到: [{}]", backupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据使用的不同数据库, 配置使用不同的 migration location
|
||||
*
|
||||
* @param flywayProperties
|
||||
* flyway 配置项
|
||||
*/
|
||||
private void processFlywayLocations(FlywayProperties flywayProperties) {
|
||||
String driveClassName = SpringUtil.getProperty(DRIVE_CLASS_NAME_PROPERTIES);
|
||||
if (SQLITE_DRIVE_CLASS_NAME.equals(driveClassName)) {
|
||||
flywayProperties.setLocations(List.of("classpath:db/migration-sqlite"));
|
||||
} else if (MYSQL_DRIVE_CLASS_NAME.equals(driveClassName)) {
|
||||
flywayProperties.setLocations(List.of("classpath:db/migration-mysql"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package im.zhaojun.zfile.core.config.docs;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.StringSchema;
|
||||
import io.swagger.v3.oas.models.parameters.HeaderParameter;
|
||||
import org.springdoc.core.customizers.OperationCustomizer;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j 参数配置,区分前台功能和管理员功能,并为管理员接口增加统一 token header 配置.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfiguration {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi groupedOpenApi() {
|
||||
String groupName = "前台功能";
|
||||
return GroupedOpenApi.builder()
|
||||
.group(groupName)
|
||||
.packagesToScan("im.zhaojun.zfile.module")
|
||||
.pathsToExclude("/admin/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi groupedOpenApi2() {
|
||||
String groupName = "管理员功能";
|
||||
return GroupedOpenApi.builder()
|
||||
.group(groupName)
|
||||
.packagesToScan("im.zhaojun.zfile.module")
|
||||
.pathsToMatch("/admin/**")
|
||||
.addOperationCustomizer(globalOperationCustomizer())
|
||||
.build();
|
||||
}
|
||||
|
||||
public OperationCustomizer globalOperationCustomizer() {
|
||||
return (operation, handlerMethod) -> {
|
||||
operation.addParametersItem(new HeaderParameter()
|
||||
.name("zfile-token")
|
||||
.description("token")
|
||||
.required(true)
|
||||
.schema(new StringSchema()));
|
||||
return operation;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
Contact contact = new Contact();
|
||||
contact.setName("zhaojun");
|
||||
contact.setUrl("https://zfile.vip");
|
||||
contact.setEmail("873019219@qq.com");
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("ZFILE 文档")
|
||||
.description("# 这是 ZFILE Restful 接口文档展示页面")
|
||||
.termsOfService("https://www.zfile.vip")
|
||||
.contact(contact)
|
||||
.version("1.0")
|
||||
.license(new License()
|
||||
.name("Apache 2.0")
|
||||
.url("http://doc.xiaominfo.com")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package im.zhaojun.zfile.core.config.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JSON String 反序列化器, 用于将 JSON 字符串反序列化为 JSON 对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class JSONStringDeserializer extends JsonDeserializer<String> {
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext context) throws IOException {
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.writeValueAsString(node);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package im.zhaojun.zfile.core.config.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* JSON String 序列化器, 用于将 JSON 字符串序列化为 JSON 对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class JSONStringSerializer extends JsonSerializer<String> {
|
||||
|
||||
@Override
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeRawValue(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class CollectionIntegerTypeHandler extends CollectionTypeHandler<Set<Integer>> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class CollectionStrTypeHandler extends CollectionTypeHandler<Set<String>> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 自定义 Set 类型处理器, 用于处理数据库 VARCHAR 类型字段和 Java Set 类型属性之间的转换.
|
||||
* 支持字符串格式为: "[a, b, c]".
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public abstract class CollectionTypeHandler<T> extends BaseTypeHandler<Object> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
|
||||
throws SQLException {
|
||||
if (parameter instanceof Collection collection) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
for (Object o : collection) {
|
||||
joiner.add(Convert.toStr(o));
|
||||
}
|
||||
ps.setString(i, joiner.toString());
|
||||
} else {
|
||||
ps.setString(i, Convert.toStr(parameter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet rs, String columnName)
|
||||
throws SQLException {
|
||||
String str = rs.getString(columnName);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet rs, int columnIndex)
|
||||
throws SQLException {
|
||||
String str = rs.getString(columnIndex);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(CallableStatement cs, int columnIndex)
|
||||
throws SQLException {
|
||||
String str = cs.getString(columnIndex);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
private Class<?> collectionClazz;
|
||||
|
||||
private Type innerType;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public CollectionTypeHandler() {
|
||||
ResolvableType resolvableType = ResolvableType.forClass(getClass());
|
||||
Type type = resolvableType.as(CollectionTypeHandler.class).getGeneric().getType();
|
||||
|
||||
if (type instanceof ParameterizedType parameterizedType) {
|
||||
collectionClazz = (Class<?>) parameterizedType.getRawType();
|
||||
// 获取实际类型参数(泛型参数,例如 List<String> 中的 String)
|
||||
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
|
||||
|
||||
// 使用这些信息做进一步操作
|
||||
for (Type actualTypeArgument : actualTypeArguments) {
|
||||
innerType = actualTypeArgument;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertToEntityAttribute(String dbData) {
|
||||
if (StrUtil.isEmpty(dbData)) {
|
||||
if (List.class.isAssignableFrom(collectionClazz)) {
|
||||
return Collections.emptyList();
|
||||
} else if (Set.class.isAssignableFrom(collectionClazz)) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Collection collection;
|
||||
|
||||
if (List.class.isAssignableFrom(collectionClazz)) {
|
||||
collection = new ArrayList<>();
|
||||
} else if (Set.class.isAssignableFrom(collectionClazz)) {
|
||||
collection = new HashSet<>();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] split = dbData.split(",");
|
||||
for (String s : split) {
|
||||
if (NumberUtil.isNumber(s)) {
|
||||
collection.add(Convert.convert(Integer.class, s));
|
||||
} else {
|
||||
collection.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
/**
|
||||
* mybatis plus 分页插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor(DataSource dataSource) throws SQLException {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName();
|
||||
DbType dbType = DbType.getDbType(databaseProductName);
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType));
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* MyBatis 数据库 ID Provider, 用于判断当前数据库类型来执行不同的 SQL 语句. <br>
|
||||
* 可在 xml 中使用 <code><if test="_databaseId = 'mysql'"> </code> 来判断数据库类型. <br>
|
||||
* 也可以在外层使用,如 <code><delete id="xxx" databaseId="sqlite"></code> 来判断数据库类型.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class MyDatabaseIdProvider implements DatabaseIdProvider {
|
||||
|
||||
private static final String DATABASE_MYSQL = "MySQL";
|
||||
private static final String DATABASE_SQLITE = "SQLite";
|
||||
|
||||
@Override
|
||||
public String getDatabaseId(DataSource dataSource) throws SQLException {
|
||||
Connection conn = dataSource.getConnection();
|
||||
String dbName = conn.getMetaData().getDatabaseProductName();
|
||||
String dbAlias = "";
|
||||
switch (dbName) {
|
||||
case DATABASE_MYSQL:
|
||||
dbAlias = "mysql";
|
||||
break;
|
||||
case DATABASE_SQLITE:
|
||||
dbAlias = "sqlite";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return dbAlias;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 自动填充配置类
|
||||
* 用于自动填充 createTime 和 updateTime 字段
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Jackson 定制版 SaSession,忽略 timeout 等属性的序列化
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.34.0
|
||||
*/
|
||||
@JsonIgnoreProperties({"timeout"})
|
||||
public class SaSessionForJacksonCustomized extends SaSession {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -7600983549653130681L;
|
||||
|
||||
public SaSessionForJacksonCustomized() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个Session对象
|
||||
* @param id Session的id
|
||||
*/
|
||||
public SaSessionForJacksonCustomized(String id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* SaToken 权限配置, 配置管理员才能访问管理员功能.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 注册权限校验拦截器, 拦截所有 /admin/** 请求,但不包含 /admin 因为这个是登录页面.
|
||||
*
|
||||
* @param registry
|
||||
* 拦截器注册器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new SaInterceptor(handle -> {
|
||||
SaRouter.match("/admin/**", () -> {
|
||||
StpUtil.checkLogin();
|
||||
StpUtil.checkRole("admin");
|
||||
});
|
||||
})).addPathPatterns("/**").excludePathPatterns("/admin");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.dao.SaTokenDao;
|
||||
import cn.dev33.satoken.strategy.SaStrategy;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Sa-Token 持久层实现 [ Redis存储、Jackson序列化 ]
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.34.0
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "spring.data.redis.host")
|
||||
public class SaTokenDaoRedisJackson implements SaTokenDao {
|
||||
|
||||
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
public static final String DATE_PATTERN = "yyyy-MM-dd";
|
||||
public static final String TIME_PATTERN = "HH:mm:ss";
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
|
||||
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
|
||||
|
||||
/**
|
||||
* ObjectMapper 对象 (以 public 作用域暴露出此对象,方便开发者二次更改配置)
|
||||
*
|
||||
* <p> 例如:
|
||||
* <pre>
|
||||
* SaTokenDaoRedisJackson redisJackson = (SaTokenDaoRedisJackson) SaManager.getSaTokenDao();
|
||||
* redisJackson.objectMapper.xxx = xxx;
|
||||
* </pre>
|
||||
* </p>
|
||||
*/
|
||||
public ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* String 读写专用
|
||||
*/
|
||||
public StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* Object 读写专用
|
||||
*/
|
||||
public RedisTemplate<String, Object> objectRedisTemplate;
|
||||
|
||||
/**
|
||||
* 标记:是否已初始化成功
|
||||
*/
|
||||
public boolean isInit;
|
||||
|
||||
@Autowired
|
||||
public void init(RedisConnectionFactory connectionFactory) {
|
||||
// 如果已经初始化成功了,就立刻退出,不重复初始化
|
||||
if(this.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 指定相应的序列化方案
|
||||
StringRedisSerializer keySerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
// 通过反射获取Mapper对象, 增加一些配置, 增强兼容性
|
||||
try {
|
||||
Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
|
||||
field.setAccessible(true);
|
||||
this.objectMapper = (ObjectMapper) field.get(valueSerializer);
|
||||
|
||||
// 配置[忽略未知字段]
|
||||
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
// 配置[时间类型转换]
|
||||
JavaTimeModule timeModule = new JavaTimeModule();
|
||||
|
||||
// LocalDateTime序列化与反序列化
|
||||
timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
|
||||
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
|
||||
|
||||
// LocalDate序列化与反序列化
|
||||
timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
|
||||
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
|
||||
|
||||
// LocalTime序列化与反序列化
|
||||
timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
|
||||
timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
|
||||
|
||||
this.objectMapper.registerModule(timeModule);
|
||||
|
||||
// 重写 SaSession 生成策略
|
||||
SaStrategy.instance.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId);
|
||||
} catch (Exception e) {
|
||||
System.err.println(e.getMessage());
|
||||
}
|
||||
// 构建StringRedisTemplate
|
||||
StringRedisTemplate stringTemplate = new StringRedisTemplate();
|
||||
stringTemplate.setConnectionFactory(connectionFactory);
|
||||
stringTemplate.afterPropertiesSet();
|
||||
|
||||
// 构建RedisTemplate
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(keySerializer);
|
||||
template.setHashKeySerializer(keySerializer);
|
||||
template.setValueSerializer(valueSerializer);
|
||||
template.setHashValueSerializer(valueSerializer);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
// 开始初始化相关组件
|
||||
this.stringRedisTemplate = stringTemplate;
|
||||
this.objectRedisTemplate = template;
|
||||
|
||||
// 打上标记,表示已经初始化成功,后续无需再重新初始化
|
||||
this.isInit = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Value,如无返空
|
||||
*/
|
||||
@Override
|
||||
public String get(String key) {
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Value,并设定存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void set(String key, String value, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
// 判断是否为永不过期
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
stringRedisTemplate.opsForValue().set(key, value);
|
||||
} else {
|
||||
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修修改指定key-value键值对 (过期时间不变)
|
||||
*/
|
||||
@Override
|
||||
public void update(String key, String value) {
|
||||
long expire = getTimeout(key);
|
||||
// -2 = 无此键
|
||||
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
this.set(key, value, expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Value
|
||||
*/
|
||||
@Override
|
||||
public void delete(String key) {
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Value的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public long getTimeout(String key) {
|
||||
return stringRedisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改Value的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void updateTimeout(String key, long timeout) {
|
||||
// 判断是否想要设置为永久
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
long expire = getTimeout(key);
|
||||
if(expire == SaTokenDao.NEVER_EXPIRE) {
|
||||
// 如果其已经被设置为永久,则不作任何处理
|
||||
} else {
|
||||
// 如果尚未被设置为永久,那么再次set一次
|
||||
this.set(key, this.get(key), timeout);
|
||||
}
|
||||
return;
|
||||
}
|
||||
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取Object,如无返空
|
||||
*/
|
||||
@Override
|
||||
public Object getObject(String key) {
|
||||
return objectRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Object,并设定存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void setObject(String key, Object object, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
// 判断是否为永不过期
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
objectRedisTemplate.opsForValue().set(key, object);
|
||||
} else {
|
||||
objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Object (过期时间不变)
|
||||
*/
|
||||
@Override
|
||||
public void updateObject(String key, Object object) {
|
||||
long expire = getObjectTimeout(key);
|
||||
// -2 = 无此键
|
||||
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
this.setObject(key, object, expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Object
|
||||
*/
|
||||
@Override
|
||||
public void deleteObject(String key) {
|
||||
objectRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Object的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public long getObjectTimeout(String key) {
|
||||
return objectRedisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改Object的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void updateObjectTimeout(String key, long timeout) {
|
||||
// 判断是否想要设置为永久
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
long expire = getObjectTimeout(key);
|
||||
if(expire == SaTokenDao.NEVER_EXPIRE) {
|
||||
// 如果其已经被设置为永久,则不作任何处理
|
||||
} else {
|
||||
// 如果尚未被设置为永久,那么再次set一次
|
||||
this.setObject(key, this.getObject(key), timeout);
|
||||
}
|
||||
return;
|
||||
}
|
||||
objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 搜索数据
|
||||
*/
|
||||
@Override
|
||||
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
|
||||
Set<String> keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*");
|
||||
List<String> list = new ArrayList<>(keys);
|
||||
return SaFoxUtil.searchList(list, start, size, sortType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import im.zhaojun.zfile.module.user.service.UserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义权限加载接口实现类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
private static final List<String> ADMIN_ROLE_LIST = Collections.singletonList("admin");
|
||||
|
||||
public static final List<String> EMPTY_ROLE_LIST = Collections.emptyList();
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合,这里没用到这个功能,所以返回空集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
boolean isAdmin = userService.isAdmin(Convert.toInt(loginId));
|
||||
return isAdmin ? ADMIN_ROLE_LIST : EMPTY_ROLE_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.jackson.JsonComponent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Jackson 枚举反序列化器, 用于将接收请求中的参数(一般为字符串)转换为枚举对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Setter
|
||||
@Slf4j
|
||||
@JsonComponent
|
||||
public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {
|
||||
|
||||
private Class<?> clazz;
|
||||
|
||||
/**
|
||||
* 反序列化操作
|
||||
*
|
||||
* @param jsonParser
|
||||
* json 解析器
|
||||
*
|
||||
* @param ctx
|
||||
* 反序列化上下文
|
||||
*
|
||||
* @return 反序列化后的枚举值
|
||||
* @throws IOException 反序列化异常
|
||||
*/
|
||||
@Override
|
||||
public Enum<?> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
|
||||
Class<?> enumType = clazz;
|
||||
if (Objects.isNull(enumType) || !enumType.isEnum()) {
|
||||
return null;
|
||||
}
|
||||
String text = jsonParser.getText();
|
||||
Method method = StringToEnumConverterFactory.getMethod(clazz);
|
||||
Enum<?>[] enumConstants = (Enum<?>[]) enumType.getEnumConstants();
|
||||
|
||||
// 将值与枚举对象对应并缓存
|
||||
for (Enum<?> e : enumConstants) {
|
||||
try {
|
||||
if (Objects.equals(method.invoke(e).toString(), text)) {
|
||||
return e;
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
log.error("获取枚举值错误!!! ", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 为不同的枚举获取合适的解析器
|
||||
*
|
||||
* @param ctx
|
||||
* 反序列化上下文
|
||||
*
|
||||
* @param property
|
||||
* property
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Enum<?>> createContextual(DeserializationContext ctx, BeanProperty property) {
|
||||
Class<?> rawCls = ctx.getContextualType().getRawClass();
|
||||
JacksonEnumDeserializer converter = new JacksonEnumDeserializer();
|
||||
converter.setClazz(rawCls);
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import im.zhaojun.zfile.core.config.security.SaTokenDaoRedisJackson;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.cache.transaction.TransactionAwareCacheManagerProxy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Spring Cache 相关配置
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class SpringCacheConfig {
|
||||
|
||||
@Value("${zfile.dbCache.enable:true}")
|
||||
private Boolean dbCacheEnable;
|
||||
|
||||
/**
|
||||
* 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager,使其支持事务 (将 put、evict、clear 操作延迟到事务成功提交再执行.)
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SaTokenDaoRedisJackson.class)
|
||||
public CacheManager cacheManager() {
|
||||
return BooleanUtils.isNotTrue(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* String 转枚举通用转换器工厂
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
|
||||
|
||||
/**
|
||||
* 存储枚举类型的缓存
|
||||
*/
|
||||
private static final Map<Class<?>, Converter<String, ? extends Enum<?>>> CONVERTER_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 枚举类的获取枚举值方法缓存
|
||||
*/
|
||||
private static final Map<Class<?>, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
// 缓存转换器
|
||||
Converter<String, T> converter = (Converter<String, T>) CONVERTER_MAP.get(targetType);
|
||||
if (converter == null) {
|
||||
converter = new StringToEnumConverter<>(targetType);
|
||||
CONVERTER_MAP.put(targetType, converter);
|
||||
}
|
||||
return converter;
|
||||
}
|
||||
|
||||
static class StringToEnumConverter<T extends Enum<?>> implements Converter<String, T> {
|
||||
|
||||
private final Map<String, T> enumMap = new ConcurrentHashMap<>();
|
||||
|
||||
StringToEnumConverter(Class<T> enumType) {
|
||||
Method method = getMethod(enumType);
|
||||
T[] enums = enumType.getEnumConstants();
|
||||
|
||||
// 将值与枚举对象对应并缓存
|
||||
for (T e : enums) {
|
||||
try {
|
||||
enumMap.put(method.invoke(e).toString(), e);
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
log.error("获取枚举值错误!!! ", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public T convert(@NotNull String source) {
|
||||
// 获取
|
||||
T t = enumMap.get(source);
|
||||
if (t == null) {
|
||||
throw new SystemException("该字符串找不到对应的枚举对象 字符串:" + source);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static <T> Method getMethod(Class<T> enumType) {
|
||||
Method method;
|
||||
// 找到取值的方法
|
||||
if (IEnum.class.isAssignableFrom(enumType)) {
|
||||
try {
|
||||
method = enumType.getMethod("getValue");
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new SystemException(String.format("类:%s 找不到 getValue方法",
|
||||
enumType.getName()));
|
||||
}
|
||||
} else {
|
||||
method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> {
|
||||
Field field =
|
||||
dealEnumType(enumType).orElseThrow(() -> new IllegalArgumentException(String.format(
|
||||
"类:%s 找不到 EnumValue注解", enumType.getName())));
|
||||
|
||||
Class<?> fieldType = field.getType();
|
||||
String fieldName = field.getName();
|
||||
String methodName = StringUtils.concatCapitalize(boolean.class.equals(fieldType) ? "is" : "get", fieldName);
|
||||
try {
|
||||
return enumType.getDeclaredMethod(methodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
private static Optional<Field> dealEnumType(Class<?> clazz) {
|
||||
return clazz.isEnum() ?
|
||||
Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package im.zhaojun.zfile.config;
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import im.zhaojun.zfile.model.enums.StorageTypeEnumDeSerializerConvert;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
|
||||
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -9,26 +9,35 @@ import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* ZFile Web 相关配置.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
registry.addConverter(new StorageTypeEnumDeSerializerConvert());
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持 url 中传入 <>[\]^`{|} 这些特殊字符.
|
||||
*/
|
||||
@Bean
|
||||
public ServletWebServerFactory webServerFactory() {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory();
|
||||
|
||||
// 添加对 URL 中特殊符号的支持.
|
||||
webServerFactory.addConnectorCustomizers(connector -> {
|
||||
connector.setAttribute("relaxedPathChars", "<>[\\]^`{|}");
|
||||
connector.setAttribute("relaxedQueryChars", "<>[\\]^`{|}");
|
||||
connector.setProperty("relaxedPathChars", "<>[\\]^`{|}%[]");
|
||||
connector.setProperty("relaxedQueryChars", "<>[\\]^`{|}%[]");
|
||||
});
|
||||
return webServerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义枚举格式化器.
|
||||
* @see StorageTypeEnum
|
||||
*/
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
registry.addConverterFactory(new StringToEnumConverterFactory());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package im.zhaojun.zfile.core.config.totp;
|
||||
|
||||
import dev.samstevens.totp.TotpInfo;
|
||||
import dev.samstevens.totp.code.*;
|
||||
import dev.samstevens.totp.qr.QrDataFactory;
|
||||
import dev.samstevens.totp.qr.QrGenerator;
|
||||
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
|
||||
import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
|
||||
import dev.samstevens.totp.secret.DefaultSecretGenerator;
|
||||
import dev.samstevens.totp.secret.SecretGenerator;
|
||||
import dev.samstevens.totp.time.SystemTimeProvider;
|
||||
import dev.samstevens.totp.time.TimeProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnClass({TotpInfo.class})
|
||||
@EnableConfigurationProperties({TotpProperties.class})
|
||||
public class TotpAutoConfiguration {
|
||||
|
||||
private final TotpProperties props;
|
||||
|
||||
@Autowired
|
||||
public TotpAutoConfiguration(TotpProperties props) {
|
||||
this.props = props;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public SecretGenerator secretGenerator() {
|
||||
int length = this.props.getSecret().getLength();
|
||||
return new DefaultSecretGenerator(length);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TimeProvider timeProvider() {
|
||||
return new SystemTimeProvider();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public HashingAlgorithm hashingAlgorithm() {
|
||||
return HashingAlgorithm.SHA1;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public QrDataFactory qrDataFactory(HashingAlgorithm hashingAlgorithm) {
|
||||
return new QrDataFactory(hashingAlgorithm, this.getCodeLength(), this.getTimePeriod());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public QrGenerator qrGenerator() {
|
||||
return new ZxingPngQrGenerator();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CodeGenerator codeGenerator(HashingAlgorithm algorithm) {
|
||||
return new DefaultCodeGenerator(algorithm, this.getCodeLength());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public CodeVerifier codeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) {
|
||||
DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
|
||||
verifier.setTimePeriod(this.getTimePeriod());
|
||||
verifier.setAllowedTimePeriodDiscrepancy(this.props.getTime().getDiscrepancy());
|
||||
return verifier;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RecoveryCodeGenerator recoveryCodeGenerator() {
|
||||
return new RecoveryCodeGenerator();
|
||||
}
|
||||
|
||||
private int getCodeLength() {
|
||||
return this.props.getCode().getLength();
|
||||
}
|
||||
|
||||
private int getTimePeriod() {
|
||||
return this.props.getTime().getPeriod();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package im.zhaojun.zfile.core.config.totp;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(
|
||||
prefix = "totp"
|
||||
)
|
||||
public class TotpProperties {
|
||||
private static final int DEFAULT_SECRET_LENGTH = 32;
|
||||
private static final int DEFAULT_CODE_LENGTH = 6;
|
||||
private static final int DEFAULT_TIME_PERIOD = 30;
|
||||
private static final int DEFAULT_TIME_DISCREPANCY = 1;
|
||||
private final Secret secret = new Secret();
|
||||
private final Code code = new Code();
|
||||
private final Time time = new Time();
|
||||
|
||||
public TotpProperties() {
|
||||
}
|
||||
|
||||
public Secret getSecret() {
|
||||
return this.secret;
|
||||
}
|
||||
|
||||
public Code getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public Time getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
public static class Time {
|
||||
private int period = 30;
|
||||
private int discrepancy = 1;
|
||||
|
||||
public Time() {
|
||||
}
|
||||
|
||||
public int getPeriod() {
|
||||
return this.period;
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public int getDiscrepancy() {
|
||||
return this.discrepancy;
|
||||
}
|
||||
|
||||
public void setDiscrepancy(int discrepancy) {
|
||||
this.discrepancy = discrepancy;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Code {
|
||||
private int length = 6;
|
||||
|
||||
public Code() {
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Secret {
|
||||
private int length = 32;
|
||||
|
||||
public Secret() {
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* Slf4j mdc 常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class MdcConstant {
|
||||
|
||||
public static final String TRACE_ID = "traceId";
|
||||
|
||||
public static final String IP = "ip";
|
||||
|
||||
public static final String USER = "user";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* 规则表达式类型常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class RuleTypeConstant {
|
||||
|
||||
public static final String IP = "ip";
|
||||
|
||||
public static final String REGEX = "regex";
|
||||
|
||||
public static final String ANT_PATH = "antPath";
|
||||
|
||||
public static final String SPRING_SIMPLE = "springSimple";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* ZFile 常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class ZFileConstant {
|
||||
|
||||
/**
|
||||
* 最大支持文本文件大小为 ? KB 的文件内容.
|
||||
*/
|
||||
public static Long TEXT_MAX_FILE_SIZE_KB = 100L;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setTextMaxFileSizeMb(@Value("${zfile.preview.text.maxFileSizeKb}") Long maxFileSizeKb) {
|
||||
ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* ZFile 自定义 HTTP 请求头常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ZFileHttpHeaderConstant {
|
||||
|
||||
public static final String ZFILE_TOKEN = "Zfile-Token";
|
||||
|
||||
public static final String AXIOS_REQUEST = "Axios-Request";
|
||||
|
||||
public static final String AXIOS_FROM = "Axios-From";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package im.zhaojun.zfile.core.controller;
|
||||
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.module.config.service.SystemConfigService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties;
|
||||
import org.springframework.core.io.FileSystemResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 处理前端首页 Controller
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class FrontIndexController {
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
@Resource
|
||||
private WebProperties webProperties;
|
||||
|
||||
/**
|
||||
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
|
||||
* 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回.
|
||||
*
|
||||
* @return 转发到 /index.html
|
||||
*/
|
||||
@RequestMapping(value = { "/"})
|
||||
@ResponseBody
|
||||
public String redirect() {
|
||||
// 读取 resources/static/index.html 文件修改 title 和 favicon 后返回
|
||||
ResourceLoader resourceLoader = new FileSystemResourceLoader();
|
||||
String[] staticLocations = webProperties.getResources().getStaticLocations();
|
||||
|
||||
// 如果 staticLocations 里没有包含 file:static/, 则手动添加
|
||||
boolean fileStaticExist = false;
|
||||
for (String staticLocation : staticLocations) {
|
||||
if (staticLocation.startsWith("file:")) {
|
||||
fileStaticExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fileStaticExist) {
|
||||
staticLocations = org.apache.commons.lang3.ArrayUtils.add(staticLocations, "file:static/");
|
||||
}
|
||||
|
||||
for (String staticLocation : staticLocations) {
|
||||
org.springframework.core.io.Resource resource = resourceLoader.getResource(staticLocation + "/index.html");
|
||||
boolean exists = resource.exists();
|
||||
if (exists) {
|
||||
String content;
|
||||
try {
|
||||
content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
log.debug("读取 index.html 文件成功, 文件路径: {}", staticLocation);
|
||||
} catch (Exception e) {
|
||||
log.error("{} 资源存在但读取 index.html 文件失败.", staticLocation);
|
||||
return "static index.html read error";
|
||||
}
|
||||
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
|
||||
// 替换为系统设置中的站点名称
|
||||
String siteName = systemConfig.getSiteName();
|
||||
if (StringUtils.isNotBlank(siteName)) {
|
||||
content = content.replace("<title>ZFile</title>", "<title>" + siteName + "</title>");
|
||||
}
|
||||
|
||||
// 替换为系统设置中的 favicon 地址
|
||||
String faviconUrl = systemConfig.getFaviconUrl();
|
||||
if (StringUtils.isNotBlank(faviconUrl)) {
|
||||
content = content.replace("/favicon.svg", faviconUrl);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return "static index.html not found";
|
||||
}
|
||||
|
||||
@RequestMapping(value = { "/guest"})
|
||||
@ResponseBody
|
||||
public String guest() {
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
return systemConfig.getGuestIndexHtml();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package im.zhaojun.zfile.core.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ZipUtil;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiSort;
|
||||
import im.zhaojun.zfile.core.annotation.DemoDisable;
|
||||
import im.zhaojun.zfile.core.util.FileResponseUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 获取系统日志接口
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Tag(name = "日志")
|
||||
@ApiSort(8)
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
public class LogController {
|
||||
|
||||
@Value("${zfile.log.path}")
|
||||
private String zfileLogPath;
|
||||
|
||||
@GetMapping("/log/download")
|
||||
@Operation(summary = "下载系统日志")
|
||||
@DemoDisable
|
||||
public ResponseEntity<Resource> downloadLog() {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("下载诊断日志");
|
||||
}
|
||||
|
||||
File fileZip = ZipUtil.zip(zfileLogPath);
|
||||
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
|
||||
return FileResponseUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
|
||||
}
|
||||
|
||||
}
|
||||
135
src/main/java/im/zhaojun/zfile/core/exception/ErrorCode.java
Normal file
135
src/main/java/im/zhaojun/zfile/core/exception/ErrorCode.java
Normal file
@@ -0,0 +1,135 @@
|
||||
package im.zhaojun.zfile.core.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 异常信息枚举类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public enum ErrorCode {
|
||||
|
||||
/**
|
||||
* 系统异常
|
||||
*/
|
||||
SYSTEM_ERROR("50000", "系统异常"),
|
||||
INVALID_STORAGE_SOURCE("50001", "无效或初始化失败的存储源"),
|
||||
DEMO_SITE_DISABLE_OPERATOR("50002", "演示站点不允许此操作"),
|
||||
|
||||
/**
|
||||
* 业务异常 4xxxx.
|
||||
* 第二位为 0 时,是系统初始化相关错误
|
||||
* 第二位为 1 时,是前台(文件管理)错误
|
||||
* 第二位为 2 时,是登录错误
|
||||
* 第二位为 3 时,是管理员端错误
|
||||
*/
|
||||
BIZ_ERROR("40000", "操作失败"),
|
||||
BIZ_NOT_FOUND("40400", "NOT FOUND"),
|
||||
|
||||
// 第二位为 0 时,是系统初始化相关错误
|
||||
BIZ_SYSTEM_ALREADY_INIT("40001", "系统已初始化,请勿重复初始化"),
|
||||
BIZ_SYSTEM_INIT_ERROR("40002", "系统初始化错误"),
|
||||
|
||||
// 第二位为 1 时,是前台(文件管理)错误
|
||||
BIZ_BAD_REQUEST("41000", "请求参数异常"),
|
||||
BIZ_UNSUPPORTED_PROXY_DOWNLOAD("41001", "该存储源不支持代理下载"),
|
||||
BIZ_INVALID_SIGNATURE("41002", "签名无效或下载地址已过期"),
|
||||
BIZ_PREVIEW_FILE_SIZE_EXCEED("41003", "预览文本文件大小超出系统限制"),
|
||||
BIZ_FILE_NOT_EXIST("41004", "文件不存在"),
|
||||
BIZ_ACCESS_TOO_FREQUENT("41005", "请求太频繁了,请稍后再试"),
|
||||
BIZ_UPLOAD_FILE_NOT_EMPTY("41006", "上传文件不能为空"),
|
||||
BIZ_UPLOAD_FILE_ERROR("41010", "上传文件失败"),
|
||||
BIZ_UPLOAD_FILE_TIMEOUT_ERROR("41026", "上传文件超时"),
|
||||
BIZ_EXPIRE_TIME_ILLEGAL("41007", "过期时间不合法"),
|
||||
BIZ_DELETE_FILE_NOT_EMPTY("41008", "非空文件夹不允许删除"),
|
||||
BIZ_FILE_PATH_ILLEGAL("41009", "文件名/路径存在安全隐患"),
|
||||
BIZ_DIRECT_LINK_NOT_ALLOWED("41011", "当前系统不允许使用直链"),
|
||||
BIZ_SHORT_LINK_NOT_ALLOWED("41012", "当前系统不允许使用短链"),
|
||||
BIZ_SHORT_LINK_EXPIRED("41013", "短链已失效"),
|
||||
BIZ_SHORT_LINK_NOT_FOUNT("41014", "短链不存在"),
|
||||
BIZ_DIRECT_LINK_EXPIRED("41015", "直链已失效"),
|
||||
BIZ_STORAGE_NOT_SUPPORT_OPERATION("41016", "该存储类型不支持此操作"),
|
||||
BIZ_STORAGE_NOT_FOUND("41017", "存储源不存在"),
|
||||
BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION("41018", "非法或未授权的操作"),
|
||||
BIZ_STORAGE_SOURCE_FILE_FORBIDDEN("41019", "文件目录无访问权限"),
|
||||
BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_REQUIRED("41020", "此文件夹需要密码"),
|
||||
BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_ERROR("41021", "密码错误"),
|
||||
BIZ_INVALID_FILE_NAME("41022", "文件名不合法"),
|
||||
BIZ_UNSUPPORTED_OPERATION("41023", "不支持的操作"),
|
||||
BIZ_FTP_CLIENT_POOL_FULL("41024", "FTP 客户端连接池已满"),
|
||||
BIZ_SFTP_CLIENT_POOL_FULL("41025", "SFTP 客户端连接池已满"),
|
||||
BIZ_FOLDER_NOT_EXIST("41026", "文件夹不存在"),
|
||||
BIZ_UPLOAD_FILE_TYPE_NOT_ALLOWED("41027", "不允许上传的文件"),
|
||||
BIZ_RENAME_FILE_TYPE_NOT_ALLOWED("41028", "不允许重命名到该名称"),
|
||||
|
||||
// 第二位为 2 时,是登录错误
|
||||
BIZ_UNAUTHORIZED("42000", "未登录或未授权"),
|
||||
BIZ_LOGIN_ERROR("42001", "登录失败, 账号或密码错误"),
|
||||
BIZ_VERIFY_CODE_ERROR("42002", "验证码错误或已失效"),
|
||||
|
||||
// 第二位为 3 时,是管理员端错误
|
||||
BIZ_ADMIN_ERROR("43000", "操作失败"),
|
||||
BIZ_USER_NOT_EXIST("43001", "用户不存在"),
|
||||
BIZ_USER_EXIST("43002", "用户已存在"),
|
||||
BIZ_PASSWORD_NOT_SAME("43003", "两次密码不一致"),
|
||||
BIZ_OLD_PASSWORD_ERROR("43004", "旧密码不匹配"),
|
||||
BIZ_DELETE_BUILT_IN_USER("43005", "不能删除内置用户"),
|
||||
BIZ_UNSUPPORTED_STORAGE_TYPE("43006", "不支持的存储类型"),
|
||||
BIZ_STORAGE_KEY_EXIST("43007", "存储源别名已存在"),
|
||||
BIZ_AUTO_GET_SHARE_POINT_SITES_ERROR("43008", "自动获取 SharePoint 网站列表失败"),
|
||||
BIZ_ORIGINS_NOT_EMPTY("43009", "请先在 \"站点设置\" 中配置站点域名"),
|
||||
BIZ_2FA_CODE_ERROR("43010", "两步验证失败"),
|
||||
BIZ_STORAGE_INIT_ERROR("43011", "存储源初始化失败"),
|
||||
BIZ_RULE_EXIST("43012", "规则已存在"),
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 通用的无权限异常
|
||||
*/
|
||||
NO_FORBIDDEN("30000", "没有权限"),
|
||||
|
||||
|
||||
/**
|
||||
* 授权校验异常
|
||||
*/
|
||||
PRO_AUTH_CODE_EMPTY("20000", "请先去后台 \"基本设置\" 填写 \"授权码\""),
|
||||
PRO_CHECK_REFERER_EMPTY("20001", "Referer 无效,请检查服务端设置,20001"), // Referer 无效,请检查服务端设置
|
||||
PRO_CHECK_TIME_NO_SYNC("20002", "授权校验失败, 服务器时间异常,20002"), // 授权校验失败, 服务器时间异常.
|
||||
PRO_AUTH_CODE_INVALID_ERROR("20003", "授权码无效, 请检查后台 \"站点设置\" 中的 \"授权码\" 20003"),
|
||||
PRO_CHECK_UNKNOWN_ERROR("20004", "授权验证异常,未知异常,20098"),
|
||||
PRO_MSG_ERROR("20005", null);
|
||||
|
||||
private String code;
|
||||
|
||||
private String message;
|
||||
|
||||
ErrorCode(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误码
|
||||
*
|
||||
* @param code 错误码
|
||||
* @return 返回当前枚举
|
||||
*/
|
||||
public ErrorCode setCode(String code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误信息
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @return 返回当前枚举
|
||||
*/
|
||||
public ErrorCode setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package im.zhaojun.zfile.core.exception;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.dev33.satoken.exception.NotRoleException;
|
||||
import im.zhaojun.zfile.core.controller.FrontIndexController;
|
||||
import im.zhaojun.zfile.core.exception.biz.*;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.core.ErrorPageBizException;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
import im.zhaojun.zfile.core.exception.status.*;
|
||||
import im.zhaojun.zfile.core.exception.system.UploadFileFailSystemException;
|
||||
import im.zhaojun.zfile.core.exception.system.ZFileAuthorizationSystemException;
|
||||
import im.zhaojun.zfile.core.util.AjaxJson;
|
||||
import im.zhaojun.zfile.core.util.RequestHolder;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import im.zhaojun.zfile.module.config.service.SystemConfigService;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
import org.sqlite.SQLiteException;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
@Order(1)
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final ThreadLocal<String> exceptionMessage = new ThreadLocal<>();
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
@Resource
|
||||
private FrontIndexController frontIndexController;
|
||||
|
||||
|
||||
private static final int MAX_FIND_CAUSE_EXCEPTION_DEPTH = 10;
|
||||
|
||||
// ---------------------- status exception start ----------------------
|
||||
|
||||
@ExceptionHandler(value = UnauthorizedAccessException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public AjaxJson<?> unauthorizedAccessException() {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getUnauthorizedResult();
|
||||
}
|
||||
try {
|
||||
String unauthorizedUrl = systemConfigService.getUnauthorizedUrl();
|
||||
RequestHolder.getResponse().sendRedirect(unauthorizedUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getUnauthorizedResult();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = {
|
||||
NotRoleException.class
|
||||
})
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public AjaxJson<?> forbiddenAccessException() {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getForbiddenResult();
|
||||
}
|
||||
try {
|
||||
String forbiddenUrl = systemConfigService.getForbiddenUrl();
|
||||
RequestHolder.getResponse().sendRedirect(forbiddenUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getForbiddenResult();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = ForbiddenAccessException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public AjaxJson<?> forbiddenAccessException(ForbiddenAccessException e) {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
try {
|
||||
String forbiddenUrl = systemConfigService.getForbiddenUrl(e.getCode(), e.getMessage());
|
||||
RequestHolder.getResponse().sendRedirect(forbiddenUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = NotFoundAccessException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public AjaxJson<?> notFoundAccessException(NotFoundAccessException e) {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
try {
|
||||
String notFoundUrl = systemConfigService.getNotFoundUrl(e.getCode(), e.getMessage());
|
||||
RequestHolder.getResponse().sendRedirect(notFoundUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
|
||||
* 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回.
|
||||
*
|
||||
* @return 转发到 /index.html
|
||||
*/
|
||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||
@ResponseBody
|
||||
public String notFoundAccessException() {
|
||||
return frontIndexController.redirect();
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = MethodNotAllowedAccessException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
public AjaxJson<String> methodNotAllowedAccessException(MethodNotAllowedAccessException e) {
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = BadRequestAccessException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public AjaxJson<String> badRequestAccessException(BadRequestAccessException e) {
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
// ---------------------- status exception end ----------------------
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------------- biz exception start ----------------------
|
||||
|
||||
@ExceptionHandler(value = APIHttpRequestBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> apiHttpRequestBizException(APIHttpRequestBizException e) {
|
||||
log.warn("请求第三方 API 异常, 请求地址: {}, 响应码: {}, 响应体: {}", e.getUrl(), e.getResponseCode(), e.getResponseBody());
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = FilePathSecurityBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public AjaxJson<String> filePathSecurityBizException(FilePathSecurityBizException e) {
|
||||
log.warn("获取文件路径存在安全风险, 文件路径: {}", e.getPath());
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = GetPreviewTextContentBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> getPreviewTextContentBizException(GetPreviewTextContentBizException e) {
|
||||
log.warn("获取预览文件内容失败, 文件 url: {}", e.getUrl(), e);
|
||||
return new AjaxJson<>(e.getCode(), "预览文件内容失败, 请联系管理员.");
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = InitializeStorageSourceBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> initializeStorageSourceBizException(InitializeStorageSourceBizException e) {
|
||||
log.error("存储源初始化失败, 存储源 ID: {}.", e.getStorageId(), e);
|
||||
return new AjaxJson<>(e.getCode(), "存储源初始化失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = StorageSourceFileForbiddenAccessBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException e) {
|
||||
log.warn("尝试访问不被授权的文件/目录, 存储源 ID: {}: 目录: {}", e.getStorageId(), e.getPath());
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = StorageSourceIllegalOperationBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException e) {
|
||||
log.warn("存储源非法或未授权的操作, 存储源 ID: {}, 操作类型: {}", e.getStorageId(), e.getAction());
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = CorsBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> corsBizException(CorsBizException e) {
|
||||
log.warn("跨域异常:", e);
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = ErrorPageBizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<?> errorPageBizException(ErrorPageBizException e) {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
try {
|
||||
String errorPageUrl = systemConfigService.getErrorPageUrl(e.getCode(), e.getMessage());
|
||||
RequestHolder.getResponse().sendRedirect(errorPageUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getError(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(value = BizException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> bizException(BizException e) {
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
// ---------------------- biz exception end ----------------------
|
||||
|
||||
|
||||
// ---------------------- system exception end ----------------------
|
||||
|
||||
@ExceptionHandler(value = UploadFileFailSystemException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<String> uploadFileFailSystemException(UploadFileFailSystemException e) {
|
||||
log.warn("上传文件失败, 存储类型: {}, 上传路径: {}, 输入流可用字节数: {}, 响应码: {}, 响应体: {}",
|
||||
e.getStorageTypeEnum(), e.getUploadPath(), e.getInputStreamAvailable(), e.getResponseCode(), e.getResponseBody());
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = ZFileAuthorizationSystemException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<?> zfileAuthorizationSystemException(ZFileAuthorizationSystemException e) {
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = SystemException.class)
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<?> systemException(SystemException e) {
|
||||
return new AjaxJson<>(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------- system exception end ----------------------
|
||||
|
||||
|
||||
|
||||
// ---------------------- common exception end ----------------------
|
||||
|
||||
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
|
||||
@ResponseBody
|
||||
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
|
||||
public AjaxJson<Map<String, String>> handleValidException(Exception e) {
|
||||
BindingResult bindingResult = null;
|
||||
if (e instanceof MethodArgumentNotValidException) {
|
||||
bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
|
||||
} else if (e instanceof BindException) {
|
||||
bindingResult = ((BindException) e).getBindingResult();
|
||||
}
|
||||
Map<String, String> errorMap = new HashMap<>(16);
|
||||
|
||||
Optional.ofNullable(bindingResult)
|
||||
.map(BindingResult::getFieldErrors)
|
||||
.ifPresent(fieldErrors -> {
|
||||
for (FieldError fieldError : fieldErrors) {
|
||||
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
|
||||
}
|
||||
});
|
||||
return new AjaxJson<>(ErrorCode.BIZ_BAD_REQUEST.getCode(), ErrorCode.BIZ_BAD_REQUEST.getMessage(), errorMap);
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler({FileNotFoundException.class})
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public AjaxJson<Void> fileNotFound() {
|
||||
return AjaxJson.getError("文件不存在");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 登录异常拦截器
|
||||
*/
|
||||
@ExceptionHandler(NotLoginException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
@ResponseBody
|
||||
public AjaxJson<?> handlerNotLoginException(NotLoginException e) {
|
||||
if (RequestHolder.isAxiosRequest()) {
|
||||
return AjaxJson.getUnauthorizedResult();
|
||||
}
|
||||
try {
|
||||
String domain = systemConfigService.getRealFrontDomain();
|
||||
if (StringUtils.isBlank(domain)) {
|
||||
domain = "";
|
||||
}
|
||||
String loginUrl = StringUtils.concat(domain, "/login");
|
||||
RequestHolder.getResponse().sendRedirect(loginUrl);
|
||||
} catch (IOException ex) {
|
||||
return AjaxJson.getUnauthorizedResult();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ExceptionHandler
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public AjaxJson<?> extraExceptionHandler(Exception e) {
|
||||
ExceptionType exceptionType = getExceptionType(e);
|
||||
if (exceptionType == ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION) {
|
||||
log.warn(e.getMessage());
|
||||
} else if (exceptionType == ExceptionType.OTHER) {
|
||||
log.error(e.getMessage(), e);
|
||||
} else if (exceptionType == ExceptionType.SPECIFY_MESSAGE_EXCEPTION) {
|
||||
if (exceptionMessage.get() != null) {
|
||||
String message = exceptionMessage.get();
|
||||
log.error("发生异常: {}", message,e );
|
||||
exceptionMessage.remove();
|
||||
return AjaxJson.getError(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.getClass() == Exception.class) {
|
||||
return AjaxJson.getError("系统异常, 请联系管理员");
|
||||
} else {
|
||||
return AjaxJson.getError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static ExceptionType getExceptionType(Exception e) {
|
||||
int findCauseCount = 0;
|
||||
do {
|
||||
if (e instanceof BizException) {
|
||||
return ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION;
|
||||
} else if (e instanceof ClientAbortException) {
|
||||
return ExceptionType.IGNORE_EXCEPTION;
|
||||
} else if (e instanceof SQLiteException && e.getMessage().contains("database is locked")) {
|
||||
exceptionMessage.set("数据库繁忙,请稍后再试");
|
||||
return ExceptionType.SPECIFY_MESSAGE_EXCEPTION;
|
||||
}
|
||||
e = (Exception) e.getCause();
|
||||
findCauseCount++;
|
||||
} while (e != null && findCauseCount < MAX_FIND_CAUSE_EXCEPTION_DEPTH);
|
||||
|
||||
return ExceptionType.OTHER;
|
||||
}
|
||||
|
||||
enum ExceptionType {
|
||||
/**
|
||||
* 忽略打印异常信息和堆栈信息
|
||||
*/
|
||||
IGNORE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 仅打印异常信息, 不打印堆栈信息
|
||||
*/
|
||||
IGNORE_PRINT_STACK_TRACE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 不打印堆栈信息,但指定异常信息
|
||||
*/
|
||||
SPECIFY_MESSAGE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 其他异常, 打印异常信息和堆栈信息
|
||||
*/
|
||||
OTHER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 请求第三方 API 时如果返回非 2xx 状态码, 则抛出此异常. 需记录请求地址, 响应状态码, 响应内容.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#apiHttpRequestBizException(APIHttpRequestBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class APIHttpRequestBizException extends BizException {
|
||||
|
||||
private final String url;
|
||||
|
||||
private final int responseCode;
|
||||
|
||||
private final String responseBody;
|
||||
|
||||
public APIHttpRequestBizException(ErrorCode errorCode, String url, int responseCode, String responseBody) {
|
||||
super(errorCode);
|
||||
this.url = url;
|
||||
this.responseCode = responseCode;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class CorsBizException extends BizException {
|
||||
|
||||
public CorsBizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean printExceptionStackTrace() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文件路径安全异常, 表示文件路径不合法,如包含了 "./" 或 "../" 等字符来尝试访问非法目录.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#filePathSecurityBizException(FilePathSecurityBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class FilePathSecurityBizException extends BizException {
|
||||
|
||||
private final String path;
|
||||
|
||||
public FilePathSecurityBizException(String path) {
|
||||
super(ErrorCode.BIZ_FILE_PATH_ILLEGAL);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 获取预览文件内容异常, 可能是目标连接无法访问/文件不存在等原因.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#getPreviewTextContentBizException(GetPreviewTextContentBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class GetPreviewTextContentBizException extends BizException {
|
||||
|
||||
/**
|
||||
* 获取预览文件的 URL
|
||||
*/
|
||||
private final String url;
|
||||
|
||||
public GetPreviewTextContentBizException(String url, Throwable cause) {
|
||||
super(cause);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 初始化存储源时失败产生的异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#initializeStorageSourceBizException(InitializeStorageSourceBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class InitializeStorageSourceBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
public InitializeStorageSourceBizException(String message, Integer storageId) {
|
||||
super(message);
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
public InitializeStorageSourceBizException(String code, String message, Integer storageId, Throwable cause) {
|
||||
super(code, message, cause);
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 不存在或初始化失败的存储源异常。
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class InvalidStorageSourceBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final String storageKey;
|
||||
|
||||
public InvalidStorageSourceBizException(String storageKey) {
|
||||
super(ErrorCode.INVALID_STORAGE_SOURCE);
|
||||
this.storageKey = storageKey;
|
||||
this.storageId = null;
|
||||
}
|
||||
|
||||
public InvalidStorageSourceBizException(Integer storageId) {
|
||||
super(ErrorCode.INVALID_STORAGE_SOURCE);
|
||||
this.storageId = storageId;
|
||||
this.storageKey = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 访问了禁止访问的存储源文件/目录时抛出此异常.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class StorageSourceFileForbiddenAccessBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final String path;
|
||||
|
||||
public StorageSourceFileForbiddenAccessBizException(Integer storageId, String path) {
|
||||
super(ErrorCode.BIZ_STORAGE_SOURCE_FILE_FORBIDDEN);
|
||||
this.storageId = storageId;
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 对存储源进行非法(未授权)的操作产生的异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class StorageSourceIllegalOperationBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final FileOperatorTypeEnum action;
|
||||
|
||||
public StorageSourceIllegalOperationBizException(Integer storageId, FileOperatorTypeEnum action) {
|
||||
super(ErrorCode.BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION);
|
||||
this.storageId = storageId;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
|
||||
*/
|
||||
public boolean printExceptionStackTrace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public BizException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public BizException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public BizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public BizException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public BizException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.<br>
|
||||
* 使用该类的异常,当该异常被抛出时,会跳转到 500 错误页面(错误码和错误消息可被 {@link #code} 和 {@link #getMessage()} 覆盖),而不是返回 JSON 数据.<br>
|
||||
* 一般使用该异常得请求不会是 AJAX 请求,而是直接在浏览器中访问的页面请求.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class ErrorPageBizException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
|
||||
*/
|
||||
public boolean printExceptionStackTrace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public ErrorPageBizException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public ErrorPageBizException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public ErrorPageBizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public ErrorPageBizException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public ErrorPageBizException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 系统异常, 该类异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 服务器异常等.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class SystemException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public SystemException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public SystemException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public SystemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public SystemException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public SystemException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 错误请求异常, 表示请求参数有误或者服务器无法理解, 一般返回 400 状态码
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#badRequestAccessException(BadRequestAccessException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class BadRequestAccessException extends BizException {
|
||||
|
||||
public BadRequestAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 禁止访问异常, 表示用户没有权限访问该资源, 一般返回 403 状态码. (已经有身份,如果没有身份,应该是 UnauthorizedAccessException)
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#forbiddenAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ForbiddenAccessException extends BizException {
|
||||
|
||||
public ForbiddenAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 错误请求异常, 表示请求方法有误或者服务器无法理解, 一般返回 405 状态码
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#methodNotAllowedAccessException(MethodNotAllowedAccessException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class MethodNotAllowedAccessException extends BizException {
|
||||
|
||||
public MethodNotAllowedAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 访问内容不存在异常, 表示用户请求的资源不存在时抛出, 一般返回 404 状态码.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#notFoundAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class NotFoundAccessException extends BizException {
|
||||
|
||||
public NotFoundAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 禁止访问异常, 表示用户未进行身份认证, 一般返回 401 状态码.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#unauthorizedAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class UnauthorizedAccessException extends BizException {
|
||||
|
||||
public UnauthorizedAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package im.zhaojun.zfile.core.exception.system;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件失败系统异常, 该异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 目标存储源异常等
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#uploadFileFailSystemException(UploadFileFailSystemException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class UploadFileFailSystemException extends SystemException {
|
||||
|
||||
private final StorageTypeEnum storageTypeEnum;
|
||||
|
||||
private final String uploadPath;
|
||||
|
||||
private final Integer inputStreamAvailable;
|
||||
|
||||
private final int responseCode;
|
||||
|
||||
private final String responseBody;
|
||||
|
||||
public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Integer inputStreamAvailable, int responseCode, String responseBody) {
|
||||
super(ErrorCode.BIZ_UPLOAD_FILE_ERROR);
|
||||
this.storageTypeEnum = storageTypeEnum;
|
||||
this.uploadPath = uploadPath;
|
||||
this.inputStreamAvailable = inputStreamAvailable;
|
||||
this.responseCode = responseCode;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package im.zhaojun.zfile.core.exception.system;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
|
||||
/**
|
||||
* ZFile 授权异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#zfileAuthorizationSystemException(ZFileAuthorizationSystemException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ZFileAuthorizationSystemException extends SystemException {
|
||||
|
||||
public ZFileAuthorizationSystemException(String code, String message) {
|
||||
super(code, message);
|
||||
}
|
||||
|
||||
public ZFileAuthorizationSystemException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
public ZFileAuthorizationSystemException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode, cause);
|
||||
}
|
||||
|
||||
}
|
||||
52
src/main/java/im/zhaojun/zfile/core/filter/CorsFilter.java
Normal file
52
src/main/java/im/zhaojun/zfile/core/filter/CorsFilter.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import im.zhaojun.zfile.core.constant.ZFileHttpHeaderConstant;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.cors.CorsUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
@Order(Integer.MIN_VALUE)
|
||||
@Component
|
||||
public class CorsFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
if (httpServletRequest.getRequestURI().equals("/favicon.ico")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String header = httpServletRequest.getHeader(HttpHeaders.ORIGIN);
|
||||
|
||||
List<String> allowHeaders = Arrays.asList("Origin", "X-Requested-With", "Content-Type", "Accept", ZFileHttpHeaderConstant.ZFILE_TOKEN, ZFileHttpHeaderConstant.AXIOS_REQUEST, ZFileHttpHeaderConstant.AXIOS_FROM);
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ObjectUtil.defaultIfNull(header, "*"));
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.join(",", allowHeaders));
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false");
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "600");
|
||||
|
||||
if (!CorsUtils.isPreFlightRequest(httpServletRequest)) {
|
||||
chain.doFilter(httpServletRequest, httpServletResponse);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
39
src/main/java/im/zhaojun/zfile/core/filter/MDCFilter.java
Normal file
39
src/main/java/im/zhaojun/zfile/core/filter/MDCFilter.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import im.zhaojun.zfile.core.constant.MdcConstant;
|
||||
import im.zhaojun.zfile.core.util.ZFileAuthUtil;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* MDC 过滤器, 用于写入 TraceId, 请求 IP, 用户名等信息到日志中.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
public class MDCFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
MDC.put(MdcConstant.TRACE_ID, IdUtil.fastUUID());
|
||||
MDC.put(MdcConstant.IP, JakartaServletUtil.getClientIP(httpServletRequest));
|
||||
MDC.put(MdcConstant.USER, ZFileAuthUtil.getCurrentUserId().toString());
|
||||
|
||||
try {
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse);
|
||||
} finally {
|
||||
MDC.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import im.zhaojun.zfile.core.util.matcher.IRuleMatcher;
|
||||
import im.zhaojun.zfile.core.util.matcher.RuleMatcherFactory;
|
||||
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.module.config.service.SystemConfigService;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检测访问的 IP 和 UA 是否符合系统安全设置中的规则
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
public class SecurityFilter implements Filter {
|
||||
|
||||
private static volatile SystemConfigService systemConfigService;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
// 双重检测锁, 防止多次初始化
|
||||
if (systemConfigService == null) {
|
||||
synchronized (this) {
|
||||
if (systemConfigService == null) {
|
||||
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
String accessIpBlocklist = systemConfig.getAccessIpBlocklist();
|
||||
String accessUaBlocklist = systemConfig.getAccessUaBlocklist();
|
||||
|
||||
// 判断当前访问 IP 是否在黑名单中
|
||||
String currentAccessIp = JakartaServletUtil.getClientIP(httpServletRequest);
|
||||
if (StringUtils.isNotBlank(accessIpBlocklist) && checkIsDisableIP(accessIpBlocklist, currentAccessIp)) {
|
||||
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
httpServletResponse.getWriter().write("disable access.[" + currentAccessIp + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断当前访问 User-Agent 是否在黑名单中
|
||||
String userAgent = httpServletRequest.getHeader(HttpHeaders.USER_AGENT);
|
||||
if (StringUtils.isNotBlank(accessUaBlocklist) && checkIsDisableUA(accessUaBlocklist, userAgent)) {
|
||||
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
httpServletResponse.getWriter().write("disable access.[" + userAgent + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse);
|
||||
}
|
||||
|
||||
private boolean checkIsDisableIP(String accessIpBlocklist, String currentAccessIp) {
|
||||
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.IP);
|
||||
List<String> ruleList = StringUtils.split(accessIpBlocklist, StringUtils.LF);
|
||||
return ruleMatcher.matchAny(ruleList, currentAccessIp);
|
||||
}
|
||||
|
||||
private boolean checkIsDisableUA(String accessUaBlocklist, String currentAccessUA) {
|
||||
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.SPRING_SIMPLE);
|
||||
List<String> ruleList = StringUtils.split(accessUaBlocklist, StringUtils.LF);
|
||||
return ruleMatcher.matchAny(ruleList, currentAccessUA);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.zhaojun.zfile.core.io;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* 自定义 EnsureContentLengthInputStreamResource 可以保证必须实现 InputStream 的 contentLength 方法返回实际的长度.
|
||||
* 此类相较于 {@link org.springframework.core.io.InputStreamResource} 仅实现了 contentLength 方法.
|
||||
* <br><br>
|
||||
* {@link org.springframework.core.io.Resource} implementation for a given {@link InputStream}.
|
||||
* <p>Should only be used if no other specific {@code Resource} implementation
|
||||
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
|
||||
* file-based {@code Resource} implementations where possible.
|
||||
*
|
||||
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
|
||||
* for an <i>already opened</i> resource - therefore returning {@code true} from
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
|
||||
* keep the resource descriptor somewhere, or if you need to read from a stream
|
||||
* multiple times.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 28.12.2003
|
||||
* @see ByteArrayResource
|
||||
* @see org.springframework.core.io.ClassPathResource
|
||||
* @see org.springframework.core.io.FileSystemResource
|
||||
* @see org.springframework.core.io.UrlResource
|
||||
*/
|
||||
public class EnsureContentLengthInputStreamResource extends InputStreamResource {
|
||||
|
||||
private final long contentLength;
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* @param inputStream the InputStream to use
|
||||
*/
|
||||
public EnsureContentLengthInputStreamResource(InputStream inputStream, long contentLength) {
|
||||
super(inputStream);
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package im.zhaojun.zfile.core.io;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 使用装饰器模式, 限速输入流, 单位为字节/秒.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public final class ThrottledInputStream extends InputStream {
|
||||
|
||||
private final InputStream originalInputStream;
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public ThrottledInputStream(InputStream originalInputStream, double bytesPerSecond) {
|
||||
this.originalInputStream = originalInputStream;
|
||||
this.rateLimiter = RateLimiter.create(bytesPerSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
rateLimiter.acquire();
|
||||
return originalInputStream.read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NotNull byte[] b) throws IOException {
|
||||
return originalInputStream.read(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(@NotNull byte[] b, int off, int len) throws IOException {
|
||||
rateLimiter.acquire(len);
|
||||
return originalInputStream.read(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readAllBytes() throws IOException {
|
||||
return originalInputStream.readAllBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readNBytes(int len) throws IOException {
|
||||
return originalInputStream.readNBytes(len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readNBytes(byte[] b, int off, int len) throws IOException {
|
||||
return originalInputStream.readNBytes(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
return originalInputStream.skip(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipNBytes(long n) throws IOException {
|
||||
originalInputStream.skipNBytes(n);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return originalInputStream.available();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
originalInputStream.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mark(int readlimit) {
|
||||
originalInputStream.mark(readlimit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() throws IOException {
|
||||
originalInputStream.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return originalInputStream.markSupported();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package im.zhaojun.zfile.core.io;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* 使用装饰器模式, 限速输出流, 单位为字节/秒.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
public final class ThrottledOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream originalOutputStream;
|
||||
private final RateLimiter rateLimiter;
|
||||
|
||||
public ThrottledOutputStream(OutputStream out, double bytesPerSecond) {
|
||||
this.originalOutputStream = out;
|
||||
this.rateLimiter = RateLimiter.create(bytesPerSecond);
|
||||
}
|
||||
|
||||
public void setRate(double bytesPerSecond) {
|
||||
rateLimiter.setRate(bytesPerSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
rateLimiter.acquire();
|
||||
originalOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
rateLimiter.acquire(b.length);
|
||||
originalOutputStream.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
rateLimiter.acquire(len);
|
||||
originalOutputStream.write(b, off, len);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
originalOutputStream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
originalOutputStream.close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package im.zhaojun.zfile.core.model.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 通用分页请求对象,可继承该类增加业务字段.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
public class PageQueryRequest {
|
||||
|
||||
@Schema(name="分页页数")
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(name="每页条数")
|
||||
private Integer limit = 10;
|
||||
|
||||
@Schema(name="排序字段")
|
||||
private String orderBy = "create_date";
|
||||
|
||||
@Schema(name="排序顺序")
|
||||
private String orderDirection = "desc";
|
||||
|
||||
}
|
||||
107
src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
Normal file
107
src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
Normal file
@@ -0,0 +1,107 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* ajax 请求返回 JSON 格式数据的封装
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class AjaxJson<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final String CODE_SUCCESS = "0"; // 成功状态码
|
||||
|
||||
@Schema(name = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
|
||||
private final String code;
|
||||
|
||||
@Schema(name = "响应消息", example = "ok")
|
||||
private String msg;
|
||||
|
||||
@Schema(name = "响应数据")
|
||||
private T data;
|
||||
|
||||
@Schema(name = "数据总条数,分页情况有效")
|
||||
private final Long dataCount;
|
||||
|
||||
@Schema(name = "跟踪 ID")
|
||||
private String traceId;
|
||||
|
||||
public AjaxJson(String code, String msg) {
|
||||
if (code == null) {
|
||||
code = ErrorCode.SYSTEM_ERROR.getCode();
|
||||
}
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.dataCount = null;
|
||||
}
|
||||
|
||||
public AjaxJson(String code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.dataCount = null;
|
||||
}
|
||||
|
||||
public AjaxJson(String code, String msg, T data, Long dataCount) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.dataCount = dataCount;
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
public static AjaxJson<Void> getSuccess() {
|
||||
return new AjaxJson<>(CODE_SUCCESS, "ok");
|
||||
}
|
||||
|
||||
public static AjaxJson<Void> getSuccess(String msg) {
|
||||
return new AjaxJson<>(CODE_SUCCESS, msg);
|
||||
}
|
||||
|
||||
public static <T> AjaxJson<T> getSuccess(String msg, T data) {
|
||||
return new AjaxJson<>(CODE_SUCCESS, msg, data);
|
||||
}
|
||||
|
||||
public static <T> AjaxJson<T> getSuccessData(T data) {
|
||||
return new AjaxJson<>(CODE_SUCCESS, "ok", data);
|
||||
}
|
||||
|
||||
// 返回分页和数据的
|
||||
public static <T> AjaxJson<T> getPageData(Long dataCount, T data) {
|
||||
return new AjaxJson<>(CODE_SUCCESS, "ok", data, dataCount);
|
||||
}
|
||||
|
||||
// 返回错误
|
||||
public static AjaxJson<Void> getError(String msg) {
|
||||
return new AjaxJson<>(ErrorCode.SYSTEM_ERROR.getCode(), msg);
|
||||
}
|
||||
|
||||
// 返回未登录
|
||||
public static AjaxJson<?> getUnauthorizedResult() {
|
||||
return new AjaxJson<>(ErrorCode.BIZ_UNAUTHORIZED.getCode(), "未登录,请登录后再次访问");
|
||||
}
|
||||
|
||||
// 返回没权限的
|
||||
public static AjaxJson<?> getForbiddenResult() {
|
||||
return new AjaxJson<>(ErrorCode.NO_FORBIDDEN.getCode(), "未授权,请登录正确权限账号再试");
|
||||
}
|
||||
|
||||
// 返回未找到的
|
||||
public static AjaxJson<?> getNotFoundResult() {
|
||||
return new AjaxJson<>(ErrorCode.BIZ_NOT_FOUND.getCode(), ErrorCode.BIZ_NOT_FOUND.getMessage());
|
||||
}
|
||||
|
||||
public static AjaxJson<?> getError(String code, String msg) {
|
||||
return new AjaxJson<>(code, msg);
|
||||
}
|
||||
|
||||
}
|
||||
40
src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java
Normal file
40
src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
/**
|
||||
* 数组工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ArrayUtils {
|
||||
|
||||
/**
|
||||
* 数组是否为空
|
||||
*
|
||||
* @param <T>
|
||||
* 数组元素类型
|
||||
*
|
||||
* @param array
|
||||
* 数组
|
||||
*
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static <T> boolean isEmpty(T[] array) {
|
||||
return array == null || array.length == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组是否不为空
|
||||
*
|
||||
* @param <T>
|
||||
* 数组元素类型
|
||||
*
|
||||
* @param array
|
||||
* 数组
|
||||
*
|
||||
* @return 是否不为空
|
||||
*/
|
||||
public static <T> boolean isNotEmpty(T[] array) {
|
||||
return !isEmpty(array);
|
||||
}
|
||||
|
||||
}
|
||||
10
src/main/java/im/zhaojun/zfile/core/util/CharPool.java
Normal file
10
src/main/java/im/zhaojun/zfile/core/util/CharPool.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
public interface CharPool {
|
||||
|
||||
/**
|
||||
* CHAR 常量:斜杠 {@code '/'} ASCII 47
|
||||
*/
|
||||
char SLASH_CHAR = '/';
|
||||
|
||||
}
|
||||
727
src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java
Normal file
727
src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java
Normal file
@@ -0,0 +1,727 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.text.StrSplitter;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class CharSequenceUtil implements CharPool {
|
||||
|
||||
/**
|
||||
* 找不到索引时的返回值
|
||||
*/
|
||||
public static final int INDEX_NOT_FOUND = -1;
|
||||
|
||||
/**
|
||||
* 字符串常量:{@code "null"} <br>
|
||||
* 注意:{@code "null" != null}
|
||||
*/
|
||||
public static final String NULL = "null";
|
||||
|
||||
/**
|
||||
* 字符串常量:空字符串 {@code ""}
|
||||
*/
|
||||
public static final String EMPTY = "";
|
||||
|
||||
/**
|
||||
* 字符串常量:空格符 {@code " "}
|
||||
*/
|
||||
public static final String SPACE = " ";
|
||||
|
||||
|
||||
/**
|
||||
* 获取 CharSequence 的长度, 如果为 null, 返回 0
|
||||
*
|
||||
* @param ch
|
||||
* 要获取长度的 CharSequence, 可能为 null
|
||||
*
|
||||
* @return CharSequence 的长度
|
||||
*/
|
||||
public static int length(final @Nullable CharSequence ch) {
|
||||
return ch == null ? 0 : ch.length();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link CharSequence} 转为字符串
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static String str(final @Nullable CharSequence cs) {
|
||||
return null == cs ? null : cs.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断 CharSequence 是否为空
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static boolean isEmpty(final @Nullable CharSequence cs) {
|
||||
return cs == null || cs.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* CharSequence 是否不为空
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(final @Nullable CharSequence cs) {
|
||||
return !isEmpty(cs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>指定字符串数组中的元素,是否全部为空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty() // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("", null) // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("123", "") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("123", "abc") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty(" ", "\t", "\n") // false}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param strs
|
||||
* 字符串列表
|
||||
*
|
||||
* @return 所有字符串是否都为空
|
||||
*/
|
||||
public static boolean isAllEmpty(final @Nullable CharSequence... strs) {
|
||||
if (strs == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CharSequence str : strs) {
|
||||
if (isNotEmpty(str)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>是否包含空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty() // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("", null) // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("123", "") // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("123", "abc") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty(" ", "\t", "\n") // false}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param strs
|
||||
* 字符串列表
|
||||
*
|
||||
* @return 是否包含空字符串
|
||||
*/
|
||||
public static boolean hasEmpty(final @Nullable CharSequence... strs) {
|
||||
if (ArrayUtils.isEmpty(strs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CharSequence str : strs) {
|
||||
if (isEmpty(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>指定字符串数组中的元素,是否都不为空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty() // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("", null) // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "abc") // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty(" ", "\t", "\n") // true}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param args
|
||||
* 字符串数组
|
||||
*
|
||||
* @return 所有字符串是否都不为为空白
|
||||
*/
|
||||
public static boolean isAllNotEmpty(final @Nullable CharSequence... args) {
|
||||
return !hasEmpty(args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否为空白
|
||||
*
|
||||
* @param ch
|
||||
* 要判断的字符串, 可能为 null
|
||||
*
|
||||
* @return 是否为空白
|
||||
*/
|
||||
public static boolean isBlank(final @Nullable CharSequence ch) {
|
||||
final int strLen = ch == null ? 0 : ch.length();
|
||||
if (strLen == 0) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < strLen; i++) {
|
||||
if (!Character.isWhitespace(ch.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否不为空白
|
||||
*
|
||||
* @param cs
|
||||
* 字符串
|
||||
*
|
||||
* @return 是否不为空白
|
||||
*/
|
||||
public static boolean isNotBlank(final @Nullable CharSequence cs) {
|
||||
return !isBlank(cs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个 CharSequence 是否相等, 区分大小写, 如果两个都为 null, 返回 true
|
||||
*
|
||||
* @param cs1
|
||||
* CharSequence 1, 可能为 null
|
||||
*
|
||||
* @param cs2
|
||||
* CharSequence 2, 可能为 null
|
||||
*
|
||||
* @return 是否相等
|
||||
*/
|
||||
public static boolean equals(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
|
||||
if (cs1 == cs2) {
|
||||
return true;
|
||||
}
|
||||
if (cs1 == null || cs2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (cs1.length() != cs2.length()) {
|
||||
return false;
|
||||
}
|
||||
if (cs1 instanceof String && cs2 instanceof String) {
|
||||
return cs1.equals(cs2);
|
||||
}
|
||||
// 逐个比较
|
||||
final int length = cs1.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (cs1.charAt(i) != cs2.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个 CharSequence 是否相等, 可以选择是否忽略大小写, 如果两个都为 null, 返回 true
|
||||
*
|
||||
* @param cs1
|
||||
* 字符串 1
|
||||
*
|
||||
* @param cs2
|
||||
* 字符串 2
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否相等
|
||||
*/
|
||||
public static boolean equals(final @Nullable CharSequence cs1,final @Nullable CharSequence cs2, boolean ignoreCase) {
|
||||
return ignoreCase ? equalsIgnoreCase(cs1, cs2) : equals(cs1, cs2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否相等, 忽略大小写
|
||||
*
|
||||
* @param cs1
|
||||
* 字符串 1
|
||||
*
|
||||
* @param cs2
|
||||
* 字符串 2
|
||||
*
|
||||
* @return 忽略大小写后是否相等
|
||||
*/
|
||||
public static boolean equalsIgnoreCase(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
|
||||
if (cs1 == cs2) {
|
||||
return true;
|
||||
}
|
||||
if (cs1 == null || cs2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (cs1.length() != cs2.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cs1.toString().equalsIgnoreCase(cs2.toString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串,如果分隔符不存在则返回原字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static List<String> split(final CharSequence str, final CharSequence separator) {
|
||||
return split(str, separator, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符字符
|
||||
*
|
||||
* @param isTrim
|
||||
* 是否去除切分字符串后每个元素两边的空格
|
||||
*
|
||||
* @param ignoreEmpty
|
||||
* 是否忽略空串
|
||||
*
|
||||
* @return 切分后的集合
|
||||
*/
|
||||
public static List<String> split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) {
|
||||
return split(str, separator, 0, isTrim, ignoreEmpty);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符字符
|
||||
*
|
||||
* @param limit
|
||||
* 限制分片数,-1 不限制
|
||||
*
|
||||
* @param isTrim
|
||||
* 是否去除切分字符串后每个元素两边的空格
|
||||
*
|
||||
* @param ignoreEmpty
|
||||
* 是否忽略空串
|
||||
*
|
||||
* @return 切分后的集合
|
||||
*/
|
||||
public static List<String> split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) {
|
||||
final String separatorStr = (null == separator) ? null : separator.toString();
|
||||
return StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 指定字符串是否在字符串中出现过
|
||||
*
|
||||
* @param str
|
||||
* 字符串
|
||||
*
|
||||
* @param searchStr
|
||||
* 被查找的字符串
|
||||
*
|
||||
* @return 是否包含
|
||||
*/
|
||||
public static boolean contains(final @Nullable CharSequence str, final @Nullable CharSequence searchStr) {
|
||||
if (null == str || null == searchStr) {
|
||||
return false;
|
||||
}
|
||||
return str.toString().contains(searchStr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串
|
||||
*
|
||||
* @param str
|
||||
* 指定字符串
|
||||
*
|
||||
* @param testStrs
|
||||
* 需要检查的字符串数组
|
||||
*
|
||||
* @return 是否包含任意一个字符串
|
||||
*/
|
||||
public static boolean containsAny(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
|
||||
if (isEmpty(str) || ArrayUtils.isEmpty(testStrs)) {
|
||||
return false;
|
||||
}
|
||||
for (CharSequence checkStr : testStrs) {
|
||||
if (null != checkStr && str.toString().contains(checkStr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串<br>
|
||||
* 忽略大小写
|
||||
*
|
||||
* @param str
|
||||
* 指定字符串
|
||||
*
|
||||
* @param testStrs
|
||||
* 需要检查的字符串数组
|
||||
*
|
||||
* @return 是否包含任意一个字符串
|
||||
*/
|
||||
public static boolean containsAnyIgnoreCase(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
|
||||
return StringUtils.containsAnyIgnoreCase(str, testStrs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 以 conjunction 为分隔符将多个对象转换为字符串
|
||||
*
|
||||
* @param conjunction
|
||||
* 分隔符
|
||||
*
|
||||
* @param objs
|
||||
* 数组
|
||||
*
|
||||
* @return 连接后的字符串
|
||||
*/
|
||||
public static String join(CharSequence conjunction, Object... objs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < objs.length; i++) {
|
||||
Object item = objs[i];
|
||||
sb.append(item);
|
||||
if (i < objs.length - 1) {
|
||||
sb.append(conjunction);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 以 conjunction 为分隔符将 Collection 对象转换为字符串
|
||||
*
|
||||
* @param conjunction
|
||||
* 分隔符
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 连接后的字符串
|
||||
*/
|
||||
public static String join(CharSequence conjunction, Collection<?> collection) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Object item : collection) {
|
||||
sb.append(item).append(conjunction);
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
sb.delete(sb.length() - conjunction.length(), sb.length());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(CharSequence str, CharSequence prefix) {
|
||||
return startWith(str, prefix, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头,忽略大小写
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) {
|
||||
return startWith(str, prefix, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) {
|
||||
return startWith(str, prefix, ignoreCase, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头<br>
|
||||
* 如果给定的字符串和开头字符串都为 null 则返回 true,否则任意一个值为 null 返回 false<br>
|
||||
* <pre>
|
||||
* CharSequenceUtil.startWith("123", "123", false, true); -- false
|
||||
* CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
|
||||
* CharSequenceUtil.startWith("abc", "abc", true, true); -- false
|
||||
* </pre>
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @param ignoreEquals
|
||||
* 是否忽略字符串相等的情况
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(final @Nullable CharSequence str, final @Nullable CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) {
|
||||
if (null == str || null == prefix) {
|
||||
if (ignoreEquals) {
|
||||
return false;
|
||||
}
|
||||
return null == str && null == prefix;
|
||||
}
|
||||
|
||||
boolean isStartWith = str.toString()
|
||||
.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());
|
||||
|
||||
if (isStartWith) {
|
||||
return (!ignoreEquals) || (!equals(str, prefix, ignoreCase));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix) {
|
||||
return endWith(str, suffix, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase) {
|
||||
return endWith(str, suffix, ignoreCase, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @param ignoreEquals
|
||||
* 是否忽略字符串相等的情况
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) {
|
||||
if (null == str || null == suffix) {
|
||||
if (ignoreEquals) {
|
||||
return false;
|
||||
}
|
||||
return null == str && null == suffix;
|
||||
}
|
||||
|
||||
final int strOffset = str.length() - suffix.length();
|
||||
boolean isEndWith = str.toString()
|
||||
.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());
|
||||
|
||||
if (isEndWith) {
|
||||
return (!ignoreEquals) || (!equals(str, suffix, ignoreCase));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 去掉指定前缀
|
||||
*
|
||||
* @param str
|
||||
* 字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 前缀
|
||||
*
|
||||
* @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串
|
||||
*/
|
||||
public static String removePrefix(final @Nullable CharSequence str, final @Nullable CharSequence prefix) {
|
||||
if (isEmpty(str) || isEmpty(prefix)) {
|
||||
return str(str);
|
||||
}
|
||||
String str2 = str.toString();
|
||||
String prefix2 = prefix.toString();
|
||||
if (str2.startsWith(prefix2)) {
|
||||
return str.subSequence(prefix.length(), str.length()).toString();
|
||||
}
|
||||
return str2; // 若前缀不是 prefix,返回原字符串
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回第一个非 {@code null} 元素
|
||||
*
|
||||
* @param strs
|
||||
* 多个元素
|
||||
*
|
||||
* @param <T>
|
||||
* 元素类型
|
||||
*
|
||||
* @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends CharSequence> T firstNonNull(T... strs) {
|
||||
if (ArrayUtils.isNotEmpty(strs)) {
|
||||
for (T str : strs) {
|
||||
if (isNotEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取分隔字符串之前的字符串,不包括分隔字符串<br>
|
||||
* 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串<br>
|
||||
* 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下:
|
||||
*
|
||||
* <pre>
|
||||
* CharSequenceUtil.subBefore(null, *, false) = null
|
||||
* CharSequenceUtil.subBefore("", *, false) = ""
|
||||
* CharSequenceUtil.subBefore("abc", "a", false) = ""
|
||||
* CharSequenceUtil.subBefore("abcba", "b", false) = "a"
|
||||
* CharSequenceUtil.subBefore("abc", "c", false) = "ab"
|
||||
* CharSequenceUtil.subBefore("abc", "d", false) = "abc"
|
||||
* CharSequenceUtil.subBefore("abc", "", false) = ""
|
||||
* CharSequenceUtil.subBefore("abc", null, false) = "abc"
|
||||
* </pre>
|
||||
*
|
||||
* @param string
|
||||
* 被查找的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔字符串(不包括)
|
||||
*
|
||||
* @param isLastSeparator
|
||||
* 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个
|
||||
*
|
||||
* @return 切割后的字符串
|
||||
*/
|
||||
public static String subBefore(final @Nullable CharSequence string, final @Nullable CharSequence separator, boolean isLastSeparator) {
|
||||
if (isEmpty(string) || separator == null) {
|
||||
return null == string ? null : string.toString();
|
||||
}
|
||||
|
||||
final String str = string.toString();
|
||||
final String sep = separator.toString();
|
||||
if (sep.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);
|
||||
if (INDEX_NOT_FOUND == pos) {
|
||||
return str;
|
||||
}
|
||||
if (0 == pos) {
|
||||
return EMPTY;
|
||||
}
|
||||
return str.substring(0, pos);
|
||||
}
|
||||
|
||||
}
|
||||
41
src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
Normal file
41
src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* Class & 反射相关工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ClassUtils {
|
||||
|
||||
public static Class<?> forName(String className) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类的泛型类型, 只获取第一个泛型类型
|
||||
*
|
||||
* @param clazz
|
||||
* 泛型类
|
||||
*
|
||||
* @return 泛型类型
|
||||
*/
|
||||
public static Class<?> getClassFirstGenericsParam(Class<?> clazz) {
|
||||
Type genericSuperclass = clazz.getGenericSuperclass();
|
||||
Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
|
||||
return (Class<?>) actualTypeArgument;
|
||||
}
|
||||
|
||||
public static Class<?> getGenericType(Field field) {
|
||||
ParameterizedType listType = (ParameterizedType) field.getGenericType();
|
||||
return (Class<?>) listType.getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
}
|
||||
131
src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java
Normal file
131
src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.lang.func.Func1;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
public class CollectionUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 判断集合是否为空
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static boolean isEmpty(@Nullable Collection<?> collection) {
|
||||
return (collection == null || collection.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断集合是否不为空
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(@Nullable Collection<?> collection) {
|
||||
return !isEmpty(collection);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从集合中获取第一个元素, 如果集合为空则返回 {@code null}
|
||||
*
|
||||
* @param list
|
||||
* 集合,可能为 {@code null}
|
||||
*
|
||||
* @return 第一个元素,如果集合为空则返回 {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T getFirst(@Nullable List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从集合中获取最后一个元素, 如果集合为空则返回 {@code null}
|
||||
*
|
||||
* @param list
|
||||
* 集合,可能为 {@code null}
|
||||
*
|
||||
* @return 最后一个元素,如果集合为空则返回 {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T getLast(@Nullable List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加入全部
|
||||
*
|
||||
* @param <T>
|
||||
* 集合元素类型
|
||||
*
|
||||
* @param collection
|
||||
* 被加入的集合 {@link Collection}
|
||||
*
|
||||
* @param values
|
||||
* 要加入的内容数组
|
||||
*
|
||||
* @return 原集合
|
||||
*/
|
||||
public static <T> Collection<T> addAll(Collection<T> collection, T[] values) {
|
||||
if (null != collection && null != values) {
|
||||
Collections.addAll(collection, values);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterable 转换为 Map, 根据指定的 keyFunc 函数生成 Key. Value 为 Iterable 中的元素.<br>
|
||||
* 可以指定将结果放入的 Map, 如不指定则会新建一个 HashMap 返回.
|
||||
*
|
||||
* @param <K>
|
||||
* Map Key 类型
|
||||
*
|
||||
* @param <V>
|
||||
* Map Value 类型
|
||||
*
|
||||
* @param values
|
||||
* 被转换的 Iterable
|
||||
*
|
||||
* @param map
|
||||
* 转换后的 Value 存放的 Map, 如果为 {@code null} 则新建一个 HashMap
|
||||
*
|
||||
* @param keyFunc
|
||||
* 生成 Map 的 Key 的函数
|
||||
*
|
||||
* @return 转换后的 Map
|
||||
*/
|
||||
public static <K, V> Map<K, V> toMap(final @Nullable Iterable<V> values, final @Nullable Map<K, V> map, final @Nullable Func1<V, K> keyFunc) {
|
||||
if (values == null || keyFunc == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final Map<K, V> result = map == null ? new HashMap<>() : map;
|
||||
|
||||
for (V value : values) {
|
||||
try {
|
||||
result.put(keyFunc.call(value), value);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
52
src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java
Normal file
52
src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import com.alibaba.dcm.DnsCacheManipulator;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
public class DnsUtil {
|
||||
|
||||
/**
|
||||
* 通过 HTTP DNS 获取域名对应的 IP 地址
|
||||
*
|
||||
* @param domain
|
||||
* 域名
|
||||
*
|
||||
* @return IP 地址数组
|
||||
*/
|
||||
public static @Nullable String[] getDomainIpByHttpDns(String domain) {
|
||||
String jsonArrayStr = cn.hutool.http.HttpUtil.get("http://223.5.5.5/resolve?name=" + domain + "&short=1", 3000);
|
||||
JSONArray jsonArray = JSONArray.parseArray(jsonArrayStr);
|
||||
if (!jsonArray.isEmpty()) {
|
||||
String[] result = new String[jsonArray.size()];
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
result[i] = jsonArray.getString(i);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 HTTP DNS 获取域名对应的 IP 地址, 并设置 DNS 缓存.
|
||||
*
|
||||
* @param domain
|
||||
* 域名
|
||||
*
|
||||
* @param cacheTime
|
||||
* 缓存时间, 单位: 毫秒
|
||||
*
|
||||
* @return IP 地址数组
|
||||
*/
|
||||
public static String[] getDomainIpByHttpDnsAndCache(String domain, int cacheTime) {
|
||||
String[] domainIpByHttpDns = getDomainIpByHttpDns(domain);
|
||||
if (domainIpByHttpDns != null) {
|
||||
// 设置 DNS 缓存
|
||||
DnsCacheManipulator.setDnsCache(cacheTime, domain, domainIpByHttpDns);
|
||||
}
|
||||
return domainIpByHttpDns;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 枚举转换工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class EnumConvertUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 根据枚举 class 和值获取对应的枚举对象
|
||||
*
|
||||
* @param clazz
|
||||
* 枚举类 Class
|
||||
*
|
||||
* @param value
|
||||
* 枚举值
|
||||
*
|
||||
* @return 枚举对象
|
||||
*/
|
||||
public static Enum<?> convertStrToEnum(Class<?> clazz, Object value) {
|
||||
if (!ClassUtil.isEnum(clazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Field[] fields = ReflectUtil.getFields(clazz);
|
||||
for (Field field : fields) {
|
||||
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
|
||||
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
|
||||
|
||||
if (jsonValuePresent || enumValuePresent) {
|
||||
Object[] enumConstants = clazz.getEnumConstants();
|
||||
|
||||
for (Object enumObj : enumConstants) {
|
||||
if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) {
|
||||
return (Enum<?>) enumObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换枚举对象为字符串, 如果枚举对象没有定义 JsonValue 注解, 则使用 EnumValue 注解的值
|
||||
*
|
||||
* @param enumObj
|
||||
* 枚举对象
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static String convertEnumToStr(Object enumObj) {
|
||||
Class<?> clazz = enumObj.getClass();
|
||||
if (!ClassUtil.isEnum(clazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Field[] fields = ReflectUtil.getFields(clazz);
|
||||
for (Field field : fields) {
|
||||
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
|
||||
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
|
||||
|
||||
if (jsonValuePresent || enumValuePresent) {
|
||||
return Convert.toStr(ReflectUtil.getFieldValue(enumObj, field));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
Normal file
70
src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.comparator.CompareUtil;
|
||||
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* 文件比较器
|
||||
* <ul>
|
||||
* <li>文件夹始终比文件排序高</li>
|
||||
* <li>默认按照名称排序</li>
|
||||
* <li>默认排序为升序</li>
|
||||
* <li>按名称排序不区分大小写</li>
|
||||
* </ul>
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class FileComparator implements Comparator<FileItemResult> {
|
||||
|
||||
private String sortBy;
|
||||
|
||||
private String order;
|
||||
|
||||
public FileComparator(String sortBy, String order) {
|
||||
this.sortBy = sortBy;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个文件的大小
|
||||
*
|
||||
* @param o1
|
||||
* 第一个文件
|
||||
*
|
||||
* @param o2
|
||||
* 第二个文件
|
||||
*
|
||||
* @return 比较结果
|
||||
*/
|
||||
@Override
|
||||
public int compare(FileItemResult o1, FileItemResult o2) {
|
||||
if (sortBy == null) {
|
||||
sortBy = "name";
|
||||
}
|
||||
|
||||
if (order == null) {
|
||||
order = "asc";
|
||||
}
|
||||
FileTypeEnum o1Type = o1.getType();
|
||||
FileTypeEnum o2Type = o2.getType();
|
||||
NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();
|
||||
if (o1Type.equals(o2Type)) {
|
||||
int result = switch (sortBy) {
|
||||
case "time" -> CompareUtil.compare(o1.getTime(), o2.getTime());
|
||||
case "size" -> CompareUtil.compare(o1.getSize(), o2.getSize());
|
||||
default -> naturalOrderComparator.compare(o1.getName(), o2.getName());
|
||||
};
|
||||
return "asc".equals(order) ? result : -result;
|
||||
}
|
||||
|
||||
if (o1Type.equals(FileTypeEnum.FOLDER)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user