Compare commits

..

85 Commits
4.1.2 ... 4.2.0

Author SHA1 Message Date
zhaojun
f5b6f8da4e 补充脚本文件 2025-04-03 22:54:42 +08:00
zhaojun
6da72845e4 补充 Dockerfile 文件 2025-04-03 22:40:10 +08:00
zhaojun
81b9ac5923 从捐赠版合并部分代码 2025-04-03 21:33:53 +08:00
zhao jun
8f8dc86365 Update README.md 2024-10-26 10:17:40 +08:00
zhao jun
f9d46edbcc Update README.md 2024-10-03 17:00:53 +08:00
zhao jun
e3991be3b7 📝 修改 issue 模板,使用自定义 issue helper 来实现 2023-12-10 04:40:40 +00:00
zhao jun
d42853abe2 Update README.md 2023-11-22 19:27:51 +08:00
zhao jun
e5c6a1aca4 Update README.md 2023-08-01 08:38:57 +08:00
zhao jun
b5d46912d2 📝 更新文档描述,增加赞助商 Logo 2023-07-10 20:38:21 +08:00
zhaojun
dfcbaf7158 👷 支持通过参数调整打包方式 2023-05-30 11:22:14 +08:00
zhaojun
17257dccda 🔖 升级依赖版本,发布 4.1.5 版本 2023-05-28 13:07:51 +08:00
zhaojun
1c1248306c 提交静态页面 2023-05-28 13:06:29 +08:00
zhaojun
d5873a7f97 🐛 修复类加载顺序不同,导致在未初始化数据库时就执行了 sql 脚本的 bug 2023-05-28 10:14:54 +08:00
zhaojun
7437dc8936 🙈 更新 .gitignore 文件 2023-05-27 18:43:42 +08:00
zhaojun
549a599a5b 修改 "是否默认记住密码" 功能值为 false 2023-05-27 18:42:18 +08:00
zhaojun
38effb0bb7 新增存储源复制功能 2023-05-27 16:54:02 +08:00
zhaojun
1e25c23b0b 🎨 优化代码 2023-05-27 16:53:44 +08:00
zhaojun
c870d32777 新增是否默认记住文件夹密码功能 2023-05-27 16:51:51 +08:00
zhaojun
b86e487a47 新增辅助测试 ant 表达式请求参数 2023-05-27 16:51:24 +08:00
zhaojun
330713509e 直链日志页返回值新增存储源 key 2023-05-27 16:51:01 +08:00
zhaojun
ecf85dfe9e 🐛 修复多吉云令牌无法自动刷新的 bug 2023-05-27 16:50:31 +08:00
zhaojun
ac3b4283a3 新增网站 favicon 网站地址自定义功能,返回的 html 是已经修改过的,不是等待页面加载完再修改。 2023-05-27 16:50:14 +08:00
zhaojun
141f9dee5e 修改直链代码处理方式,兼容性更好,并增加短链有效期功能 2023-05-27 16:47:23 +08:00
zhaojun
ce9f809ab5 新增辅助测试 ant 表达式和获取客户端 ip 的 Controller 2023-05-27 16:44:11 +08:00
zhaojun
b83af8dc5e 复制移动文件功能 2023-05-27 16:42:39 +08:00
zhaojun
ce0a7bd6ef 抽取代码到工具类 2023-05-27 16:26:22 +08:00
zhaojun
008425734c 下载日志增加下载类型功能 2023-05-27 16:25:53 +08:00
zhaojun
e078366395 ✏️ 修改拼写错误的方法名 2023-05-27 16:20:02 +08:00
zhaojun
69ec12ab99 🐛 修复直短链下载响应头问题,导致安卓手机下载 apk 时自动变 zip 的问题 2023-05-27 16:19:23 +08:00
zhaojun
8e93874c69 🐛 修复某些情况无限重定向的 bug 2023-05-27 16:18:54 +08:00
zhaojun
2a6f0f94cc ⬆️ 升级依赖版本,修复安全漏洞 2023-05-27 16:18:20 +08:00
zhaojun
d501d96ad6 🗃️ 去除无用 mybatis xml 片段 2023-05-27 16:13:14 +08:00
zhaojun
73569a63f8 🗃️ 数据库兼容性增强 2023-05-27 16:12:15 +08:00
zhaojun
71e6ba4d8b 🔊 增加 sqlite 数据库初始化日志,便于排查问题 2023-05-27 16:03:47 +08:00
赵俊
72a627be74 合并拉取请求 #526
fix: 修复目录穿越问题
2023-05-27 16:00:22 +08:00
赵俊
0344f687b6 合并拉取请求 #538
fix dogecloud 保存存储源异常
2023-05-27 15:59:35 +08:00
LinXin
1b0789cdd0 补充响应错误码文档地址. 2023-05-24 22:13:16 +08:00
liupengyu
7cf16754f8 fix dogecloud 保存存储源异常 2023-05-23 14:03:38 +08:00
lxh
6aefc107e7 fix: 修复目录穿越问题 2023-04-23 16:59:11 +08:00
zhaojun
e082043f99 🔖 发布 4.1.4 版本 2023-03-05 17:03:38 +08:00
zhaojun
a55e8ae2ad 提交静态页面 2023-03-05 17:02:43 +08:00
zhaojun
188431b64d Merge remote-tracking branch 'origin/main' 2023-03-05 16:50:04 +08:00
赵俊
7f23dcb7c4 合并拉取请求 #507
fix-bug:本地存储上传文件没有关闭流
2023-03-05 16:49:35 +08:00
zhaojun
8dcb64a60d ✏️ 修改 issue 模板 2023-03-05 15:42:35 +08:00
zhaojun
d9c64ff369 ⬆️ 升级依赖版本 2023-03-05 15:38:51 +08:00
zhaojun
6963b1d593 ✏️ 修改文档,增加谷歌云和多吉云支持 2023-03-05 15:37:12 +08:00
zhaojun
0a61e1047d 捕获 ClientAbortException 异常,不进行异常输出 2023-03-05 15:34:34 +08:00
zhaojun
3f02cd9832 🐛 修复存储源别名修改后再修改回去提示占用的 BUG 2023-03-05 15:34:00 +08:00
zhaojun
3aa42c00fa 🐛 修复七牛对私有空间使用自定义域名后无法正常下载的 bug 2023-03-05 15:33:38 +08:00
zhaojun
77b2253ff6 优化本地存储检测安全性的代码 2023-03-05 15:33:29 +08:00
zhaojun
71a4fdfbaf 🐛 修复 GoogleDrive 快捷方式文件夹无法正常获取内容的 BUG 2023-03-05 15:33:14 +08:00
zhaojun
2ecd69dc51 🐛 修复自动设置 CORS 时,某些 S3 兼容性不同导致的 BUG(BackBlaze 不支持 * 和实际域名写到一起,不支持空值) 2023-03-05 15:32:45 +08:00
zhaojun
ebae9ba5c8 增加动态忽略参数不显示到前端功能 2023-03-05 15:31:59 +08:00
zhaojun
b2fd722443 为多吉云增加存储源参数 2023-03-05 15:31:46 +08:00
zhaojun
e2ce404e87 为了安全性,去除从服务器加载文本文件的功能 2023-03-05 15:31:18 +08:00
zhaojun
6cfbe7689e 更显眼的提示用户使用 Google Drive 时需要自建 API 2023-03-05 15:30:38 +08:00
zhaojun
b95c1d890b 更显眼的提示用户腾讯云使用 CDN 回源鉴权后需要关闭 ZFile 中私有空间开关。 2023-03-05 15:29:48 +08:00
zhaojun
461c77012a 增加动态忽略参数不显示到前端功能 2023-03-05 15:29:36 +08:00
zhaojun
9328e0ea9d 根据查询条件批量删除直链 2023-03-05 15:28:14 +08:00
zhaojun
aefa928a19 🐛 修复短链对应的存储源关闭后,存储源仍然可以访问的 bug 2023-03-05 15:27:31 +08:00
zhaojun
89c6c515f1 🐛 避免未控制并发生成短链导致生成多条的 BUG 2023-03-05 15:26:22 +08:00
zhaojun
dcadffa265 增加是否缓存数据库的开关 2023-03-05 15:24:27 +08:00
zhaojun
300e58e92c 限制单 IP 直链单位时间内下载次数 2023-03-05 15:23:47 +08:00
zhaojun
96b71e4f8d 增加站点首页 Logo 自定义功能,Logo 支持点击跳转第三方链接。
增加默认排序字段功能,增加分页加载更多功能。
2023-03-05 15:16:55 +08:00
zhaojun
456aabb893 新增多吉云支持 2023-03-05 15:12:26 +08:00
wlplove007
b4d2ca238f fix-bug:本地存储上传文件没有关闭流 2023-03-02 20:45:53 +08:00
zhaojun
2afb841fd9 🔖 发布 4.1.3 版本 2022-11-26 20:02:31 +08:00
zhaojun
2b09812153 💄 更新前端页面 2022-11-26 20:02:18 +08:00
zhaojun
972099a598 优化 logback 日志输出格式及存储方式 2022-11-26 18:14:56 +08:00
zhaojun
9335d78d60 兼容 3.x 版本获取令牌功能 2022-11-26 18:11:50 +08:00
zhaojun
f332cb929b 🐛 修复本地存储不支持上传大小为 0 的文件. 2022-11-26 18:11:26 +08:00
zhaojun
a190a2ec6e 🐛 校验本地存储路径合法性,防止恶意获取上级目录。 2022-11-26 18:11:18 +08:00
zhaojun
5bfa7037cb ✏️ 增加 SharePoint 网站提示文本 2022-11-26 18:10:26 +08:00
zhaojun
4a94c879b8 🐛 修复 referer 防盗链允许为空时,仍然去黑/白名单校验的 bug 2022-11-26 18:10:04 +08:00
zhaojun
38161f96e1 优化微软 OneDrive、SharePoint 相关存储的代码, 更好的输出日志和重构代码。 2022-11-26 18:00:16 +08:00
zhaojun
33c751ab33 🐛 修复密码文件夹不正确的情况下,也显示出了文件夹 readme 的 bug 2022-11-26 17:59:52 +08:00
zhaojun
d12cbd2383 🐛 修复本地存储不支持上传大小为 0 的文件. 2022-11-26 17:59:14 +08:00
zhaojun
5f84becf08 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:58:50 +08:00
zhaojun
432fd89c0f 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:57:26 +08:00
zhaojun
84c8adc9d2 优化存储源初始化方式,增加 name 的注入,以便更全面的日志输出。 2022-11-26 17:56:54 +08:00
zhaojun
ba2523ac8a 重构代码,优化方法名 2022-11-26 17:56:12 +08:00
zhaojun
dcdd25c01f 🐛 修复 referer 防盗链允许为空时,仍然去黑/白名单校验的 bug 2022-11-26 17:55:15 +08:00
zhaojun
278e320550 🐛 修复 Referer 防盗链不生效的 bug 2022-11-26 17:54:43 +08:00
zhaojun
1bfa66cc49 更新 onedrive 使用的 restTemplate 客户端,增加日志记录功能,并移除请求头中写 storageId 来获取 AccessToken 的功能。 2022-11-26 17:54:07 +08:00
zhaojun
20fb2b3baa 🐛 修复本地存储删除失败的 bug 2022-09-24 20:11:03 +08:00
922 changed files with 18043 additions and 12878 deletions

