chore: import upstream snapshot with attribution
@@ -0,0 +1,13 @@
|
||||
name: 'Blank Issue'
|
||||
description: 请使用 https://issue.zfile.vip 创建新的问题.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**注意:**
|
||||
不要通过此页面创建问题, 请使用 https://issue.zfile.vip 创建新的问题.
|
||||
如果不是通过此链接创建的问题, 将会被直接关闭.
|
||||
- type: textarea
|
||||
id: add-a-description
|
||||
attributes:
|
||||
label: Add a description
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 创建 Issue
|
||||
url: https://issue.zfile.vip/
|
||||
about: 未通过 https://issue.zfile.vip/ 创建的问题可能会被立即关闭。
|
||||
@@ -0,0 +1,39 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
.fastRequest
|
||||
.murphy.yml
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/.mvn/wrapper/
|
||||
/mvnw
|
||||
/mvnw.cmd
|
||||
/result/
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
tail -fn100 ~/.zfile-v4/logs/zfile.log
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
$DIR/stop.sh
|
||||
$DIR/start.sh
|
||||
@@ -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}"
|
||||
@@ -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 --------------"
|
||||
@@ -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 --------------"
|
||||
@@ -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
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
# 此文件仅作为示例使用,与 ZFile 实际打包的 Dockerfile 不同(采用 Graal Native 打包,这部分不开源)
|
||||
FROM maven:3.9.9-eclipse-temurin-21-alpine AS builder
|
||||
|
||||
WORKDIR /root
|
||||
|
||||
ADD ./pom.xml pom.xml
|
||||
ADD ./src src
|
||||
|
||||
RUN mvn clean package -Dmaven.test.skip=true
|
||||
|
||||
FROM ibm-semeru-runtimes:open-21-jre-jammy
|
||||
|
||||
WORKDIR /root
|
||||
EXPOSE 8080
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' >/etc/timezone
|
||||
|
||||
RUN apt update -y && apt install --no-install-recommends fontconfig -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY --from=builder /root/target/*.jar /root/app.jar
|
||||
|
||||
CMD ["java", "-jar", "app.jar"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 ZhaoJun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,91 @@
|
||||
<div align="center">
|
||||
<a href="https://zfile.vip" target="_blank" rel="noopener noreferrer">
|
||||
<img style="margin: auto; width: 100px; display: block" src="/img/logo-zfile.png" alt="ZFile" />
|
||||
</a>
|
||||
<p>ZFile 是一个适用于个人或小团队的在线网盘程序,可以将多种存储类型统一管理,再也不用登录各种网站管理文件,现在你只需要在 ZFile 中畅快使用!</p>
|
||||
<div>
|
||||
<img alt="last commit" src="https://shields.io/github/last-commit/zfile-dev/zfile.svg?style=flat-square"/>
|
||||
<img alt="downloads" src="https://shields.io/github/downloads/zfile-dev/zfile/total?style=flat-square"/>
|
||||
<img alt="release version" src="https://shields.io/github/v/release/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="commit activity" src="https://shields.io/github/commit-activity/y/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="open issues" src="https://shields.io/github/issues/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="closed issues" src="https://shields.io/github/issues-closed-raw/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="forks" src="https://shields.io/github/forks/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="stars" src="https://shields.io/github/stars/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="watchers" src="https://shields.io/github/watchers/zfile-dev/zfile?style=flat-square"/>
|
||||
<img alt="gitcode" src="https://gitcode.com/zfile-dev/zfile/star/badge.svg"/>
|
||||
</div>
|
||||
<span>
|
||||
<a href="https://zfile.vip">官网</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.zfile.vip">文档</a>
|
||||
<span> | </span>
|
||||
<a href="https://demo.zfile.vip">预览地址</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
## 系统特色
|
||||
|
||||
- Docker、Docker Compose 支持(amd64, arm64)。
|
||||
- 支持对文件生成直链、短链(可设过期时间)。
|
||||
- 响应式设计,支持手机、平板、电脑等多种设备访问。
|
||||
- 支持多用户功能,可分配给指定用户指定存储源或目录。
|
||||
- 支持在线浏览图片、播放音视频,文本文件、Office、Obj(3d)等文件类型。
|
||||
- 支持对接 S3、OneDrive、SharePoint、Google Drive、多吉云、又拍云、本地存储、FTP、SFTP 等存储源。
|
||||
- 支持常用快捷键,`Ctrl + A` 全选,`Ctrl + 左键` 多选,`Shift + 左键` 范围选择,`Esc` 取消全选等。
|
||||
- 支持限速下载(捐赠版)
|
||||
- 支持限制指定用户可查看、上传的文件类型(捐赠版)
|
||||
|
||||
## 快速开始
|
||||
|
||||
一键脚本安装:
|
||||
|
||||
```bash
|
||||
curl -sSL https://docs.zfile.vip/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
更多安装方式请参考 [安装文档](https://docs.zfile.vip/install/)
|
||||
|
||||
|
||||
## 功能预览
|
||||
|
||||
### 文件列表
|
||||

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

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

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

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

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

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

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

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

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

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

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

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

|
||||
### 后台设置-用户管理
|
||||

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

|
||||
|
||||
## 支持作者
|
||||
|
||||
如果本项目对你有帮助,请作者喝杯咖啡吧。
|
||||
|
||||
<img src="https://cdn.jun6.net/2021/03/27/152704e91f13d.png" width="400" alt="赞助我">
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#zfile-dev/zfile&Date)
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`zfile-dev/zfile`
|
||||
- 原始仓库:https://github.com/zfile-dev/zfile
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 790 KiB |
|
After Width: | Height: | Size: 483 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,329 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>im.zhaojun</groupId>
|
||||
<artifactId>zfile</artifactId>
|
||||
<version>4.5.0</version>
|
||||
<name>zfile</name>
|
||||
<packaging>jar</packaging>
|
||||
<description>一个在线的文件浏览系统</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<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 官方相关-->
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据库相关 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
<artifactId>sqlite-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
<version>${flyway.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-mysql</artifactId>
|
||||
<version>${flyway.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.6</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 存储策略相关 API, 对象存储、FTP、 Rest API-->
|
||||
<dependency>
|
||||
<groupId>com.upyun</groupId>
|
||||
<artifactId>java-sdk</artifactId>
|
||||
<version>4.2.3</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/s3 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.qiniu</groupId>
|
||||
<artifactId>qiniu-java-sdk</artifactId>
|
||||
<version>7.12.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.mwiede</groupId>
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.2.20</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.lookfirst</groupId>
|
||||
<artifactId>sardine</artifactId>
|
||||
<version>5.12</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 登陆/权限相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>1.38.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- 文档相关 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>4.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.31</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.4.0</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>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.29</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>33.3.0-jre</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-chain</groupId>
|
||||
<artifactId>commons-chain</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.samstevens.totp</groupId>
|
||||
<artifactId>totp</artifactId>
|
||||
<version>1.7.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>javase</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20231013</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpmime</artifactId>
|
||||
<version>4.5.13</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk15on</artifactId>
|
||||
<version>1.70</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
|
||||
<dependency>
|
||||
<groupId>commons-fileupload</groupId>
|
||||
<artifactId>commons-fileupload</artifactId>
|
||||
<version>1.6.0</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>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${org.mapstruct.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.32</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,23 @@
|
||||
package im.zhaojun.zfile;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.web.servlet.ServletComponentScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true)
|
||||
@ServletComponentScan(basePackages = {"im.zhaojun.zfile.core.filter", "im.zhaojun.zfile.module.storage.filter"})
|
||||
@ComponentScan(basePackages = "im.zhaojun.zfile.*")
|
||||
public class ZfileApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ZfileApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package im.zhaojun.zfile.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口限流注解
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiLimit {
|
||||
|
||||
/**
|
||||
* 持续时间
|
||||
*/
|
||||
int timeout();
|
||||
|
||||
/**
|
||||
* 时间单位, 默认为秒
|
||||
*/
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 单位时间内允许访问的最大次数
|
||||
*/
|
||||
long maxCount();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package im.zhaojun.zfile.core.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 演示系统禁用功能注解
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DemoDisable {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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.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_";
|
||||
|
||||
/**
|
||||
* 在标记了 {@link ApiLimit} 注解的方法执行前进行限流校验.
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@Before("@annotation(apiLimit)")
|
||||
public void before(JoinPoint joinPoint, ApiLimit apiLimit) {
|
||||
// 获取当前请求的方法上的注解中设置的值
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
// 反射获取当前被调用的方法
|
||||
Method method = signature.getMethod();
|
||||
int timeout = apiLimit.timeout();
|
||||
TimeUnit timeUnit = apiLimit.timeUnit();
|
||||
long millis = timeUnit.toMillis(timeout);
|
||||
long maxCount = apiLimit.maxCount();
|
||||
|
||||
// 获取请求相关信息
|
||||
String ip = JakartaServletUtil.getClientIP(RequestHolder.getRequest());
|
||||
|
||||
// 限制访问次数
|
||||
String key = API_LIMIT_KEY_PREFIX.concat(ip).concat(method.getName());
|
||||
AtomicLong atomicLong = apiLimitTimedCache.get(key, false);
|
||||
if (atomicLong == null) {
|
||||
apiLimitTimedCache.put(key, new AtomicLong(1), millis);
|
||||
} else {
|
||||
if (atomicLong.incrementAndGet() > maxCount) {
|
||||
throw new BizException(ErrorCode.BIZ_ACCESS_TOO_FREQUENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package im.zhaojun.zfile.core.aspect;
|
||||
|
||||
import im.zhaojun.zfile.core.constant.MdcConstant;
|
||||
import im.zhaojun.zfile.core.util.AjaxJson;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
/**
|
||||
* Controller 切面, 用于处理返回值统一封装.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@ControllerAdvice
|
||||
public class CommonResultControllerAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||
return AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public final Object beforeBodyWrite(@Nullable Object body,
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull MediaType contentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType,
|
||||
@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response) {
|
||||
MappingJacksonValue container = getOrCreateContainer(body);
|
||||
// The contain body will never be null
|
||||
beforeBodyWriteInternal(container, contentType, returnType, request, response);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the body in a {@link MappingJacksonValue} value container (for providing
|
||||
* additional serialization instructions) or simply cast it if already wrapped.
|
||||
*/
|
||||
private MappingJacksonValue getOrCreateContainer(Object body) {
|
||||
return body instanceof MappingJacksonValue ? (MappingJacksonValue) body :
|
||||
new MappingJacksonValue(body);
|
||||
}
|
||||
|
||||
private void beforeBodyWriteInternal(MappingJacksonValue bodyContainer,
|
||||
MediaType contentType,
|
||||
MethodParameter returnType,
|
||||
ServerHttpRequest request,
|
||||
ServerHttpResponse response) {
|
||||
// Get return body
|
||||
Object returnBody = bodyContainer.getValue();
|
||||
|
||||
if (returnBody instanceof AjaxJson<?> baseResponse) {
|
||||
// 将 MDC 中的 TraceId 设置到返回值中
|
||||
baseResponse.setTraceId(MDC.get(MdcConstant.TRACE_ID));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package im.zhaojun.zfile.core.aspect;
|
||||
|
||||
import im.zhaojun.zfile.core.annotation.DemoDisable;
|
||||
import im.zhaojun.zfile.core.config.ZFileProperties;
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 通过注解 {@link DemoDisable} 限制演示系统不可操作的功能.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class DemoDisableAspect {
|
||||
|
||||
@Resource
|
||||
private ZFileProperties zFileProperties;
|
||||
|
||||
/**
|
||||
* 定义一个切点(通过注解)
|
||||
*/
|
||||
@Pointcut("@annotation(im.zhaojun.zfile.core.annotation.DemoDisable)")
|
||||
public void demoDisable() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在标记了 {@link DemoDisable} 注解的方法执行前进行限流校验.
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@Before("demoDisable()")
|
||||
public void before(JoinPoint joinPoint) {
|
||||
if (zFileProperties.isDemoSite()) {
|
||||
throw new BizException(ErrorCode.DEMO_SITE_DISABLE_OPERATOR);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package im.zhaojun.zfile.core.cache;
|
||||
|
||||
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* ZFile 业务缓存,针对无法使用 jsr-107 的缓存进行处理的业务逻辑。
|
||||
*/
|
||||
@Component
|
||||
public class ZFileCacheManager {
|
||||
|
||||
/**
|
||||
* 用户可用的存储源列表缓存
|
||||
*/
|
||||
private static final Map<Integer, List<StorageSource>> userEnableStorageSourceCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 根据用户 ID 获取可用的存储源列表,若缓存中不存在,则通过 mappingFunction 获取并返回。
|
||||
*
|
||||
* @param userId
|
||||
* 用户 ID
|
||||
*
|
||||
* @param mappingFunction
|
||||
* 当缓存中不存在时,用于获取存储源列表的函数。
|
||||
*
|
||||
* @return 存储源列表函数
|
||||
*/
|
||||
public List<StorageSource> findAllEnableOrderByOrderNum(Integer userId, Function<Integer, List<StorageSource>> mappingFunction) {
|
||||
return userEnableStorageSourceCache.computeIfAbsent(userId, mappingFunction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有用户的存储源缓存。
|
||||
*/
|
||||
public void clearUserEnableStorageSourceCache() {
|
||||
userEnableStorageSourceCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定用户的存储源缓存。
|
||||
*
|
||||
* @param userId
|
||||
* 用户 ID
|
||||
*/
|
||||
public void clearUserEnableStorageSourceCache(Integer userId) {
|
||||
userEnableStorageSourceCache.remove(userId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package im.zhaojun.zfile.core.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* ZFile 配置类,将配置文件中的 zfile 配置项映射到该类中.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@EnableConfigurationProperties
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "zfile")
|
||||
public class ZFileProperties {
|
||||
|
||||
private boolean debug;
|
||||
|
||||
private String version;
|
||||
|
||||
private boolean isDemoSite;
|
||||
|
||||
private OAuth2Properties onedrive = new OAuth2Properties();
|
||||
private OAuth2Properties onedriveChina = new OAuth2Properties();
|
||||
private OAuth2Properties gd = new OAuth2Properties();
|
||||
private Open115Properties open115 = new Open115Properties();
|
||||
|
||||
@Data
|
||||
public static class OAuth2Properties {
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private String redirectUri;
|
||||
private String scope;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Open115Properties {
|
||||
private String appId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.db.version";
|
||||
|
||||
public static final String DRIVE_CLASS_NAME_PROPERTIES = "spring.datasource.driver-class-name";
|
||||
|
||||
public static final String DATA_SOURCE_BEAN_NAME = "dataSource";
|
||||
|
||||
public static final String SQLITE_DRIVE_CLASS_NAME = "org.sqlite.JDBC";
|
||||
|
||||
public static final String MYSQL_DRIVE_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
// 如果更改了数据源类型这里要修改
|
||||
if (bean instanceof HikariDataSource dataSource && DATA_SOURCE_BEAN_NAME.equals(beanName)) {
|
||||
processSqliteDataSource(dataSource);
|
||||
} else if (bean instanceof FlywayProperties flywayProperties) {
|
||||
processFlywayLocations(flywayProperties);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果是 sqlite 数据库, 提前创建数据库文件所在目录. <br/>
|
||||
*
|
||||
* 如果检测到版本更新, 自动备份原数据库文件.
|
||||
*
|
||||
* @param dataSource
|
||||
* 数据源
|
||||
*/
|
||||
private void processSqliteDataSource(HikariDataSource dataSource) {
|
||||
String driverClassName = dataSource.getDriverClassName();
|
||||
String jdbcUrl = dataSource.getJdbcUrl();
|
||||
if (StringUtils.equals(driverClassName, SQLITE_DRIVE_CLASS_NAME)) {
|
||||
String path = jdbcUrl.replace("jdbc:sqlite:", "");
|
||||
String folderPath = FileUtil.getAbsolutePath(new File(path).getParentFile());
|
||||
log.info("SQLite 数据库文件所在目录: [{}]", folderPath);
|
||||
File file = new File(folderPath);
|
||||
if (!file.exists()) {
|
||||
log.info("检测到 SQLite 数据库文件所在目录不存在, 已自动创建.");
|
||||
if (!file.mkdirs()) {
|
||||
log.error("SQLite 数据库文件创建失败.");
|
||||
}
|
||||
} else {
|
||||
log.info("检测到 SQLite 数据库文件所在目录已存在, 无需自动创建.");
|
||||
|
||||
// 更新版本时, 先自动备份数据库文件
|
||||
String version = SpringUtil.getProperty(ZFILE_VERSION_PROPERTIES);
|
||||
if (StringUtils.isNotEmpty(version)) {
|
||||
String backupPath = folderPath + "/zfile-update-" + version + "-backup.db";
|
||||
if (!FileUtil.exist(path)) {
|
||||
log.error("检测到 SQLite 数据库文件不存在, 一般为初始化状态,无需备份.");
|
||||
return;
|
||||
}
|
||||
if (FileUtil.exist(backupPath)) {
|
||||
log.info("检测到 SQLite 数据库备份文件 [{}] 已存在, 无需再次备份.", backupPath);
|
||||
} else {
|
||||
FileUtil.copy(path, backupPath, false);
|
||||
log.info("自动备份 SQLite 数据库文件到: [{}]", backupPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据使用的不同数据库, 配置使用不同的 migration location
|
||||
*
|
||||
* @param flywayProperties
|
||||
* flyway 配置项
|
||||
*/
|
||||
private void processFlywayLocations(FlywayProperties flywayProperties) {
|
||||
String driveClassName = SpringUtil.getProperty(DRIVE_CLASS_NAME_PROPERTIES);
|
||||
if (SQLITE_DRIVE_CLASS_NAME.equals(driveClassName)) {
|
||||
flywayProperties.setLocations(List.of("classpath:db/migration-sqlite"));
|
||||
} else if (MYSQL_DRIVE_CLASS_NAME.equals(driveClassName)) {
|
||||
flywayProperties.setLocations(List.of("classpath:db/migration-mysql"));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package im.zhaojun.zfile.core.config.docs;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.StringSchema;
|
||||
import io.swagger.v3.oas.models.parameters.HeaderParameter;
|
||||
import org.springdoc.core.customizers.OperationCustomizer;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j 参数配置,区分前台功能和管理员功能,并为管理员接口增加统一 token header 配置.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfiguration {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi groupedOpenApi() {
|
||||
String groupName = "前台功能";
|
||||
return GroupedOpenApi.builder()
|
||||
.group(groupName)
|
||||
.packagesToScan("im.zhaojun.zfile.module")
|
||||
.pathsToExclude("/admin/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi groupedOpenApi2() {
|
||||
String groupName = "管理员功能";
|
||||
return GroupedOpenApi.builder()
|
||||
.group(groupName)
|
||||
.packagesToScan("im.zhaojun.zfile.module")
|
||||
.pathsToMatch("/admin/**")
|
||||
.addOperationCustomizer(globalOperationCustomizer())
|
||||
.build();
|
||||
}
|
||||
|
||||
public OperationCustomizer globalOperationCustomizer() {
|
||||
return (operation, handlerMethod) -> {
|
||||
operation.addParametersItem(new HeaderParameter()
|
||||
.name("zfile-token")
|
||||
.description("token")
|
||||
.required(true)
|
||||
.schema(new StringSchema()));
|
||||
return operation;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
Contact contact = new Contact();
|
||||
contact.setName("zhaojun");
|
||||
contact.setUrl("https://zfile.vip");
|
||||
contact.setEmail("873019219@qq.com");
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("ZFILE 文档")
|
||||
.description("# 这是 ZFILE Restful 接口文档展示页面")
|
||||
.termsOfService("https://www.zfile.vip")
|
||||
.contact(contact)
|
||||
.version("1.0")
|
||||
.license(new License()
|
||||
.name("Apache 2.0")
|
||||
.url("http://doc.xiaominfo.com")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package im.zhaojun.zfile.core.config.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JSON String 反序列化器, 用于将 JSON 字符串反序列化为 JSON 对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class JSONStringDeserializer extends JsonDeserializer<String> {
|
||||
|
||||
@Override
|
||||
public String deserialize(JsonParser p, DeserializationContext context) throws IOException {
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
return mapper.writeValueAsString(node);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package im.zhaojun.zfile.core.config.jackson;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
/**
|
||||
* JSON String 序列化器, 用于将 JSON 字符串序列化为 JSON 对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class JSONStringSerializer extends JsonSerializer<String> {
|
||||
|
||||
@Override
|
||||
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeRawValue(value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class CollectionIntegerTypeHandler extends CollectionTypeHandler<Set<Integer>> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class CollectionStrTypeHandler extends CollectionTypeHandler<Set<String>> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.apache.ibatis.type.BaseTypeHandler;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
import org.apache.ibatis.type.MappedJdbcTypes;
|
||||
import org.apache.ibatis.type.MappedTypes;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.sql.CallableStatement;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 自定义 Set 类型处理器, 用于处理数据库 VARCHAR 类型字段和 Java Set 类型属性之间的转换.
|
||||
* 支持字符串格式为: "[a, b, c]".
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@MappedJdbcTypes(JdbcType.VARCHAR)
|
||||
public abstract class CollectionTypeHandler<T> extends BaseTypeHandler<Object> {
|
||||
|
||||
@Override
|
||||
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
|
||||
throws SQLException {
|
||||
if (parameter instanceof Collection collection) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
for (Object o : collection) {
|
||||
joiner.add(Convert.toStr(o));
|
||||
}
|
||||
ps.setString(i, joiner.toString());
|
||||
} else {
|
||||
ps.setString(i, Convert.toStr(parameter));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet rs, String columnName)
|
||||
throws SQLException {
|
||||
String str = rs.getString(columnName);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(ResultSet rs, int columnIndex)
|
||||
throws SQLException {
|
||||
String str = rs.getString(columnIndex);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getNullableResult(CallableStatement cs, int columnIndex)
|
||||
throws SQLException {
|
||||
String str = cs.getString(columnIndex);
|
||||
return convertToEntityAttribute(str);
|
||||
}
|
||||
|
||||
private Class<?> collectionClazz;
|
||||
|
||||
private Type innerType;
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*/
|
||||
public CollectionTypeHandler() {
|
||||
ResolvableType resolvableType = ResolvableType.forClass(getClass());
|
||||
Type type = resolvableType.as(CollectionTypeHandler.class).getGeneric().getType();
|
||||
|
||||
if (type instanceof ParameterizedType parameterizedType) {
|
||||
collectionClazz = (Class<?>) parameterizedType.getRawType();
|
||||
// 获取实际类型参数(泛型参数,例如 List<String> 中的 String)
|
||||
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
|
||||
|
||||
// 使用这些信息做进一步操作
|
||||
for (Type actualTypeArgument : actualTypeArguments) {
|
||||
innerType = actualTypeArgument;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertToEntityAttribute(String dbData) {
|
||||
if (StrUtil.isEmpty(dbData)) {
|
||||
if (List.class.isAssignableFrom(collectionClazz)) {
|
||||
return Collections.emptyList();
|
||||
} else if (Set.class.isAssignableFrom(collectionClazz)) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Collection collection;
|
||||
|
||||
if (List.class.isAssignableFrom(collectionClazz)) {
|
||||
collection = new ArrayList<>();
|
||||
} else if (Set.class.isAssignableFrom(collectionClazz)) {
|
||||
collection = new HashSet<>();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] split = dbData.split(",");
|
||||
for (String s : split) {
|
||||
if (NumberUtil.isNumber(s)) {
|
||||
collection.add(Convert.convert(Integer.class, s));
|
||||
} else {
|
||||
collection.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisPlusConfig {
|
||||
|
||||
/**
|
||||
* mybatis plus 分页插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor(DataSource dataSource) throws SQLException {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName();
|
||||
DbType dbType = DbType.getDbType(databaseProductName);
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType));
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* MyBatis 数据库 ID Provider, 用于判断当前数据库类型来执行不同的 SQL 语句. <br>
|
||||
* 可在 xml 中使用 <code><if test="_databaseId = 'mysql'"> </code> 来判断数据库类型. <br>
|
||||
* 也可以在外层使用,如 <code><delete id="xxx" databaseId="sqlite"></code> 来判断数据库类型.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class MyDatabaseIdProvider implements DatabaseIdProvider {
|
||||
|
||||
private static final String DATABASE_MYSQL = "MySQL";
|
||||
private static final String DATABASE_SQLITE = "SQLite";
|
||||
|
||||
@Override
|
||||
public String getDatabaseId(DataSource dataSource) throws SQLException {
|
||||
Connection conn = dataSource.getConnection();
|
||||
String dbName = conn.getMetaData().getDatabaseProductName();
|
||||
String dbAlias = "";
|
||||
switch (dbName) {
|
||||
case DATABASE_MYSQL:
|
||||
dbAlias = "mysql";
|
||||
break;
|
||||
case DATABASE_SQLITE:
|
||||
dbAlias = "sqlite";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return dbAlias;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package im.zhaojun.zfile.core.config.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 自动填充配置类
|
||||
* 用于自动填充 createTime 和 updateTime 字段
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
/**
|
||||
* Jackson 定制版 SaSession,忽略 timeout 等属性的序列化
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.34.0
|
||||
*/
|
||||
@JsonIgnoreProperties({"timeout"})
|
||||
public class SaSessionForJacksonCustomized extends SaSession {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final long serialVersionUID = -7600983549653130681L;
|
||||
|
||||
public SaSessionForJacksonCustomized() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建一个Session对象
|
||||
* @param id Session的id
|
||||
*/
|
||||
public SaSessionForJacksonCustomized(String id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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 cn.dev33.satoken.strategy.SaStrategy;
|
||||
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");
|
||||
|
||||
// 不再依赖 SaToken 的默认路径检查功能
|
||||
SaStrategy.instance.checkRequestPath = (path, extArg1, extArg2) -> {};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.dao.SaTokenDao;
|
||||
import cn.dev33.satoken.strategy.SaStrategy;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Sa-Token 持久层实现 [ Redis存储、Jackson序列化 ]
|
||||
*
|
||||
* @author click33
|
||||
* @since 1.34.0
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "spring.data.redis.host")
|
||||
public class SaTokenDaoRedisJackson implements SaTokenDao {
|
||||
|
||||
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
public static final String DATE_PATTERN = "yyyy-MM-dd";
|
||||
public static final String TIME_PATTERN = "HH:mm:ss";
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
|
||||
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
|
||||
|
||||
/**
|
||||
* ObjectMapper 对象 (以 public 作用域暴露出此对象,方便开发者二次更改配置)
|
||||
*
|
||||
* <p> 例如:
|
||||
* <pre>
|
||||
* SaTokenDaoRedisJackson redisJackson = (SaTokenDaoRedisJackson) SaManager.getSaTokenDao();
|
||||
* redisJackson.objectMapper.xxx = xxx;
|
||||
* </pre>
|
||||
* </p>
|
||||
*/
|
||||
public ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* String 读写专用
|
||||
*/
|
||||
public StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
/**
|
||||
* Object 读写专用
|
||||
*/
|
||||
public RedisTemplate<String, Object> objectRedisTemplate;
|
||||
|
||||
/**
|
||||
* 标记:是否已初始化成功
|
||||
*/
|
||||
public boolean isInit;
|
||||
|
||||
@Autowired
|
||||
public void init(RedisConnectionFactory connectionFactory) {
|
||||
// 如果已经初始化成功了,就立刻退出,不重复初始化
|
||||
if(this.isInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 指定相应的序列化方案
|
||||
StringRedisSerializer keySerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
|
||||
// 通过反射获取Mapper对象, 增加一些配置, 增强兼容性
|
||||
try {
|
||||
Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
|
||||
field.setAccessible(true);
|
||||
this.objectMapper = (ObjectMapper) field.get(valueSerializer);
|
||||
|
||||
// 配置[忽略未知字段]
|
||||
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
// 配置[时间类型转换]
|
||||
JavaTimeModule timeModule = new JavaTimeModule();
|
||||
|
||||
// LocalDateTime序列化与反序列化
|
||||
timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
|
||||
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
|
||||
|
||||
// LocalDate序列化与反序列化
|
||||
timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
|
||||
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
|
||||
|
||||
// LocalTime序列化与反序列化
|
||||
timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
|
||||
timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
|
||||
|
||||
this.objectMapper.registerModule(timeModule);
|
||||
|
||||
// 重写 SaSession 生成策略
|
||||
SaStrategy.instance.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId);
|
||||
} catch (Exception e) {
|
||||
System.err.println(e.getMessage());
|
||||
}
|
||||
// 构建StringRedisTemplate
|
||||
StringRedisTemplate stringTemplate = new StringRedisTemplate();
|
||||
stringTemplate.setConnectionFactory(connectionFactory);
|
||||
stringTemplate.afterPropertiesSet();
|
||||
|
||||
// 构建RedisTemplate
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
template.setKeySerializer(keySerializer);
|
||||
template.setHashKeySerializer(keySerializer);
|
||||
template.setValueSerializer(valueSerializer);
|
||||
template.setHashValueSerializer(valueSerializer);
|
||||
template.afterPropertiesSet();
|
||||
|
||||
// 开始初始化相关组件
|
||||
this.stringRedisTemplate = stringTemplate;
|
||||
this.objectRedisTemplate = template;
|
||||
|
||||
// 打上标记,表示已经初始化成功,后续无需再重新初始化
|
||||
this.isInit = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Value,如无返空
|
||||
*/
|
||||
@Override
|
||||
public String get(String key) {
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Value,并设定存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void set(String key, String value, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
// 判断是否为永不过期
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
stringRedisTemplate.opsForValue().set(key, value);
|
||||
} else {
|
||||
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修修改指定key-value键值对 (过期时间不变)
|
||||
*/
|
||||
@Override
|
||||
public void update(String key, String value) {
|
||||
long expire = getTimeout(key);
|
||||
// -2 = 无此键
|
||||
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
this.set(key, value, expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Value
|
||||
*/
|
||||
@Override
|
||||
public void delete(String key) {
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Value的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public long getTimeout(String key) {
|
||||
return stringRedisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改Value的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void updateTimeout(String key, long timeout) {
|
||||
// 判断是否想要设置为永久
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
long expire = getTimeout(key);
|
||||
if(expire == SaTokenDao.NEVER_EXPIRE) {
|
||||
// 如果其已经被设置为永久,则不作任何处理
|
||||
} else {
|
||||
// 如果尚未被设置为永久,那么再次set一次
|
||||
this.set(key, this.get(key), timeout);
|
||||
}
|
||||
return;
|
||||
}
|
||||
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取Object,如无返空
|
||||
*/
|
||||
@Override
|
||||
public Object getObject(String key) {
|
||||
return objectRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入Object,并设定存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void setObject(String key, Object object, long timeout) {
|
||||
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
// 判断是否为永不过期
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
objectRedisTemplate.opsForValue().set(key, object);
|
||||
} else {
|
||||
objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Object (过期时间不变)
|
||||
*/
|
||||
@Override
|
||||
public void updateObject(String key, Object object) {
|
||||
long expire = getObjectTimeout(key);
|
||||
// -2 = 无此键
|
||||
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
|
||||
return;
|
||||
}
|
||||
this.setObject(key, object, expire);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Object
|
||||
*/
|
||||
@Override
|
||||
public void deleteObject(String key) {
|
||||
objectRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Object的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public long getObjectTimeout(String key) {
|
||||
return objectRedisTemplate.getExpire(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改Object的剩余存活时间 (单位: 秒)
|
||||
*/
|
||||
@Override
|
||||
public void updateObjectTimeout(String key, long timeout) {
|
||||
// 判断是否想要设置为永久
|
||||
if(timeout == SaTokenDao.NEVER_EXPIRE) {
|
||||
long expire = getObjectTimeout(key);
|
||||
if(expire == SaTokenDao.NEVER_EXPIRE) {
|
||||
// 如果其已经被设置为永久,则不作任何处理
|
||||
} else {
|
||||
// 如果尚未被设置为永久,那么再次set一次
|
||||
this.setObject(key, this.getObject(key), timeout);
|
||||
}
|
||||
return;
|
||||
}
|
||||
objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 搜索数据
|
||||
*/
|
||||
@Override
|
||||
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
|
||||
Set<String> keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*");
|
||||
List<String> list = new ArrayList<>(keys);
|
||||
return SaFoxUtil.searchList(list, start, size, sortType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package im.zhaojun.zfile.core.config.security;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import im.zhaojun.zfile.module.user.service.UserService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义权限加载接口实现类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Component
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
private static final List<String> ADMIN_ROLE_LIST = Collections.singletonList("admin");
|
||||
|
||||
public static final List<String> EMPTY_ROLE_LIST = Collections.emptyList();
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合,这里没用到这个功能,所以返回空集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
boolean isAdmin = userService.isAdmin(Convert.toInt(loginId));
|
||||
return isAdmin ? ADMIN_ROLE_LIST : EMPTY_ROLE_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.BeanProperty;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.jackson.JsonComponent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Jackson 枚举反序列化器, 用于将接收请求中的参数(一般为字符串)转换为枚举对象.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Setter
|
||||
@Slf4j
|
||||
@JsonComponent
|
||||
public class JacksonEnumDeserializer extends JsonDeserializer<Enum<?>> implements ContextualDeserializer {
|
||||
|
||||
private Class<?> clazz;
|
||||
|
||||
/**
|
||||
* 反序列化操作
|
||||
*
|
||||
* @param jsonParser
|
||||
* json 解析器
|
||||
*
|
||||
* @param ctx
|
||||
* 反序列化上下文
|
||||
*
|
||||
* @return 反序列化后的枚举值
|
||||
* @throws IOException 反序列化异常
|
||||
*/
|
||||
@Override
|
||||
public Enum<?> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
|
||||
Class<?> enumType = clazz;
|
||||
if (Objects.isNull(enumType) || !enumType.isEnum()) {
|
||||
return null;
|
||||
}
|
||||
String text = jsonParser.getText();
|
||||
Method method = StringToEnumConverterFactory.getMethod(clazz);
|
||||
Enum<?>[] enumConstants = (Enum<?>[]) enumType.getEnumConstants();
|
||||
|
||||
// 将值与枚举对象对应并缓存
|
||||
for (Enum<?> e : enumConstants) {
|
||||
try {
|
||||
if (Objects.equals(method.invoke(e).toString(), text)) {
|
||||
return e;
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
log.error("获取枚举值错误!!! ", ex);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 为不同的枚举获取合适的解析器
|
||||
*
|
||||
* @param ctx
|
||||
* 反序列化上下文
|
||||
*
|
||||
* @param property
|
||||
* property
|
||||
*/
|
||||
@Override
|
||||
public JsonDeserializer<Enum<?>> createContextual(DeserializationContext ctx, BeanProperty property) {
|
||||
Class<?> rawCls = ctx.getContextualType().getRawClass();
|
||||
JacksonEnumDeserializer converter = new JacksonEnumDeserializer();
|
||||
converter.setClazz(rawCls);
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import im.zhaojun.zfile.core.config.security.SaTokenDaoRedisJackson;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
|
||||
import org.springframework.cache.support.NoOpCacheManager;
|
||||
import org.springframework.cache.transaction.TransactionAwareCacheManagerProxy;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Spring Cache 相关配置
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class SpringCacheConfig {
|
||||
|
||||
@Value("${zfile.dbCache.enable:true}")
|
||||
private Boolean dbCacheEnable;
|
||||
|
||||
/**
|
||||
* 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager,使其支持事务 (将 put、evict、clear 操作延迟到事务成功提交再执行.)
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SaTokenDaoRedisJackson.class)
|
||||
public CacheManager cacheManager() {
|
||||
return BooleanUtils.isNotTrue(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package im.zhaojun.zfile.core.config.spring;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* String 转枚举通用转换器工厂
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum<?>> {
|
||||
|
||||
/**
|
||||
* 存储枚举类型的缓存
|
||||
*/
|
||||
private static final Map<Class<?>, Converter<String, ? extends Enum<?>>> CONVERTER_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 枚举类的获取枚举值方法缓存
|
||||
*/
|
||||
private static final Map<Class<?>, Method> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked cast")
|
||||
public <T extends Enum<?>> Converter<String, T> getConverter(Class<T> targetType) {
|
||||
// 缓存转换器
|
||||
Converter<String, T> converter = (Converter<String, T>) CONVERTER_MAP.get(targetType);
|
||||
if (converter == null) {
|
||||
converter = new StringToEnumConverter<>(targetType);
|
||||
CONVERTER_MAP.put(targetType, converter);
|
||||
}
|
||||
return converter;
|
||||
}
|
||||
|
||||
static class StringToEnumConverter<T extends Enum<?>> implements Converter<String, T> {
|
||||
|
||||
private final Map<String, T> enumMap = new ConcurrentHashMap<>();
|
||||
|
||||
StringToEnumConverter(Class<T> enumType) {
|
||||
Method method = getMethod(enumType);
|
||||
T[] enums = enumType.getEnumConstants();
|
||||
|
||||
// 将值与枚举对象对应并缓存
|
||||
for (T e : enums) {
|
||||
try {
|
||||
enumMap.put(method.invoke(e).toString(), e);
|
||||
} catch (IllegalAccessException | InvocationTargetException ex) {
|
||||
log.error("获取枚举值错误!!! ", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public T convert(@NotNull String source) {
|
||||
// 获取
|
||||
T t = enumMap.get(source);
|
||||
if (t == null) {
|
||||
throw new SystemException("该字符串找不到对应的枚举对象 字符串:" + source);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static <T> Method getMethod(Class<T> enumType) {
|
||||
Method method;
|
||||
// 找到取值的方法
|
||||
if (IEnum.class.isAssignableFrom(enumType)) {
|
||||
try {
|
||||
method = enumType.getMethod("getValue");
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new SystemException(String.format("类:%s 找不到 getValue方法",
|
||||
enumType.getName()));
|
||||
}
|
||||
} else {
|
||||
method = TABLE_METHOD_OF_ENUM_TYPES.computeIfAbsent(enumType, k -> {
|
||||
Field field =
|
||||
dealEnumType(enumType).orElseThrow(() -> new IllegalArgumentException(String.format(
|
||||
"类:%s 找不到 EnumValue注解", enumType.getName())));
|
||||
|
||||
Class<?> fieldType = field.getType();
|
||||
String fieldName = field.getName();
|
||||
String methodName = StringUtils.concatCapitalize(boolean.class.equals(fieldType) ? "is" : "get", fieldName);
|
||||
try {
|
||||
return enumType.getDeclaredMethod(methodName);
|
||||
} catch (NoSuchMethodException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
private static Optional<Field> dealEnumType(Class<?> clazz) {
|
||||
return clazz.isEnum() ?
|
||||
Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst() : Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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.servlet.server.ServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* ZFile Web 相关配置.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
/**
|
||||
* 支持 url 中传入 <>[\]^`{|} 这些特殊字符.
|
||||
*/
|
||||
@Bean
|
||||
public ServletWebServerFactory webServerFactory() {
|
||||
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory();
|
||||
|
||||
// 添加对 URL 中特殊符号的支持.
|
||||
webServerFactory.addConnectorCustomizers(connector -> {
|
||||
connector.setProperty("relaxedPathChars", "<>[\\]^`{|}%[]");
|
||||
connector.setProperty("relaxedQueryChars", "<>[\\]^`{|}%[]");
|
||||
});
|
||||
return webServerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义枚举格式化器.
|
||||
* @see StorageTypeEnum
|
||||
*/
|
||||
@Override
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
registry.addConverterFactory(new StringToEnumConverterFactory());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
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.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 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;
|
||||
}
|
||||
|
||||
private int getCodeLength() {
|
||||
return this.props.getCode().getLength();
|
||||
}
|
||||
|
||||
private int getTimePeriod() {
|
||||
return this.props.getTime().getPeriod();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package im.zhaojun.zfile.core.config.totp;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(
|
||||
prefix = "totp"
|
||||
)
|
||||
public class TotpProperties {
|
||||
private static final int DEFAULT_SECRET_LENGTH = 32;
|
||||
private static final int DEFAULT_CODE_LENGTH = 6;
|
||||
private static final int DEFAULT_TIME_PERIOD = 30;
|
||||
private static final int DEFAULT_TIME_DISCREPANCY = 1;
|
||||
private final Secret secret = new Secret();
|
||||
private final Code code = new Code();
|
||||
private final Time time = new Time();
|
||||
|
||||
public TotpProperties() {
|
||||
}
|
||||
|
||||
public Secret getSecret() {
|
||||
return this.secret;
|
||||
}
|
||||
|
||||
public Code getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public Time getTime() {
|
||||
return this.time;
|
||||
}
|
||||
|
||||
public static class Time {
|
||||
private int period = 30;
|
||||
private int discrepancy = 1;
|
||||
|
||||
public Time() {
|
||||
}
|
||||
|
||||
public int getPeriod() {
|
||||
return this.period;
|
||||
}
|
||||
|
||||
public void setPeriod(int period) {
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public int getDiscrepancy() {
|
||||
return this.discrepancy;
|
||||
}
|
||||
|
||||
public void setDiscrepancy(int discrepancy) {
|
||||
this.discrepancy = discrepancy;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Code {
|
||||
private int length = 6;
|
||||
|
||||
public Code() {
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Secret {
|
||||
private int length = 32;
|
||||
|
||||
public Secret() {
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* Slf4j mdc 常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class MdcConstant {
|
||||
|
||||
public static final String TRACE_ID = "traceId";
|
||||
|
||||
public static final String IP = "ip";
|
||||
|
||||
public static final String USER = "user";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* 规则表达式类型常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class RuleTypeConstant {
|
||||
|
||||
public static final String IP = "ip";
|
||||
|
||||
public static final String REGEX = "regex";
|
||||
|
||||
public static final String ANT_PATH = "antPath";
|
||||
|
||||
public static final String SPRING_SIMPLE = "springSimple";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* ZFile 常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Configuration
|
||||
public class ZFileConstant {
|
||||
|
||||
/**
|
||||
* 最大支持文本文件大小为 ? KB 的文件内容.
|
||||
*/
|
||||
public static Long TEXT_MAX_FILE_SIZE_KB = 100L;
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setTextMaxFileSizeMb(@Value("${zfile.preview.text.maxFileSizeKb}") Long maxFileSizeKb) {
|
||||
ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package im.zhaojun.zfile.core.constant;
|
||||
|
||||
/**
|
||||
* ZFile 自定义 HTTP 请求头常量
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ZFileHttpHeaderConstant {
|
||||
|
||||
public static final String ZFILE_TOKEN = "Zfile-Token";
|
||||
|
||||
public static final String AXIOS_REQUEST = "Axios-Request";
|
||||
|
||||
public static final String AXIOS_FROM = "Axios-From";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 处理前端首页 Controller
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class FrontIndexController {
|
||||
|
||||
@Resource
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
@Resource
|
||||
private WebProperties webProperties;
|
||||
|
||||
/**
|
||||
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
|
||||
* 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回.
|
||||
*
|
||||
* @return 转发到 /index.html
|
||||
*/
|
||||
@RequestMapping(value = { "/"})
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> redirect() {
|
||||
// 读取 resources/static/index.html 文件修改 title 和 favicon 后返回
|
||||
ResourceLoader resourceLoader = new FileSystemResourceLoader();
|
||||
String[] staticLocations = webProperties.getResources().getStaticLocations();
|
||||
|
||||
// 如果 staticLocations 里没有包含 file:static/, 则手动添加
|
||||
boolean fileStaticExist = false;
|
||||
for (String staticLocation : staticLocations) {
|
||||
if (staticLocation.startsWith("file:")) {
|
||||
fileStaticExist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fileStaticExist) {
|
||||
staticLocations = org.apache.commons.lang3.ArrayUtils.add(staticLocations, "file:static/");
|
||||
}
|
||||
|
||||
for (String staticLocation : staticLocations) {
|
||||
org.springframework.core.io.Resource resource = resourceLoader.getResource(staticLocation + "/index.html");
|
||||
boolean exists = resource.exists();
|
||||
if (exists) {
|
||||
String content;
|
||||
try {
|
||||
content = resource.getContentAsString(StandardCharsets.UTF_8);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("读取 index.html 文件成功, 文件路径: {}", staticLocation);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("{} 资源存在但读取 index.html 文件失败.", staticLocation);
|
||||
return ResponseEntity.status(500).body("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 ResponseEntity.ok()
|
||||
.header("Cache-Control", "max-age=600, must-revalidate, proxy-revalidate") .header("Pragma", "no-cache")
|
||||
.body(content);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.status(404).body("static index.html not found");
|
||||
}
|
||||
|
||||
@RequestMapping(value = { "/guest"})
|
||||
@ResponseBody
|
||||
public String guest() {
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
return systemConfig.getGuestIndexHtml();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package im.zhaojun.zfile.core.controller;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ZipUtil;
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiSort;
|
||||
import im.zhaojun.zfile.core.annotation.DemoDisable;
|
||||
import im.zhaojun.zfile.core.util.FileResponseUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 获取系统日志接口
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Tag(name = "日志")
|
||||
@ApiSort(8)
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
public class LogController {
|
||||
|
||||
@Value("${zfile.log.path}")
|
||||
private String zfileLogPath;
|
||||
|
||||
@GetMapping("/log/download")
|
||||
@Operation(summary = "下载系统日志")
|
||||
@DemoDisable
|
||||
public ResponseEntity<Resource> downloadLog() {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("下载诊断日志");
|
||||
}
|
||||
|
||||
File fileZip = ZipUtil.zip(zfileLogPath);
|
||||
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
|
||||
return FileResponseUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
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", "不允许重命名到该名称"),
|
||||
BIZ_UNSUPPORTED_OPERATION_TYPE("41029", "不支持的操作类型"),
|
||||
BIZ_CUSTOM_SHARE_LINK_KEY_FORMAT_ILLEGAL("41030", "自定义分享 key 格式不正确,只能包含字母、数字、下划线和短横线,长度为 3-8 位"),
|
||||
BIZ_SHARE_LINK_KEY_ALREADY_EXIST("41031", "分享 key 已存在"),
|
||||
BIZ_SHARE_LINK_EXPIRY_MUST_BE_FUTURE("41032", "过期时间必须是未来的时间"),
|
||||
BIZ_SHARE_LINK_NOT_EXIST("41033", "分享链接不存在"),
|
||||
BIZ_SHARE_LINK_EXPIRED("41034", "分享链接已过期"),
|
||||
BIZ_SHARE_PASSWORD_ERROR("41036", "分享密码错误"),
|
||||
BIZ_SHARE_FILE_LIST_ERROR("41037", "获取分享文件列表失败"),
|
||||
BIZ_SHARE_FILE_DOWNLOAD_ERROR("41038", "获取文件下载地址失败"),
|
||||
BIZ_SHARE_FILE_INFO_ERROR("41039", "获取文件信息失败"),
|
||||
|
||||
// 第二位为 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", "规则已存在"),
|
||||
BIZ_SSO_PROVIDER_EXIST("43013", "单点登录配置已存在"),
|
||||
BIZ_SSO_PROVIDER_DISABLED("43014", "此单点登录未启用"),
|
||||
|
||||
|
||||
/**
|
||||
* 通用的无权限异常
|
||||
*/
|
||||
NO_FORBIDDEN("30000", "没有权限"),
|
||||
NO_CUSTOM_SHARE_LINK_KEY_PERMISSION("30001", "没有自定义分享链接 key 的权限"),
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 授权校验异常
|
||||
*/
|
||||
PRO_AUTH_CODE_EMPTY("20000", "请先去后台 \"基本设置\" 填写 \"授权码\""),
|
||||
PRO_CHECK_REFERER_EMPTY("20001", "Referer 无效,请检查服务端设置,20001"), // Referer 无效,请检查服务端设置
|
||||
PRO_CHECK_TIME_NO_SYNC("20002", "授权校验失败, 服务器时间异常,20002"), // 授权校验失败, 服务器时间异常.
|
||||
PRO_AUTH_CODE_INVALID_ERROR("20003", "授权码无效, 请检查后台 \"站点设置\" 中的 \"授权码\" 20003"),
|
||||
PRO_CHECK_UNKNOWN_ERROR("20004", "授权验证异常,未知异常,20098"),
|
||||
PRO_MSG_ERROR("20005", null);
|
||||
|
||||
private String code;
|
||||
|
||||
private String message;
|
||||
|
||||
ErrorCode(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误码
|
||||
*
|
||||
* @param code 错误码
|
||||
* @return 返回当前枚举
|
||||
*/
|
||||
public ErrorCode setCode(String code) {
|
||||
this.code = code;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置错误信息
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @return 返回当前枚举
|
||||
*/
|
||||
public ErrorCode setMessage(String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
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().getBody();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
} else if (exceptionType == ExceptionType.IGNORE_EXCEPTION) {
|
||||
// 忽略异常
|
||||
return null;
|
||||
}
|
||||
|
||||
if (e.getClass() == Exception.class) {
|
||||
return AjaxJson.getError("系统异常, 请联系管理员");
|
||||
} else {
|
||||
return AjaxJson.getError(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static ExceptionType getExceptionType(Exception e) {
|
||||
int findCauseCount = 0;
|
||||
do {
|
||||
if (e instanceof BizException) {
|
||||
return ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION;
|
||||
} else if (e instanceof ClientAbortException) {
|
||||
return ExceptionType.IGNORE_EXCEPTION;
|
||||
} else if (e instanceof SQLiteException && e.getMessage().contains("database is locked")) {
|
||||
exceptionMessage.set("数据库繁忙,请稍后再试");
|
||||
return ExceptionType.SPECIFY_MESSAGE_EXCEPTION;
|
||||
}
|
||||
e = (Exception) e.getCause();
|
||||
findCauseCount++;
|
||||
} while (e != null && findCauseCount < MAX_FIND_CAUSE_EXCEPTION_DEPTH);
|
||||
|
||||
return ExceptionType.OTHER;
|
||||
}
|
||||
|
||||
enum ExceptionType {
|
||||
/**
|
||||
* 忽略打印异常信息和堆栈信息
|
||||
*/
|
||||
IGNORE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 仅打印异常信息, 不打印堆栈信息
|
||||
*/
|
||||
IGNORE_PRINT_STACK_TRACE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 不打印堆栈信息,但指定异常信息
|
||||
*/
|
||||
SPECIFY_MESSAGE_EXCEPTION,
|
||||
|
||||
/**
|
||||
* 其他异常, 打印异常信息和堆栈信息
|
||||
*/
|
||||
OTHER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 请求第三方 API 时如果返回非 2xx 状态码, 则抛出此异常. 需记录请求地址, 响应状态码, 响应内容.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#apiHttpRequestBizException(APIHttpRequestBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class APIHttpRequestBizException extends BizException {
|
||||
|
||||
private final String url;
|
||||
|
||||
private final int responseCode;
|
||||
|
||||
private final String responseBody;
|
||||
|
||||
public APIHttpRequestBizException(ErrorCode errorCode, String url, int responseCode, String responseBody) {
|
||||
super(errorCode);
|
||||
this.url = url;
|
||||
this.responseCode = responseCode;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class CorsBizException extends BizException {
|
||||
|
||||
public CorsBizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean printExceptionStackTrace() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文件路径安全异常, 表示文件路径不合法,如包含了 "./" 或 "../" 等字符来尝试访问非法目录.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#filePathSecurityBizException(FilePathSecurityBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class FilePathSecurityBizException extends BizException {
|
||||
|
||||
private final String path;
|
||||
|
||||
public FilePathSecurityBizException(String path) {
|
||||
super(ErrorCode.BIZ_FILE_PATH_ILLEGAL);
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 获取预览文件内容异常, 可能是目标连接无法访问/文件不存在等原因.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#getPreviewTextContentBizException(GetPreviewTextContentBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class GetPreviewTextContentBizException extends BizException {
|
||||
|
||||
/**
|
||||
* 获取预览文件的 URL
|
||||
*/
|
||||
private final String url;
|
||||
|
||||
public GetPreviewTextContentBizException(String url, Throwable cause) {
|
||||
super(cause);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 初始化存储源时失败产生的异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#initializeStorageSourceBizException(InitializeStorageSourceBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class InitializeStorageSourceBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
public InitializeStorageSourceBizException(String message, Integer storageId) {
|
||||
super(message);
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
public InitializeStorageSourceBizException(String code, String message, Integer storageId, Throwable cause) {
|
||||
super(code, message, cause);
|
||||
this.storageId = storageId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 不存在或初始化失败的存储源异常。
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class InvalidStorageSourceBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final String storageKey;
|
||||
|
||||
public InvalidStorageSourceBizException(String storageKey) {
|
||||
super(ErrorCode.INVALID_STORAGE_SOURCE);
|
||||
this.storageKey = storageKey;
|
||||
this.storageId = null;
|
||||
}
|
||||
|
||||
public InvalidStorageSourceBizException(Integer storageId) {
|
||||
super(ErrorCode.INVALID_STORAGE_SOURCE);
|
||||
this.storageId = storageId;
|
||||
this.storageKey = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 访问了禁止访问的存储源文件/目录时抛出此异常.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class StorageSourceFileForbiddenAccessBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final String path;
|
||||
|
||||
public StorageSourceFileForbiddenAccessBizException(Integer storageId, String path) {
|
||||
super(ErrorCode.BIZ_STORAGE_SOURCE_FILE_FORBIDDEN);
|
||||
this.storageId = storageId;
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package im.zhaojun.zfile.core.exception.biz;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 对存储源进行非法(未授权)的操作产生的异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class StorageSourceIllegalOperationBizException extends BizException {
|
||||
|
||||
private final Integer storageId;
|
||||
|
||||
private final FileOperatorTypeEnum action;
|
||||
|
||||
public StorageSourceIllegalOperationBizException(Integer storageId, FileOperatorTypeEnum action) {
|
||||
super(ErrorCode.BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION);
|
||||
this.storageId = storageId;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
|
||||
*/
|
||||
public boolean printExceptionStackTrace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public BizException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public BizException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public BizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public BizException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public BizException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public BizException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.<br>
|
||||
* 使用该类的异常,当该异常被抛出时,会跳转到 500 错误页面(错误码和错误消息可被 {@link #code} 和 {@link #getMessage()} 覆盖),而不是返回 JSON 数据.<br>
|
||||
* 一般使用该异常得请求不会是 AJAX 请求,而是直接在浏览器中访问的页面请求.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class ErrorPageBizException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
|
||||
*/
|
||||
public boolean printExceptionStackTrace() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public ErrorPageBizException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public ErrorPageBizException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public ErrorPageBizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public ErrorPageBizException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public ErrorPageBizException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public ErrorPageBizException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package im.zhaojun.zfile.core.exception.core;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 系统异常, 该类异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 服务器异常等.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Getter
|
||||
public class SystemException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8312907182931723379L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 构造一个没有错误信息的 <code>SystemException</code>
|
||||
*/
|
||||
public SystemException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
|
||||
*
|
||||
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
|
||||
*/
|
||||
public SystemException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息 message 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public SystemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误码和错误信息构造 SystemException
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public SystemException(String code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用错误信息和 Throwable 构造 SystemException
|
||||
*
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param code 错误码
|
||||
* @param message 错误信息
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(String code, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
*/
|
||||
public SystemException(ErrorCode errorCode) {
|
||||
super(errorCode.getMessage());
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode ErrorCode
|
||||
* @param cause 错误原因
|
||||
*/
|
||||
public SystemException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode.getMessage(), cause);
|
||||
this.code = errorCode.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 错误请求异常, 表示请求参数有误或者服务器无法理解, 一般返回 400 状态码
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#badRequestAccessException(BadRequestAccessException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class BadRequestAccessException extends BizException {
|
||||
|
||||
public BadRequestAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 禁止访问异常, 表示用户没有权限访问该资源, 一般返回 403 状态码. (已经有身份,如果没有身份,应该是 UnauthorizedAccessException)
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#forbiddenAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ForbiddenAccessException extends BizException {
|
||||
|
||||
public ForbiddenAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 错误请求异常, 表示请求方法有误或者服务器无法理解, 一般返回 405 状态码
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#methodNotAllowedAccessException(MethodNotAllowedAccessException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class MethodNotAllowedAccessException extends BizException {
|
||||
|
||||
public MethodNotAllowedAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 访问内容不存在异常, 表示用户请求的资源不存在时抛出, 一般返回 404 状态码.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#notFoundAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class NotFoundAccessException extends BizException {
|
||||
|
||||
public NotFoundAccessException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package im.zhaojun.zfile.core.exception.status;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.core.BizException;
|
||||
|
||||
/**
|
||||
* 禁止访问异常, 表示用户未进行身份认证, 一般返回 401 状态码.
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#unauthorizedAccessException}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class UnauthorizedAccessException extends BizException {
|
||||
|
||||
public UnauthorizedAccessException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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 Long inputStreamAvailable;
|
||||
|
||||
private final int responseCode;
|
||||
|
||||
private final String responseBody;
|
||||
|
||||
public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Long inputStreamAvailable, int responseCode, String responseBody) {
|
||||
this(storageTypeEnum, uploadPath, inputStreamAvailable, responseCode, responseBody, null);
|
||||
}
|
||||
|
||||
public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Long inputStreamAvailable, int responseCode, String responseBody, Throwable cause) {
|
||||
super(ErrorCode.BIZ_UPLOAD_FILE_ERROR, cause);
|
||||
this.storageTypeEnum = storageTypeEnum;
|
||||
this.uploadPath = uploadPath;
|
||||
this.inputStreamAvailable = inputStreamAvailable;
|
||||
this.responseCode = responseCode;
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package im.zhaojun.zfile.core.exception.system;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
|
||||
import im.zhaojun.zfile.core.exception.core.SystemException;
|
||||
|
||||
/**
|
||||
* ZFile 授权异常
|
||||
* <p/>
|
||||
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#zfileAuthorizationSystemException(ZFileAuthorizationSystemException)}
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ZFileAuthorizationSystemException extends SystemException {
|
||||
|
||||
public ZFileAuthorizationSystemException(String code, String message) {
|
||||
super(code, message);
|
||||
}
|
||||
|
||||
public ZFileAuthorizationSystemException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
public ZFileAuthorizationSystemException(ErrorCode errorCode, Throwable cause) {
|
||||
super(errorCode, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import im.zhaojun.zfile.core.constant.ZFileHttpHeaderConstant;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.cors.CorsUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
@Order(Integer.MIN_VALUE)
|
||||
@Component
|
||||
public class CorsFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
if (httpServletRequest.getRequestURI().equals("/favicon.ico")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String header = httpServletRequest.getHeader(HttpHeaders.ORIGIN);
|
||||
|
||||
List<String> allowHeaders = Arrays.asList("Origin", "X-Requested-With", "Content-Type", "Accept", ZFileHttpHeaderConstant.ZFILE_TOKEN, ZFileHttpHeaderConstant.AXIOS_REQUEST, ZFileHttpHeaderConstant.AXIOS_FROM);
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ObjectUtil.defaultIfNull(header, "*"));
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.join(",", allowHeaders));
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false");
|
||||
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "600");
|
||||
|
||||
if (!CorsUtils.isPreFlightRequest(httpServletRequest)) {
|
||||
chain.doFilter(httpServletRequest, httpServletResponse);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import im.zhaojun.zfile.core.constant.MdcConstant;
|
||||
import im.zhaojun.zfile.core.util.ZFileAuthUtil;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* MDC 过滤器, 用于写入 TraceId, 请求 IP, 用户名等信息到日志中.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
public class MDCFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
MDC.put(MdcConstant.TRACE_ID, IdUtil.fastUUID());
|
||||
MDC.put(MdcConstant.IP, JakartaServletUtil.getClientIP(httpServletRequest));
|
||||
MDC.put(MdcConstant.USER, ZFileAuthUtil.getCurrentUserId().toString());
|
||||
|
||||
try {
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse);
|
||||
} finally {
|
||||
MDC.clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package im.zhaojun.zfile.core.filter;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
|
||||
import im.zhaojun.zfile.core.util.StringUtils;
|
||||
import im.zhaojun.zfile.core.util.matcher.IRuleMatcher;
|
||||
import im.zhaojun.zfile.core.util.matcher.RuleMatcherFactory;
|
||||
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.module.config.service.SystemConfigService;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检测访问的 IP 和 UA 是否符合系统安全设置中的规则
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@WebFilter(urlPatterns = "/*")
|
||||
public class SecurityFilter implements Filter {
|
||||
|
||||
private static volatile SystemConfigService systemConfigService;
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||
|
||||
// 双重检测锁, 防止多次初始化
|
||||
if (systemConfigService == null) {
|
||||
synchronized (this) {
|
||||
if (systemConfigService == null) {
|
||||
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
String accessIpBlocklist = systemConfig.getAccessIpBlocklist();
|
||||
String accessUaBlocklist = systemConfig.getAccessUaBlocklist();
|
||||
|
||||
// 判断当前访问 IP 是否在黑名单中
|
||||
String currentAccessIp = JakartaServletUtil.getClientIP(httpServletRequest);
|
||||
if (StringUtils.isNotBlank(accessIpBlocklist) && checkIsDisableIP(accessIpBlocklist, currentAccessIp)) {
|
||||
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
httpServletResponse.getWriter().write("disable access.[" + currentAccessIp + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断当前访问 User-Agent 是否在黑名单中
|
||||
String userAgent = httpServletRequest.getHeader(HttpHeaders.USER_AGENT);
|
||||
if (StringUtils.isNotBlank(accessUaBlocklist) && checkIsDisableUA(accessUaBlocklist, userAgent)) {
|
||||
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
|
||||
httpServletResponse.getWriter().write("disable access.[" + userAgent + "]");
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse);
|
||||
}
|
||||
|
||||
private boolean checkIsDisableIP(String accessIpBlocklist, String currentAccessIp) {
|
||||
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.IP);
|
||||
List<String> ruleList = StringUtils.split(accessIpBlocklist, StringUtils.LF);
|
||||
return ruleMatcher.matchAny(ruleList, currentAccessIp);
|
||||
}
|
||||
|
||||
private boolean checkIsDisableUA(String accessUaBlocklist, String currentAccessUA) {
|
||||
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.SPRING_SIMPLE);
|
||||
List<String> ruleList = StringUtils.split(accessUaBlocklist, StringUtils.LF);
|
||||
return ruleMatcher.matchAny(ruleList, currentAccessUA);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.zhaojun.zfile.core.io;
|
||||
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
*
|
||||
* 自定义 EnsureContentLengthInputStreamResource 可以保证必须实现 InputStream 的 contentLength 方法返回实际的长度.
|
||||
* 此类相较于 {@link org.springframework.core.io.InputStreamResource} 仅实现了 contentLength 方法.
|
||||
* <br><br>
|
||||
* {@link org.springframework.core.io.Resource} implementation for a given {@link InputStream}.
|
||||
* <p>Should only be used if no other specific {@code Resource} implementation
|
||||
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
|
||||
* file-based {@code Resource} implementations where possible.
|
||||
*
|
||||
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
|
||||
* for an <i>already opened</i> resource - therefore returning {@code true} from
|
||||
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
|
||||
* keep the resource descriptor somewhere, or if you need to read from a stream
|
||||
* multiple times.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 28.12.2003
|
||||
* @see ByteArrayResource
|
||||
* @see org.springframework.core.io.ClassPathResource
|
||||
* @see org.springframework.core.io.FileSystemResource
|
||||
* @see org.springframework.core.io.UrlResource
|
||||
*/
|
||||
public class EnsureContentLengthInputStreamResource extends InputStreamResource {
|
||||
|
||||
private final long contentLength;
|
||||
|
||||
/**
|
||||
* Create a new InputStreamResource.
|
||||
* @param inputStream the InputStream to use
|
||||
*/
|
||||
public EnsureContentLengthInputStreamResource(InputStream inputStream, long contentLength) {
|
||||
super(inputStream);
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package im.zhaojun.zfile.core.model.request;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 通用分页请求对象,可继承该类增加业务字段.
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
public class PageQueryRequest {
|
||||
|
||||
@Schema(title="分页页数")
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(title="每页条数")
|
||||
private Integer limit = 10;
|
||||
|
||||
@Schema(title="排序字段")
|
||||
private String orderBy = "create_date";
|
||||
|
||||
@Schema(title="排序顺序")
|
||||
private String orderDirection = "desc";
|
||||
|
||||
public OrderItem getOrderItem() {
|
||||
boolean asc = Objects.equals(orderDirection, "asc");
|
||||
return asc ? OrderItem.asc(orderBy) : OrderItem.desc(orderBy);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* ajax 请求返回 JSON 格式数据的封装
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Data
|
||||
@ToString
|
||||
public class AjaxJson<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L; // 序列化版本号
|
||||
|
||||
public static final String CODE_SUCCESS = "0"; // 成功状态码
|
||||
|
||||
@Schema(title = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
|
||||
private final String code;
|
||||
|
||||
@Schema(title = "响应消息", example = "ok")
|
||||
private String msg;
|
||||
|
||||
@Schema(title = "响应数据")
|
||||
private T data;
|
||||
|
||||
@Schema(title = "数据总条数,分页情况有效")
|
||||
private final Long dataCount;
|
||||
|
||||
@Schema(title = "跟踪 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
public interface CharPool {
|
||||
|
||||
/**
|
||||
* CHAR 常量:斜杠 {@code '/'} ASCII 47
|
||||
*/
|
||||
char SLASH_CHAR = '/';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.text.StrSplitter;
|
||||
import jakarta.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 字符串工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class CharSequenceUtil implements CharPool {
|
||||
|
||||
/**
|
||||
* 找不到索引时的返回值
|
||||
*/
|
||||
public static final int INDEX_NOT_FOUND = -1;
|
||||
|
||||
/**
|
||||
* 字符串常量:{@code "null"} <br>
|
||||
* 注意:{@code "null" != null}
|
||||
*/
|
||||
public static final String NULL = "null";
|
||||
|
||||
/**
|
||||
* 字符串常量:空字符串 {@code ""}
|
||||
*/
|
||||
public static final String EMPTY = "";
|
||||
|
||||
/**
|
||||
* 字符串常量:空格符 {@code " "}
|
||||
*/
|
||||
public static final String SPACE = " ";
|
||||
|
||||
|
||||
/**
|
||||
* 获取 CharSequence 的长度, 如果为 null, 返回 0
|
||||
*
|
||||
* @param ch
|
||||
* 要获取长度的 CharSequence, 可能为 null
|
||||
*
|
||||
* @return CharSequence 的长度
|
||||
*/
|
||||
public static int length(final @Nullable CharSequence ch) {
|
||||
return ch == null ? 0 : ch.length();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link CharSequence} 转为字符串
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static String str(final @Nullable CharSequence cs) {
|
||||
return null == cs ? null : cs.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断 CharSequence 是否为空
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static boolean isEmpty(final @Nullable CharSequence cs) {
|
||||
return cs == null || cs.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* CharSequence 是否不为空
|
||||
*
|
||||
* @param cs
|
||||
* {@link CharSequence}
|
||||
*
|
||||
* @return 是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(final @Nullable CharSequence cs) {
|
||||
return !isEmpty(cs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>指定字符串数组中的元素,是否全部为空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty() // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("", null) // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("123", "") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty("123", "abc") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllEmpty(" ", "\t", "\n") // false}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param strs
|
||||
* 字符串列表
|
||||
*
|
||||
* @return 所有字符串是否都为空
|
||||
*/
|
||||
public static boolean isAllEmpty(final @Nullable CharSequence... strs) {
|
||||
if (strs == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CharSequence str : strs) {
|
||||
if (isNotEmpty(str)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>是否包含空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty() // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("", null) // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("123", "") // true}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty("123", "abc") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.hasEmpty(" ", "\t", "\n") // false}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param strs
|
||||
* 字符串列表
|
||||
*
|
||||
* @return 是否包含空字符串
|
||||
*/
|
||||
public static boolean hasEmpty(final @Nullable CharSequence... strs) {
|
||||
if (ArrayUtils.isEmpty(strs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (CharSequence str : strs) {
|
||||
if (isEmpty(str)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>指定字符串数组中的元素,是否都不为空字符串。</p>
|
||||
* <p>如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。</p>
|
||||
* <br>
|
||||
*
|
||||
* <p>例:</p>
|
||||
* <ul>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty() // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("", null) // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "") // false}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "abc") // true}</li>
|
||||
* <li>{@code CharSequenceUtil.isAllNotEmpty(" ", "\t", "\n") // true}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param args
|
||||
* 字符串数组
|
||||
*
|
||||
* @return 所有字符串是否都不为为空白
|
||||
*/
|
||||
public static boolean isAllNotEmpty(final @Nullable CharSequence... args) {
|
||||
return !hasEmpty(args);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否为空白
|
||||
*
|
||||
* @param ch
|
||||
* 要判断的字符串, 可能为 null
|
||||
*
|
||||
* @return 是否为空白
|
||||
*/
|
||||
public static boolean isBlank(final @Nullable CharSequence ch) {
|
||||
final int strLen = ch == null ? 0 : ch.length();
|
||||
if (strLen == 0) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < strLen; i++) {
|
||||
if (!Character.isWhitespace(ch.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否不为空白
|
||||
*
|
||||
* @param cs
|
||||
* 字符串
|
||||
*
|
||||
* @return 是否不为空白
|
||||
*/
|
||||
public static boolean isNotBlank(final @Nullable CharSequence cs) {
|
||||
return !isBlank(cs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个 CharSequence 是否相等, 区分大小写, 如果两个都为 null, 返回 true
|
||||
*
|
||||
* @param cs1
|
||||
* CharSequence 1, 可能为 null
|
||||
*
|
||||
* @param cs2
|
||||
* CharSequence 2, 可能为 null
|
||||
*
|
||||
* @return 是否相等
|
||||
*/
|
||||
public static boolean equals(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
|
||||
if (cs1 == cs2) {
|
||||
return true;
|
||||
}
|
||||
if (cs1 == null || cs2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (cs1.length() != cs2.length()) {
|
||||
return false;
|
||||
}
|
||||
if (cs1 instanceof String && cs2 instanceof String) {
|
||||
return cs1.equals(cs2);
|
||||
}
|
||||
// 逐个比较
|
||||
final int length = cs1.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (cs1.charAt(i) != cs2.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个 CharSequence 是否相等, 可以选择是否忽略大小写, 如果两个都为 null, 返回 true
|
||||
*
|
||||
* @param cs1
|
||||
* 字符串 1
|
||||
*
|
||||
* @param cs2
|
||||
* 字符串 2
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否相等
|
||||
*/
|
||||
public static boolean equals(final @Nullable CharSequence cs1,final @Nullable CharSequence cs2, boolean ignoreCase) {
|
||||
return ignoreCase ? equalsIgnoreCase(cs1, cs2) : equals(cs1, cs2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 字符串是否相等, 忽略大小写
|
||||
*
|
||||
* @param cs1
|
||||
* 字符串 1
|
||||
*
|
||||
* @param cs2
|
||||
* 字符串 2
|
||||
*
|
||||
* @return 忽略大小写后是否相等
|
||||
*/
|
||||
public static boolean equalsIgnoreCase(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
|
||||
if (cs1 == cs2) {
|
||||
return true;
|
||||
}
|
||||
if (cs1 == null || cs2 == null) {
|
||||
return false;
|
||||
}
|
||||
if (cs1.length() != cs2.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return cs1.toString().equalsIgnoreCase(cs2.toString());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串,如果分隔符不存在则返回原字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static List<String> split(final CharSequence str, final CharSequence separator) {
|
||||
return split(str, separator, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符字符
|
||||
*
|
||||
* @param isTrim
|
||||
* 是否去除切分字符串后每个元素两边的空格
|
||||
*
|
||||
* @param ignoreEmpty
|
||||
* 是否忽略空串
|
||||
*
|
||||
* @return 切分后的集合
|
||||
*/
|
||||
public static List<String> split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) {
|
||||
return split(str, separator, 0, isTrim, ignoreEmpty);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切分字符串
|
||||
*
|
||||
* @param str
|
||||
* 被切分的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔符字符
|
||||
*
|
||||
* @param limit
|
||||
* 限制分片数,-1 不限制
|
||||
*
|
||||
* @param isTrim
|
||||
* 是否去除切分字符串后每个元素两边的空格
|
||||
*
|
||||
* @param ignoreEmpty
|
||||
* 是否忽略空串
|
||||
*
|
||||
* @return 切分后的集合
|
||||
*/
|
||||
public static List<String> split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) {
|
||||
final String separatorStr = (null == separator) ? null : separator.toString();
|
||||
return StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 指定字符串是否在字符串中出现过
|
||||
*
|
||||
* @param str
|
||||
* 字符串
|
||||
*
|
||||
* @param searchStr
|
||||
* 被查找的字符串
|
||||
*
|
||||
* @return 是否包含
|
||||
*/
|
||||
public static boolean contains(final @Nullable CharSequence str, final @Nullable CharSequence searchStr) {
|
||||
if (null == str || null == searchStr) {
|
||||
return false;
|
||||
}
|
||||
return str.toString().contains(searchStr);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串
|
||||
*
|
||||
* @param str
|
||||
* 指定字符串
|
||||
*
|
||||
* @param testStrs
|
||||
* 需要检查的字符串数组
|
||||
*
|
||||
* @return 是否包含任意一个字符串
|
||||
*/
|
||||
public static boolean containsAny(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
|
||||
if (isEmpty(str) || ArrayUtils.isEmpty(testStrs)) {
|
||||
return false;
|
||||
}
|
||||
for (CharSequence checkStr : testStrs) {
|
||||
if (null != checkStr && str.toString().contains(checkStr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串<br>
|
||||
* 忽略大小写
|
||||
*
|
||||
* @param str
|
||||
* 指定字符串
|
||||
*
|
||||
* @param testStrs
|
||||
* 需要检查的字符串数组
|
||||
*
|
||||
* @return 是否包含任意一个字符串
|
||||
*/
|
||||
public static boolean containsAnyIgnoreCase(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
|
||||
return StringUtils.containsAnyIgnoreCase(str, testStrs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 以 conjunction 为分隔符将多个对象转换为字符串
|
||||
*
|
||||
* @param conjunction
|
||||
* 分隔符
|
||||
*
|
||||
* @param objs
|
||||
* 数组
|
||||
*
|
||||
* @return 连接后的字符串
|
||||
*/
|
||||
public static String join(CharSequence conjunction, Object... objs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < objs.length; i++) {
|
||||
Object item = objs[i];
|
||||
sb.append(item);
|
||||
if (i < objs.length - 1) {
|
||||
sb.append(conjunction);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 以 conjunction 为分隔符将 Collection 对象转换为字符串
|
||||
*
|
||||
* @param conjunction
|
||||
* 分隔符
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 连接后的字符串
|
||||
*/
|
||||
public static String join(CharSequence conjunction, Collection<?> collection) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Object item : collection) {
|
||||
sb.append(item).append(conjunction);
|
||||
}
|
||||
if (!sb.isEmpty()) {
|
||||
sb.delete(sb.length() - conjunction.length(), sb.length());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(CharSequence str, CharSequence prefix) {
|
||||
return startWith(str, prefix, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头,忽略大小写
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) {
|
||||
return startWith(str, prefix, true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) {
|
||||
return startWith(str, prefix, ignoreCase, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串开头<br>
|
||||
* 如果给定的字符串和开头字符串都为 null 则返回 true,否则任意一个值为 null 返回 false<br>
|
||||
* <pre>
|
||||
* CharSequenceUtil.startWith("123", "123", false, true); -- false
|
||||
* CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
|
||||
* CharSequenceUtil.startWith("abc", "abc", true, true); -- false
|
||||
* </pre>
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 开头字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @param ignoreEquals
|
||||
* 是否忽略字符串相等的情况
|
||||
*
|
||||
* @return 是否以指定字符串开头
|
||||
*/
|
||||
public static boolean startWith(final @Nullable CharSequence str, final @Nullable CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) {
|
||||
if (null == str || null == prefix) {
|
||||
if (ignoreEquals) {
|
||||
return false;
|
||||
}
|
||||
return null == str && null == prefix;
|
||||
}
|
||||
|
||||
boolean isStartWith = str.toString()
|
||||
.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());
|
||||
|
||||
if (isStartWith) {
|
||||
return (!ignoreEquals) || (!equals(str, prefix, ignoreCase));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix) {
|
||||
return endWith(str, suffix, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase) {
|
||||
return endWith(str, suffix, ignoreCase, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 是否以指定字符串结尾<br>
|
||||
* 如果给定的字符串和开头字符串都为null则返回true,否则任意一个值为null返回false
|
||||
*
|
||||
* @param str
|
||||
* 被监测字符串
|
||||
*
|
||||
* @param suffix
|
||||
* 结尾字符串
|
||||
*
|
||||
* @param ignoreCase
|
||||
* 是否忽略大小写
|
||||
*
|
||||
* @param ignoreEquals
|
||||
* 是否忽略字符串相等的情况
|
||||
*
|
||||
* @return 是否以指定字符串结尾
|
||||
*/
|
||||
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) {
|
||||
if (null == str || null == suffix) {
|
||||
if (ignoreEquals) {
|
||||
return false;
|
||||
}
|
||||
return null == str && null == suffix;
|
||||
}
|
||||
|
||||
final int strOffset = str.length() - suffix.length();
|
||||
boolean isEndWith = str.toString()
|
||||
.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());
|
||||
|
||||
if (isEndWith) {
|
||||
return (!ignoreEquals) || (!equals(str, suffix, ignoreCase));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 去掉指定前缀
|
||||
*
|
||||
* @param str
|
||||
* 字符串
|
||||
*
|
||||
* @param prefix
|
||||
* 前缀
|
||||
*
|
||||
* @return 切掉后的字符串,若前缀不是 preffix, 返回原字符串
|
||||
*/
|
||||
public static String removePrefix(final @Nullable CharSequence str, final @Nullable CharSequence prefix) {
|
||||
if (isEmpty(str) || isEmpty(prefix)) {
|
||||
return str(str);
|
||||
}
|
||||
String str2 = str.toString();
|
||||
String prefix2 = prefix.toString();
|
||||
if (str2.startsWith(prefix2)) {
|
||||
return str.subSequence(prefix.length(), str.length()).toString();
|
||||
}
|
||||
return str2; // 若前缀不是 prefix,返回原字符串
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回第一个非 {@code null} 元素
|
||||
*
|
||||
* @param strs
|
||||
* 多个元素
|
||||
*
|
||||
* @param <T>
|
||||
* 元素类型
|
||||
*
|
||||
* @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null}
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends CharSequence> T firstNonNull(T... strs) {
|
||||
if (ArrayUtils.isNotEmpty(strs)) {
|
||||
for (T str : strs) {
|
||||
if (isNotEmpty(str)) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取分隔字符串之前的字符串,不包括分隔字符串<br>
|
||||
* 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串<br>
|
||||
* 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下:
|
||||
*
|
||||
* <pre>
|
||||
* CharSequenceUtil.subBefore(null, *, false) = null
|
||||
* CharSequenceUtil.subBefore("", *, false) = ""
|
||||
* CharSequenceUtil.subBefore("abc", "a", false) = ""
|
||||
* CharSequenceUtil.subBefore("abcba", "b", false) = "a"
|
||||
* CharSequenceUtil.subBefore("abc", "c", false) = "ab"
|
||||
* CharSequenceUtil.subBefore("abc", "d", false) = "abc"
|
||||
* CharSequenceUtil.subBefore("abc", "", false) = ""
|
||||
* CharSequenceUtil.subBefore("abc", null, false) = "abc"
|
||||
* </pre>
|
||||
*
|
||||
* @param string
|
||||
* 被查找的字符串
|
||||
*
|
||||
* @param separator
|
||||
* 分隔字符串(不包括)
|
||||
*
|
||||
* @param isLastSeparator
|
||||
* 是否查找最后一个分隔字符串(多次出现分隔字符串时选取最后一个),true为选取最后一个
|
||||
*
|
||||
* @return 切割后的字符串
|
||||
*/
|
||||
public static String subBefore(final @Nullable CharSequence string, final @Nullable CharSequence separator, boolean isLastSeparator) {
|
||||
if (isEmpty(string) || separator == null) {
|
||||
return null == string ? null : string.toString();
|
||||
}
|
||||
|
||||
final String str = string.toString();
|
||||
final String sep = separator.toString();
|
||||
if (sep.isEmpty()) {
|
||||
return EMPTY;
|
||||
}
|
||||
final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);
|
||||
if (INDEX_NOT_FOUND == pos) {
|
||||
return str;
|
||||
}
|
||||
if (0 == pos) {
|
||||
return EMPTY;
|
||||
}
|
||||
return str.substring(0, pos);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/**
|
||||
* Class & 反射相关工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class ClassUtils {
|
||||
|
||||
public static Class<?> forName(String className) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类的泛型类型, 只获取第一个泛型类型
|
||||
*
|
||||
* @param clazz
|
||||
* 泛型类
|
||||
*
|
||||
* @return 泛型类型
|
||||
*/
|
||||
public static Class<?> getClassFirstGenericsParam(Class<?> clazz) {
|
||||
Type genericSuperclass = clazz.getGenericSuperclass();
|
||||
Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
|
||||
return (Class<?>) actualTypeArgument;
|
||||
}
|
||||
|
||||
public static Class<?> getGenericType(Field field) {
|
||||
ParameterizedType listType = (ParameterizedType) field.getGenericType();
|
||||
return (Class<?>) listType.getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.lang.func.Func1;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
public class CollectionUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 判断集合是否为空
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 是否为空
|
||||
*/
|
||||
public static boolean isEmpty(@Nullable Collection<?> collection) {
|
||||
return (collection == null || collection.isEmpty());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断集合是否不为空
|
||||
*
|
||||
* @param collection
|
||||
* 集合
|
||||
*
|
||||
* @return 是否不为空
|
||||
*/
|
||||
public static boolean isNotEmpty(@Nullable Collection<?> collection) {
|
||||
return !isEmpty(collection);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从集合中获取第一个元素, 如果集合为空则返回 {@code null}
|
||||
*
|
||||
* @param list
|
||||
* 集合,可能为 {@code null}
|
||||
*
|
||||
* @return 第一个元素,如果集合为空则返回 {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T getFirst(@Nullable List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从集合中获取最后一个元素, 如果集合为空则返回 {@code null}
|
||||
*
|
||||
* @param list
|
||||
* 集合,可能为 {@code null}
|
||||
*
|
||||
* @return 最后一个元素,如果集合为空则返回 {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
public static <T> T getLast(@Nullable List<T> list) {
|
||||
if (isEmpty(list)) {
|
||||
return null;
|
||||
}
|
||||
return list.get(list.size() - 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加入全部
|
||||
*
|
||||
* @param <T>
|
||||
* 集合元素类型
|
||||
*
|
||||
* @param collection
|
||||
* 被加入的集合 {@link Collection}
|
||||
*
|
||||
* @param values
|
||||
* 要加入的内容数组
|
||||
*
|
||||
* @return 原集合
|
||||
*/
|
||||
public static <T> Collection<T> addAll(Collection<T> collection, T[] values) {
|
||||
if (null != collection && null != values) {
|
||||
Collections.addAll(collection, values);
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Iterable 转换为 Map, 根据指定的 keyFunc 函数生成 Key. Value 为 Iterable 中的元素.<br>
|
||||
* 可以指定将结果放入的 Map, 如不指定则会新建一个 HashMap 返回.
|
||||
*
|
||||
* @param <K>
|
||||
* Map Key 类型
|
||||
*
|
||||
* @param <V>
|
||||
* Map Value 类型
|
||||
*
|
||||
* @param values
|
||||
* 被转换的 Iterable
|
||||
*
|
||||
* @param map
|
||||
* 转换后的 Value 存放的 Map, 如果为 {@code null} 则新建一个 HashMap
|
||||
*
|
||||
* @param keyFunc
|
||||
* 生成 Map 的 Key 的函数
|
||||
*
|
||||
* @return 转换后的 Map
|
||||
*/
|
||||
public static <K, V> Map<K, V> toMap(final @Nullable Iterable<V> values, final @Nullable Map<K, V> map, final @Nullable Func1<V, K> keyFunc) {
|
||||
if (values == null || keyFunc == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
final Map<K, V> result = map == null ? new HashMap<>() : map;
|
||||
|
||||
for (V value : values) {
|
||||
try {
|
||||
result.put(keyFunc.call(value), value);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import com.alibaba.dcm.DnsCacheManipulator;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
public class DnsUtil {
|
||||
|
||||
/**
|
||||
* 通过 HTTP DNS 获取域名对应的 IP 地址
|
||||
*
|
||||
* @param domain
|
||||
* 域名
|
||||
*
|
||||
* @return IP 地址数组
|
||||
*/
|
||||
public static @Nullable String[] getDomainIpByHttpDns(String domain) {
|
||||
String jsonArrayStr = cn.hutool.http.HttpUtil.get("http://223.5.5.5/resolve?name=" + domain + "&short=1", 3000);
|
||||
JSONArray jsonArray = JSONArray.parseArray(jsonArrayStr);
|
||||
if (!jsonArray.isEmpty()) {
|
||||
String[] result = new String[jsonArray.size()];
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
result[i] = jsonArray.getString(i);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 HTTP DNS 获取域名对应的 IP 地址, 并设置 DNS 缓存.
|
||||
*
|
||||
* @param domain
|
||||
* 域名
|
||||
*
|
||||
* @param cacheTime
|
||||
* 缓存时间, 单位: 毫秒
|
||||
*
|
||||
* @return IP 地址数组
|
||||
*/
|
||||
public static String[] getDomainIpByHttpDnsAndCache(String domain, int cacheTime) {
|
||||
String[] domainIpByHttpDns = getDomainIpByHttpDns(domain);
|
||||
if (domainIpByHttpDns != null) {
|
||||
// 设置 DNS 缓存
|
||||
DnsCacheManipulator.setDnsCache(cacheTime, domain, domainIpByHttpDns);
|
||||
}
|
||||
return domainIpByHttpDns;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ClassUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 枚举转换工具类
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class EnumConvertUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 根据枚举 class 和值获取对应的枚举对象
|
||||
*
|
||||
* @param clazz
|
||||
* 枚举类 Class
|
||||
*
|
||||
* @param value
|
||||
* 枚举值
|
||||
*
|
||||
* @return 枚举对象
|
||||
*/
|
||||
public static Enum<?> convertStrToEnum(Class<?> clazz, Object value) {
|
||||
if (!ClassUtil.isEnum(clazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Field[] fields = ReflectUtil.getFields(clazz);
|
||||
for (Field field : fields) {
|
||||
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
|
||||
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
|
||||
|
||||
if (jsonValuePresent || enumValuePresent) {
|
||||
Object[] enumConstants = clazz.getEnumConstants();
|
||||
|
||||
for (Object enumObj : enumConstants) {
|
||||
if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) {
|
||||
return (Enum<?>) enumObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换枚举对象为字符串, 如果枚举对象没有定义 JsonValue 注解, 则使用 EnumValue 注解的值
|
||||
*
|
||||
* @param enumObj
|
||||
* 枚举对象
|
||||
*
|
||||
* @return 字符串
|
||||
*/
|
||||
public static String convertEnumToStr(Object enumObj) {
|
||||
Class<?> clazz = enumObj.getClass();
|
||||
if (!ClassUtil.isEnum(clazz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Field[] fields = ReflectUtil.getFields(clazz);
|
||||
for (Field field : fields) {
|
||||
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
|
||||
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
|
||||
|
||||
if (jsonValuePresent || enumValuePresent) {
|
||||
return Convert.toStr(ReflectUtil.getFieldValue(enumObj, field));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.comparator.CompareUtil;
|
||||
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
|
||||
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* 文件比较器
|
||||
* <ul>
|
||||
* <li>文件夹始终比文件排序高</li>
|
||||
* <li>默认按照名称排序</li>
|
||||
* <li>默认排序为升序</li>
|
||||
* <li>按名称排序不区分大小写</li>
|
||||
* </ul>
|
||||
* @author zhaojun
|
||||
*/
|
||||
public class FileComparator implements Comparator<FileItemResult> {
|
||||
|
||||
private String sortBy;
|
||||
|
||||
private String order;
|
||||
|
||||
public FileComparator(String sortBy, String order) {
|
||||
this.sortBy = sortBy;
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 比较两个文件的大小
|
||||
*
|
||||
* @param o1
|
||||
* 第一个文件
|
||||
*
|
||||
* @param o2
|
||||
* 第二个文件
|
||||
*
|
||||
* @return 比较结果
|
||||
*/
|
||||
@Override
|
||||
public int compare(FileItemResult o1, FileItemResult o2) {
|
||||
if (sortBy == null) {
|
||||
sortBy = "name";
|
||||
}
|
||||
|
||||
if (order == null) {
|
||||
order = "asc";
|
||||
}
|
||||
FileTypeEnum o1Type = o1.getType();
|
||||
FileTypeEnum o2Type = o2.getType();
|
||||
NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();
|
||||
if (o1Type.equals(o2Type)) {
|
||||
int result = switch (sortBy) {
|
||||
case "time" -> CompareUtil.compare(o1.getTime(), o2.getTime());
|
||||
case "size" -> CompareUtil.compare(o1.getSize(), o2.getSize());
|
||||
default -> naturalOrderComparator.compare(o1.getName(), o2.getName());
|
||||
};
|
||||
return "asc".equals(order) ? result : -result;
|
||||
}
|
||||
|
||||
if (o1Type.equals(FileTypeEnum.FOLDER)) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package im.zhaojun.zfile.core.util;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import im.zhaojun.zfile.core.exception.ErrorCode;
|
||||
import im.zhaojun.zfile.core.exception.status.NotFoundAccessException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 将文件输出对象
|
||||
*
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileResponseUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 文件下载,单线程,不支持断点续传
|
||||
*
|
||||
* @param file
|
||||
* 文件对象
|
||||
*
|
||||
* @param fileName
|
||||
* 要保存为的文件名
|
||||
*
|
||||
* @return 文件下载对象
|
||||
*/
|
||||
public static ResponseEntity<Resource> exportSingleThread(File file, String fileName) {
|
||||
if (!file.exists()) {
|
||||
throw new NotFoundAccessException(ErrorCode.BIZ_FILE_NOT_EXIST);
|
||||
}
|
||||
|
||||
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
|
||||
if (StringUtils.isEmpty(fileName)) {
|
||||
fileName = file.getName();
|
||||
}
|
||||
|
||||
ContentDisposition contentDisposition = ContentDisposition
|
||||
.builder("inline")
|
||||
.filename(fileName, StandardCharsets.UTF_8)
|
||||
.build();
|
||||
headers.setContentDisposition(contentDisposition);
|
||||
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers(headers)
|
||||
.contentLength(file.length())
|
||||
.contentType(mediaType)
|
||||
.body(new InputStreamResource(FileUtil.getInputStream(file)));
|
||||
}
|
||||
|
||||
}
|
||||