View File

@@ -1,24 +0,0 @@
---
name: BUG 反馈
about: 事情不像预期的那样工作吗?
title: ''
labels: 'bug'
assignees: ''
---
为了帮助我们更好的解决您的问题,请填写以下选项(不填写完整可能会被直接关闭 issue
- 是否已搜索其他 issue没有人提过这个问题
- 当前 ZFile 版本:
- 是否尝试最新版是否已解决此问题:
- 是否尝试重启 ZFile且问题依旧存在
- 是否已尝试清空浏览器缓存,且问题依旧存在?:
- 操作系统(如 Windows、Mac、iOS、安卓
- 浏览器(如 Chrome、Firefox、SafariX 浏览器):
- 做什么操作提示的错误?:
- 期望行为(应该是什么样的结果):
- 当前行为(当前是什么样的结果):
- 错误日志(可选):
- 复现步骤(可选):
- 您的额外信息(可选):

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

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 创建 Issue
url: https://issue.zfile.vip/
about: 未通过 https://issue.zfile.vip/ 创建的问题可能会被立即关闭。

View File

@@ -1,17 +0,0 @@
---
name: 功能建议
about: 想让我们为 ZFile 增加什么功能吗?
title: 'feat: '
labels: 'Feature Request'
assignees: ''
---
为了帮助我们更好的解决您的问题,请填写以下选项(不填写完整可能会被直接关闭 issue
- 是否已搜索其他 issue没有人提过这个功能
- 是否已尝试使用最新版本,且仍然没有此功能?:
- 功能概述:
- 功能动机:
- 详细解释(可选):

4
.gitignore vendored
View File

@@ -18,6 +18,8 @@ target/
*.iws
*.iml
*.ipr
.fastRequest
.murphy.yml
### NetBeans ###
/nbproject/private/
@@ -34,4 +36,4 @@ build/
/.mvn/wrapper/
/mvnw
/mvnw.cmd
/.script/
/result/

2
.package/script/log.sh Normal file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
tail -fn100 ~/.zfile-v4/logs/zfile.log

View 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
View 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
View 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
View 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 --------------"

Binary file not shown.

View 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
)

21
Dockerfile Normal file
View 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

View File

@@ -1,22 +1,17 @@
<p align="center">
# ZFile
![zfile](https://cdn.jun6.net/uPic/2022/09/04/zfile-header.png)
[![ZFile License](https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square)](https://github.com/zfile-dev/zfile/blob/main/LICENSE)
[![GitHub release](https://shields.io/github/v/release/zhaojun1998/zfile?style=flat-square)](https://github.com/zfile-dev/zfile/releases)
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a"/>
[![Docker Pulls](https://img.shields.io/docker/pulls/zhaojun1998/zfile)](https://hub.docker.com/r/zhaojun1998/zfile)
[![宝塔服务器面板,一键全能部署及管理](https://img.shields.io/badge/BT_Deploy-Install-20a53a)](https://www.bt.cn/u/WYVNdM)
基于 Java 的在线网盘程序,支持对接 S3、OneDrive、SharePoint、又拍云、本地存储、FTP、SFTP 等存储源支持在线浏览图片、播放音视频文本文件、Office、obj3d等文件类型。
<br><br>
<img src="https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square" alt="license">
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a" alt="codady">
<img src="https://img.shields.io/github/last-commit/zhaojun1998/zfile.svg?style=flat-square" alt="last commit">
<img src="https://img.shields.io/github/downloads/zhaojun1998/zfile/total?style=flat-square" alt="downloads">
<img src="https://img.shields.io/github/v/release/zhaojun1998/zfile?style=flat-square" alt="release">
<img src="https://img.shields.io/github/commit-activity/y/zhaojun1998/zfile?style=flat-square" alt="commit activity">
<br>
<img src="https://img.shields.io/github/issues/zhaojun1998/zfile?style=flat-square" alt="issues">
<img src="https://img.shields.io/github/issues-closed-raw/zhaojun1998/zfile?style=flat-square" alt="closed issues">
<img src="https://img.shields.io/github/forks/zhaojun1998/zfile?style=flat-square" alt="forks">
<img src="https://img.shields.io/github/stars/zhaojun1998/zfile?style=flat-square" alt="stars">
<img src="https://img.shields.io/github/watchers/zhaojun1998/zfile?style=flat-square" alt="watchers">
</p>
## ZFile 是什么?
ZFile 是一个适用于个人的在线网盘(列目录)程序,可以将你各个存储类型的存储源,统一到一个网页中查看、预览、维护,再也不用去登录各种各样的网页登录后管理文件,现在你只需要在 ZFile 中使用。你只需要填写存储源相关信息,其他的令牌刷新,授权都是尽量自动化的,且有完善的文档帮助你使用。
- 支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源
- 支持在线浏览图片、播放音视频文本文件、Office、obj3d等文件类型。
## 快速开始
@@ -74,4 +69,4 @@
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=zfile-dev/zfile&type=Date)](https://star-history.com/#zfile-dev/zfile&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=zfile-dev/zfile&type=Date)](https://star-history.com/#zfile-dev/zfile&Date)

278
pom.xml
View File

@@ -2,27 +2,60 @@
<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.6.8</version>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>im.zhaojun</groupId>
<artifactId>zfile</artifactId>
<version>4.1.2</version>
<name>zfile</name>
<packaging>war</packaging>
<description>一个在线的文件浏览系统</description>
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.5.1.Final</org.mapstruct.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>
<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>
<dependencies>
<!-- spring boot 官方相关 -->
<!-- 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>
@@ -31,11 +64,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</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>
@@ -49,12 +87,10 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
@@ -64,93 +100,132 @@
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>7.15.0</version>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>
<!-- 存储策略相关 SDK、 工具类-->
<!-- 存储策略相关 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>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.261</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.11.0</version>
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
<version>0.2.20</version>
</dependency>
<dependency>
<groupId>com.github.lookfirst</groupId>
<artifactId>sardine</artifactId>
<version>5.10</version>
<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-boot-starter</artifactId>
<version>1.30.0</version>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 文档相关 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
<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.3</version>
<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>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.8.0</version>
<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</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.29</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
<version>33.2.0-jre</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
@@ -164,7 +239,7 @@
</dependency>
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp-spring-boot-starter</artifactId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
@@ -175,18 +250,45 @@
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20200518</version>
<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>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>
@@ -195,21 +297,13 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
@@ -220,7 +314,7 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<version>1.18.32</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
@@ -234,30 +328,52 @@
<groupId>org.flywaydb</groupId>
<artifactId>flyway-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>
<configuration>
<jvms>
<jvm>-Djava.security.egd=file:/dev/./urandom</jvm>
<jvm>-Dfile.encoding=utf-8</jvm>
<jvm>-Djava.net.preferIPv4Stack=false</jvm>
<jvm>-Djava.net.preferIPv4Addresses=true</jvm>
<jvm>-Djava.awt.headless=true</jvm>
</jvms>
</configuration>
</plugin>
</plugins>
</build>
<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>

View File

@@ -1,173 +0,0 @@
/*
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
*
* 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
*
* http://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 com.baomidou.mybatisplus.core.handlers;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义枚举属性转换器
*
* @author hubin
* @since 2017-10-11
*/
public class MybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
private final Class<E> enumClassType;
private final Class<?> propertyType;
private final Invoker getInvoker;
public MybatisEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.enumClassType = enumClassType;
MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
String name = "value";
if (!IEnum.class.isAssignableFrom(enumClassType)) {
name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));
}
this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
this.getInvoker = metaClass.getGetInvoker(name);
}
/**
* 查找标记标记EnumValue字段
*
* @param clazz class
* @return EnumValue字段
* @since 3.3.1
*/
public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
if (clazz != null && clazz.isEnum()) {
String className = clazz.getName();
return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, key -> {
Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
return fieldOptional.map(Field::getName).orElse(null);
}));
}
return Optional.empty();
}
private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst();
}
/**
* 判断是否为MP枚举处理
*
* @param clazz class
* @return 是否为MP枚举处理
* @since 3.3.1
*/
public static boolean isMpEnums(Class<?> clazz) {
return clazz != null && clazz.isEnum() && (IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
}
@SuppressWarnings("Duplicates")
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
if (jdbcType == null) {
ps.setObject(i, this.getValue(parameter));
} else {
// see r3589
ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object value = rs.getObject(columnName);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object value = rs.getObject(columnIndex, this.propertyType);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object value = cs.getObject(columnIndex, this.propertyType);
if (null == value && cs.wasNull()) {
return null;
}
return this.valueOf(value);
}
private E valueOf(Object value) {
E[] es = this.enumClassType.getEnumConstants();
return Arrays.stream(es).filter((e) -> equalsValue(value, getValue(e))).findAny().orElse(null);
}
/**
* 值比较
*
* @param sourceValue 数据库字段值
* @param targetValue 当前枚举属性值
* @return 是否匹配
* @since 3.3.0
*/
protected boolean equalsValue(Object sourceValue, Object targetValue) {
String sValue = StringUtils.toStringTrim(sourceValue);
String tValue = StringUtils.toStringTrim(targetValue);
if (sourceValue instanceof Number && targetValue instanceof Number
&& new BigDecimal(sValue).compareTo(new BigDecimal(tValue)) == 0) {
return true;
}
return Objects.equals(sValue, tValue);
}
private Object getValue(Object object) {
try {
return this.getInvoker.invoke(object, new Object[0]);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.mpe(e);
}
}
}

View 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();
}

View File

@@ -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 {
}

View File

@@ -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);
}
}
}
}

View File

@@ -1,5 +1,6 @@
package im.zhaojun.zfile.core;
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;
@@ -15,6 +16,8 @@ import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* Controller 切面, 用于处理返回值统一封装.
*
* @author zhaojun
*/
@ControllerAdvice
@@ -57,10 +60,9 @@ public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object>
// Get return body
Object returnBody = bodyContainer.getValue();
if (returnBody instanceof AjaxJson) {
// If the return body is instance of BaseResponse, then just do nothing
AjaxJson<?> baseResponse = (AjaxJson<?>) returnBody;
baseResponse.setTraceId(MDC.get("traceId"));
if (returnBody instanceof AjaxJson<?> baseResponse) {
// MDC 中的 TraceId 设置到返回值中
baseResponse.setTraceId(MDC.get(MdcConstant.TRACE_ID));
}
}

View File

@@ -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);
}
}
}

View File

@@ -1 +0,0 @@
package im.zhaojun.zfile.core.config;

View File

@@ -1 +0,0 @@
package im.zhaojun.zfile.core.config;

View File

@@ -1,60 +0,0 @@
package im.zhaojun.zfile.core.config;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* mybatis-plus 配置类
*
* @author zhaojun
*/
@Configuration
public class MyBatisPlusConfig {
@Resource
private DataSource dataSource;
@Value("${spring.datasource.driver-class-name}")
private String datasourceDriveClassName;
@Value("${spring.datasource.url}")
private String datasourceUrl;
/**
* 如果是 sqlite 数据库,自动创建数据库文件所在目录
*/
@PostConstruct
public void init() {
if (StrUtil.equals(datasourceDriveClassName, "org.sqlite.JDBC")) {
String path = datasourceUrl.replace("jdbc:sqlite:", "");
String folderPath = FileUtil.getParent(path, 1);
if (!FileUtil.exist(folderPath)) {
FileUtil.mkdir(folderPath);
}
}
}
/**
* mybatis plus 分页插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() throws SQLException {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName();
DbType dbType = DbType.getDbType(databaseProductName);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType));
return interceptor;
}
}

View File

@@ -1,79 +0,0 @@
package im.zhaojun.zfile.core.config;
import im.zhaojun.zfile.module.storage.constant.StorageConfigConstant;
import im.zhaojun.zfile.module.storage.model.entity.StorageSourceConfig;
import im.zhaojun.zfile.module.storage.service.StorageSourceConfigService;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
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.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
/**
* restTemplate 相关配置
*
* @author zhaojun
*/
@Configuration
public class RestTemplateConfig {
@Resource
private StorageSourceConfigService storageSourceConfigService;
/**
* OneDrive 请求 RestTemplate.
* 获取 header 中的 storageId 来判断到底是哪个存储源 ID, 在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证.
*/
@Bean
public RestTemplate oneDriveRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory();
restTemplate.setRequestFactory(factory);
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, clientHttpRequestExecution) -> {
HttpHeaders headers = httpRequest.getHeaders();
Integer storageId = Integer.valueOf(((List)headers.get("storageId")).get(0).toString());
StorageSourceConfig accessTokenConfig =
storageSourceConfigService.findByStorageIdAndName(storageId, 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;
}
/**
* restTemplate 设置请求和响应字符集都为 UTF-8, 并设置响应头为 Content-Type: application/text;
*/
@Bean
public RestTemplate restTemplate(){
HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
HttpClient httpClient = HttpClientBuilder.create().build();
httpRequestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
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;
}
}

View File

@@ -1 +0,0 @@
package im.zhaojun.zfile.core.config;

View File

@@ -6,6 +6,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.stereotype.Component;
/**
* ZFile 配置类,将配置文件中的 zfile 配置项映射到该类中.
*
* @author zhaojun
*/
@Data
@@ -16,4 +18,20 @@ 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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")));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,7 @@
package im.zhaojun.zfile.core.config.mybatis;
import java.util.Set;
public class CollectionIntegerTypeHandler extends CollectionTypeHandler<Set<Integer>> {
}

View File

@@ -0,0 +1,7 @@
package im.zhaojun.zfile.core.config.mybatis;
import java.util.Set;
public class CollectionStrTypeHandler extends CollectionTypeHandler<Set<String>> {
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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>&lt;if test="_databaseId = 'mysql'"&gt; </code> 来判断数据库类型. <br>
* 也可以在外层使用,如 <code>&lt;delete id="xxx" databaseId="sqlite"&gt;</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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -1,4 +1,4 @@
package im.zhaojun.zfile.core.config;
package im.zhaojun.zfile.core.config.spring;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
@@ -15,18 +15,17 @@ import java.lang.reflect.Method;
import java.util.Objects;
/**
* Jackson 枚举反序列化器
* Jackson 枚举反序列化器, 用于将接收请求中的参数(一般为字符串)转换为枚举对象.
*
* @author zhaojun
*/
@Slf4j
@Setter
@Slf4j
@JsonComponent
public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {
private Class<?> clazz;
/**
* 反序列化操作
*

View File

@@ -1,8 +1,13 @@
package im.zhaojun.zfile.core.config;
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;
@@ -15,13 +20,17 @@ import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class SpringCacheConfig {
@Value("${zfile.dbCache.enable:true}")
private Boolean dbCacheEnable;
/**
* 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager使其支持事务 putevictclear 操作延迟到事务成功提交再执行.
*/
@Bean
@ConditionalOnMissingBean(SaTokenDaoRedisJackson.class)
public CacheManager cacheManager() {
return new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
return BooleanUtils.isNotTrue(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
}
}

View File

@@ -1,13 +1,14 @@
package im.zhaojun.zfile.core.config;
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 javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -70,7 +71,7 @@ public class StringToEnumConverterFactory implements ConverterFactory<String, En
// 获取
T t = enumMap.get(source);
if (t == null) {
throw new IllegalArgumentException("该字符串找不到对应的枚举对象 字符串:" + source);
throw new SystemException("该字符串找不到对应的枚举对象 字符串:" + source);
}
return t;
}
@@ -84,7 +85,7 @@ public class StringToEnumConverterFactory implements ConverterFactory<String, En
try {
method = enumType.getMethod("getValue");
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(String.format("类:%s 找不到 getValue方法",
throw new SystemException(String.format("类:%s 找不到 getValue方法",
enumType.getName()));
}
} else {

View File

@@ -1,20 +1,13 @@
package im.zhaojun.zfile.core.config;
package im.zhaojun.zfile.core.config.spring;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.HashSet;
import java.util.Set;
/**
* ZFile Web 相关配置.
*
@@ -23,17 +16,6 @@ import java.util.Set;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 添加自定义枚举格式化器.
* @see StorageTypeEnum
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToEnumConverterFactory());
}
/**
* 支持 url 中传入 <>[\]^`{|} 这些特殊字符.
*/
@@ -49,20 +31,13 @@ public class WebMvcConfig implements WebMvcConfigurer {
return webServerFactory;
}
@Bean
public WebServerFactoryCustomizer<ConfigurableWebServerFactory> webServerFactoryCustomizer(){
return factory -> {
ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");
ErrorPage error200Page = new ErrorPage(HttpStatus.OK, "/index.html");
Set<ErrorPage> errorPages = new HashSet<>();
errorPages.add(error404Page);
errorPages.add(error200Page);
factory.setErrorPages(errorPages);
};
/**
* 添加自定义枚举格式化器.
* @see StorageTypeEnum
*/
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToEnumConverterFactory());
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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";
}

View File

@@ -12,10 +12,6 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class ZFileConstant {
public static final Character PATH_SEPARATOR_CHAR = '/';
public static final String PATH_SEPARATOR = "/";
/**
* 最大支持文本文件大小为 ? KB 的文件内容.
*/

View File

@@ -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";
}

View File

@@ -1,25 +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 = "/**/{[path:[^\\.]*}")
@RequestMapping(value = { "/"})
@ResponseBody
public String redirect() {
// Forward to home page so that route is preserved.
return "forward:/";
// 读取 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();
}
}

View File

@@ -3,9 +3,10 @@ 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.annotations.Api;
import io.swagger.annotations.ApiOperation;
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;
@@ -22,7 +23,7 @@ import java.util.Date;
*
* @author zhaojun
*/
@Api(tags = "日志")
@Tag(name = "日志")
@ApiSort(8)
@Slf4j
@RestController
@@ -33,7 +34,8 @@ public class LogController {
private String zfileLogPath;
@GetMapping("/log/download")
@ApiOperation(value = "下载系统日志")
@Operation(summary = "下载系统日志")
@DemoDisable
public ResponseEntity<Resource> downloadLog() {
if (log.isDebugEnabled()) {
log.debug("下载诊断日志");

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 非法使用下载链接异常.
*
* @author zhaojun
*/
public class IllegalDownloadLinkException extends ZFileRuntimeException {
public IllegalDownloadLinkException(String message) {
super(message);
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 系统初始化异常
*
* @author zhaojun
*/
public class InstallSystemException extends ZFileRuntimeException {
public InstallSystemException(String message) {
super(message);
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 无效的直链异常
*
* @author zhaojun
*/
public class InvalidShortLinkException extends ZFileRuntimeException {
public InvalidShortLinkException(String message) {
super(message);
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 登陆验证码验证异常
*
* @author zhaojun
*/
public class LoginVerifyException extends ZFileRuntimeException {
public LoginVerifyException(String message) {
super(message);
}
}

View File

@@ -1,22 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 密码校验失败异常
*
* @author zhaojun
*/
public class PasswordVerifyException extends RuntimeException {
private final Integer code;
public PasswordVerifyException(Integer code, String message) {
super(message);
this.code = code;
}
public Integer getCode() {
return code;
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 文件预览异常类
*
* @author zhaojun
*/
public class PreviewException extends ZFileRuntimeException {
public PreviewException(String message) {
super(message);
}
}

View File

@@ -1,42 +0,0 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Service 层异常
* 所有 message 均为系统日志打印输出, CodeMsg 中的消息才是返回给客户端的消息.
*
* @author zhaojun
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException {
private CodeMsg codeMsg;
public ServiceException(CodeMsg codeMsg) {
this.codeMsg = codeMsg;
}
public ServiceException(String message, CodeMsg codeMsg) {
super(message);
this.codeMsg = codeMsg;
}
public ServiceException(String message, Throwable cause, CodeMsg codeMsg) {
super(message, cause);
this.codeMsg = codeMsg;
}
public ServiceException(Throwable cause, CodeMsg codeMsg) {
super(cause);
this.codeMsg = codeMsg;
}
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, CodeMsg codeMsg) {
super(message, cause, enableSuppression, writableStackTrace);
this.codeMsg = codeMsg;
}
}

View File

@@ -1,21 +0,0 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.module.storage.model.param.IStorageParam;
import lombok.Getter;
/**
* 存储源自动设置 cors 异常
*
* @author zhaojun
*/
@Getter
public class StorageSourceAutoConfigCorsException extends RuntimeException {
private final IStorageParam iStorageParam;
public StorageSourceAutoConfigCorsException(String message, Throwable cause, IStorageParam iStorageParam) {
super(message, cause);
this.iStorageParam = iStorageParam;
}
}

View File

@@ -1,60 +0,0 @@
package im.zhaojun.zfile.core.exception;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* 存储源异常
*
* @author zhaojun
*/
@EqualsAndHashCode(callSuper = true)
@Getter
public class StorageSourceException extends ServiceException {
/**
* 是否使用异常消息进行接口返回,如果是则取异常的 message, 否则取 CodeMsg 中的 message
*/
private boolean responseExceptionMessage;
/**
* 存储源 ID
*/
private final Integer storageId;
public StorageSourceException(CodeMsg codeMsg, Integer storageId, String message) {
super(message, codeMsg);
this.storageId = storageId;
}
public StorageSourceException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(message, cause, codeMsg);
this.storageId = storageId;
}
/**
* 根据 responseExceptionMessage 判断使用异常消息进行接口返回,如果是则取异常的 message, 否则取 CodeMsg 中的 message
*
* @return 异常消息
*/
public String getResultMessage() {
return responseExceptionMessage ? super.getMessage() : super.getCodeMsg().getMsg();
}
/**
* 设置值是否使用异常消息进行接口返回
*
* @param responseExceptionMessage
* 是否使用异常消息进行接口返回
*
* @return 当前对象
*/
public StorageSourceException setResponseExceptionMessage(boolean responseExceptionMessage) {
this.responseExceptionMessage = responseExceptionMessage;
return this;
}
}

View File

@@ -1,14 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 存储源不支持代理上传异常
*
* @author zhaojun
*/
public class StorageSourceNotSupportProxyUploadException extends ZFileRuntimeException {
public StorageSourceNotSupportProxyUploadException(String message) {
super(message);
}
}

View File

@@ -1,22 +0,0 @@
package im.zhaojun.zfile.core.exception;
import lombok.Getter;
/**
* @author zhaojun
*/
@Getter
public class StorageSourceRefreshTokenException extends RuntimeException {
private final Integer storageId;
public StorageSourceRefreshTokenException(String message, Integer storageId) {
super(message);
this.storageId = storageId;
}
public StorageSourceRefreshTokenException(String message, Throwable cause, Integer storageId) {
super(message, cause);
this.storageId = storageId;
}
}

View File

@@ -1,17 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* 文件解析异常
*
* @author zhaojun
*/
public class TextParseException extends ZFileRuntimeException {
public TextParseException(String message) {
super(message);
}
public TextParseException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,15 +0,0 @@
package im.zhaojun.zfile.core.exception;
/**
* @author zhaojun
*/
public class ZFileRuntimeException extends RuntimeException {
public ZFileRuntimeException(String message) {
super(message);
}
public ZFileRuntimeException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -1,25 +0,0 @@
package im.zhaojun.zfile.core.exception.file;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 无效的存储源异常
*
* @author zhaojun
*/
public class InvalidStorageSourceException extends StorageSourceException {
public InvalidStorageSourceException(String message) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, null, message);
}
public InvalidStorageSourceException(Integer storageId) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, storageId, CodeMsg.STORAGE_SOURCE_NOT_FOUND.getMsg());
}
public InvalidStorageSourceException(Integer storageId, String message) {
super(CodeMsg.STORAGE_SOURCE_NOT_FOUND, storageId, message);
}
}

View File

@@ -1,21 +0,0 @@
package im.zhaojun.zfile.core.exception.file.init;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 存储源初始化异常
*
* @author zhaojun
*/
public class InitializeStorageSourceException extends StorageSourceException {
public InitializeStorageSourceException(CodeMsg codeMsg, Integer storageId, String message) {
super(codeMsg, storageId, message);
}
public InitializeStorageSourceException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

View File

@@ -1,24 +0,0 @@
package im.zhaojun.zfile.core.exception.file.operator;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
/**
* 禁止服务器代理下载异常
*
* @author zhaojun
*/
public class DisableProxyDownloadException extends StorageSourceException {
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId) {
super(codeMsg, storageId, null);
}
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId, String message) {
super(codeMsg, storageId, message);
}
public DisableProxyDownloadException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

View File

@@ -1,18 +0,0 @@
package im.zhaojun.zfile.core.exception.file.operator;
import im.zhaojun.zfile.core.exception.StorageSourceException;
import im.zhaojun.zfile.core.util.CodeMsg;
import lombok.Getter;
/**
* 存储源文件操作异常
* @author zhaojun
*/
@Getter
public class StorageSourceFileOperatorException extends StorageSourceException {
public StorageSourceFileOperatorException(CodeMsg codeMsg, Integer storageId, String message, Throwable cause) {
super(codeMsg, storageId, message, cause);
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
package im.zhaojun.zfile.core.exception.http;
/**
* Http 请求状态码异常 (返回状态码为 5xx 抛出此异常)
* @author zhaojun
*/
public class HttpResponseStatusErrorException extends RuntimeException {
public HttpResponseStatusErrorException(String message) {
super(message);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -1,18 +1,20 @@
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 javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
@@ -20,16 +22,24 @@ import java.io.IOException;
* @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, "Origin, X-Requested-With, Content-Type, Accept, zfile-token, axios-request");
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");

View File

@@ -1,22 +1,20 @@
package im.zhaojun.zfile.core.filter;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.extra.servlet.ServletUtil;
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 javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* MDC 过滤器, 用于写入 TraceId, 请求 IP, 用户名等信息到日志中.
*
* @author zhaojun
*/
@WebFilter(urlPatterns = "/*")
@@ -28,8 +26,8 @@ public class MDCFilter implements Filter {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
MDC.put(MdcConstant.TRACE_ID, IdUtil.fastUUID());
MDC.put(MdcConstant.IP, ServletUtil.getClientIP(httpServletRequest));
MDC.put(MdcConstant.USER, StpUtil.isLogin() ? StpUtil.getLoginIdAsString() : "anonymous");
MDC.put(MdcConstant.IP, JakartaServletUtil.getClientIP(httpServletRequest));
MDC.put(MdcConstant.USER, ZFileAuthUtil.getCurrentUserId().toString());
try {
filterChain.doFilter(httpServletRequest, httpServletResponse);

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -1,24 +1,26 @@
package im.zhaojun.zfile.core.model.request;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 通用分页请求对象,可继承该类增加业务字段.
*
* @author zhaojun
*/
@Data
public class PageQueryRequest {
@ApiModelProperty(value="分页页数")
@Schema(name="分页页数")
private Integer page = 1;
@ApiModelProperty(value="每页条数")
@Schema(name="每页条数")
private Integer limit = 10;
@ApiModelProperty(value="排序字段")
@Schema(name="排序字段")
private String orderBy = "create_date";
@ApiModelProperty(value="排序顺序")
@Schema(name="排序顺序")
private String orderDirection = "desc";
}

View File

@@ -1 +1,107 @@
package im.zhaojun.zfile.core.util;
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);
}
}

View 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);
}
}

View File

@@ -0,0 +1,10 @@
package im.zhaojun.zfile.core.util;
public interface CharPool {
/**
* CHAR 常量:斜杠 {@code '/'} ASCII 47
*/
char SLASH_CHAR = '/';
}

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