从捐赠版合并部分代码

This commit is contained in:
zhaojun
2025-04-03 21:33:53 +08:00
parent 8f8dc86365
commit 81b9ac5923
908 changed files with 16269 additions and 12912 deletions

224
pom.xml
View File

@@ -4,29 +4,58 @@
<groupId>im.zhaojun</groupId>
<artifactId>zfile</artifactId>
<version>4.1.5</version>
<version>4.2.0</version>
<name>zfile</name>
<packaging>${packaging}</packaging>
<packaging>jar</packaging>
<description>一个在线的文件浏览系统</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.12</version>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<skipTests>true</skipTests>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
<snakeyaml.version>2.0</snakeyaml.version>
<jackson-bom.version>2.14.1</jackson-bom.version>
<sqlite-jdbc.version>3.41.2.2</sqlite-jdbc.version>
<packaging>jar</packaging>
<sqlite-jdbc.version>3.46.0.1</sqlite-jdbc.version>
<flyway.version>10.12.0</flyway.version>
<lombok.version>1.18.32</lombok.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.24.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- spring boot 官方相关 -->
<!-- spring boot 官方相关-->
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>24.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@@ -35,11 +64,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
@@ -53,12 +87,10 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库相关 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<dependency>
@@ -68,12 +100,18 @@
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>7.15.0</version>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-mysql</artifactId>
<version>${flyway.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>
@@ -83,10 +121,10 @@
<artifactId>java-sdk</artifactId>
<version>4.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/software.amazon.awssdk/s3 -->
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>1.12.470</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>com.qiniu</groupId>
@@ -94,53 +132,86 @@
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
<version>0.2.20</version>
</dependency>
<dependency>
<groupId>com.github.lookfirst</groupId>
<artifactId>sardine</artifactId>
<version>5.10</version>
<version>5.12</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.35.2</version>
</dependency>
<!-- 登陆/权限相关 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0.temp1</version>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- 文档相关 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version>
</dependency>
<!-- 工具类 -->
<!-- <dependency>-->
<!-- <groupId>com.hierynomus</groupId>-->
<!-- <artifactId>sshj</artifactId>-->
<!-- <version>0.38.0</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
<version>5.8.28</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
<exclusions>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.9.0</version>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
@@ -154,7 +225,7 @@
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
<version>33.2.0-jre</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
@@ -168,7 +239,7 @@
</dependency>
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp-spring-boot-starter</artifactId>
<artifactId>totp</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
@@ -179,13 +250,17 @@
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20230227</version>
<version>20231013</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
@@ -206,8 +281,15 @@
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dns-cache-manipulator</artifactId>
<version>1.8.1</version>
<version>1.8.2</version>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.6.3</version>
</dependency>
</dependencies>
<build>
@@ -215,21 +297,13 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
@@ -240,7 +314,7 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<version>1.18.32</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
@@ -257,51 +331,49 @@
</plugins>
</build>
<profiles>
<profile>
<id>enable-war-packaging</id>
<activation>
<property>
<name>packaging</name>
<value>war</value>
</property>
</activation>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>com.uyoqu.framework</groupId>
<artifactId>maven-plugin-starter</artifactId>
<version>1.0.0</version>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<phase>package</phase>
<id>build-native</id>
<goals>
<goal>bin</goal>
<goal>compile-no-fork</goal>
</goals>
<configuration>
<jvms>
<jvm>-Djava.security.egd=file:/dev/./urandom</jvm>
<jvm>-Dfile.encoding=utf-8</jvm>
<jvm>-Djava.net.preferIPv4Stack=false</jvm>
<jvm>-Djava.net.preferIPv4Addresses=true</jvm>
<jvm>-Djava.awt.headless=true</jvm>
</jvms>
</configuration>
<phase>package</phase>
</execution>
</executions>
<configuration>
<fallback>false</fallback>
<imageName>${project.name}</imageName>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
<jvmArgs>
<jvmArg>--add-opens=java.base/java.net=ALL-UNNAMED</jvmArg>
<jvmArg>--add-opens=java.base/sun.net=ALL-UNNAMED</jvmArg>
</jvmArgs>
<buildArgs>
<arg>
-march=compatibility
-H:+AddAllCharsets
--features=im.zhaojun.zfile.aot.LambdaRegistrationFeature
--features=im.zhaojun.zfile.aot.BouncyCastleFeature
--features=im.zhaojun.zfile.aot.SQLiteNativeConfiguration
</arg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>enable-jar-packaging</id>
<activation>
<property>
<name>packaging</name>
<value>jar</value>
</property>
</activation>
</profile>
</profiles>
</project>

View File

@@ -1,173 +0,0 @@
/*
* Copyright (c) 2011-2022, baomidou (jobob@qq.com).
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.mybatisplus.core.handlers;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.ExceptionUtils;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.invoker.Invoker;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义枚举属性转换器
*
* @author hubin
* @since 2017-10-11
*/
public class MybatisEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private static final Map<String, String> TABLE_METHOD_OF_ENUM_TYPES = new ConcurrentHashMap<>();
private static final ReflectorFactory REFLECTOR_FACTORY = new DefaultReflectorFactory();
private final Class<E> enumClassType;
private final Class<?> propertyType;
private final Invoker getInvoker;
public MybatisEnumTypeHandler(Class<E> enumClassType) {
if (enumClassType == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.enumClassType = enumClassType;
MetaClass metaClass = MetaClass.forClass(enumClassType, REFLECTOR_FACTORY);
String name = "value";
if (!IEnum.class.isAssignableFrom(enumClassType)) {
name = findEnumValueFieldName(this.enumClassType).orElseThrow(() -> new IllegalArgumentException(String.format("Could not find @EnumValue in Class: %s.", this.enumClassType.getName())));
}
this.propertyType = ReflectionKit.resolvePrimitiveIfNecessary(metaClass.getGetterType(name));
this.getInvoker = metaClass.getGetInvoker(name);
}
/**
* 查找标记标记EnumValue字段
*
* @param clazz class
* @return EnumValue字段
* @since 3.3.1
*/
public static Optional<String> findEnumValueFieldName(Class<?> clazz) {
if (clazz != null && clazz.isEnum()) {
String className = clazz.getName();
return Optional.ofNullable(CollectionUtils.computeIfAbsent(TABLE_METHOD_OF_ENUM_TYPES, className, key -> {
Optional<Field> fieldOptional = findEnumValueAnnotationField(clazz);
return fieldOptional.map(Field::getName).orElse(null);
}));
}
return Optional.empty();
}
private static Optional<Field> findEnumValueAnnotationField(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EnumValue.class)).findFirst();
}
/**
* 判断是否为MP枚举处理
*
* @param clazz class
* @return 是否为MP枚举处理
* @since 3.3.1
*/
public static boolean isMpEnums(Class<?> clazz) {
return clazz != null && clazz.isEnum() && (IEnum.class.isAssignableFrom(clazz) || findEnumValueFieldName(clazz).isPresent());
}
@SuppressWarnings("Duplicates")
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType)
throws SQLException {
if (jdbcType == null) {
ps.setObject(i, this.getValue(parameter));
} else {
// see r3589
ps.setObject(i, this.getValue(parameter), jdbcType.TYPE_CODE);
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object value = rs.getObject(columnName);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object value = rs.getObject(columnIndex, this.propertyType);
if (null == value && rs.wasNull()) {
return null;
}
return this.valueOf(value);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object value = cs.getObject(columnIndex, this.propertyType);
if (null == value && cs.wasNull()) {
return null;
}
return this.valueOf(value);
}
private E valueOf(Object value) {
E[] es = this.enumClassType.getEnumConstants();
return Arrays.stream(es).filter((e) -> equalsValue(value, getValue(e))).findAny().orElse(null);
}
/**
* 值比较
*
* @param sourceValue 数据库字段值
* @param targetValue 当前枚举属性值
* @return 是否匹配
* @since 3.3.0
*/
protected boolean equalsValue(Object sourceValue, Object targetValue) {
String sValue = StringUtils.toStringTrim(sourceValue);
String tValue = StringUtils.toStringTrim(targetValue);
if (sourceValue instanceof Number && targetValue instanceof Number
&& new BigDecimal(sValue).compareTo(new BigDecimal(tValue)) == 0) {
return true;
}
return Objects.equals(sValue, tValue);
}
private Object getValue(Object object) {
try {
return this.getInvoker.invoke(object, new Object[0]);
} catch (ReflectiveOperationException e) {
throw ExceptionUtils.mpe(e);
}
}
}

View File

@@ -0,0 +1,33 @@
package im.zhaojun.zfile.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* 接口限流注解
*
* @author zhaojun
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLimit {
/**
* 持续时间
*/
int timeout();
/**
* 时间单位, 默认为秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 单位时间内允许访问的最大次数
*/
long maxCount();
}

View File

@@ -0,0 +1,17 @@
package im.zhaojun.zfile.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 演示系统禁用功能注解
*
* @author zhaojun
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DemoDisable {
}

View File

@@ -0,0 +1,74 @@
package im.zhaojun.zfile.core.aspect;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.extra.servlet.JakartaServletUtil;
import im.zhaojun.zfile.core.annotation.ApiLimit;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.util.RequestHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 接口限流切面, 通过注解 {@link ApiLimit} 进行限流.
*
* @author zhaojun
*/
@Aspect
@Component
public class ApiLimitAspect {
private final TimedCache<String, AtomicLong> apiLimitTimedCache = CacheUtil.newTimedCache(1000);
public static final String API_LIMIT_KEY_PREFIX = "api_limit_";
/**
* 定义一个切点(通过注解)
*/
@Pointcut("@annotation(im.zhaojun.zfile.core.annotation.ApiLimit)")
public void apiLimit() {
}
/**
* 在标记了 {@link ApiLimit} 注解的方法执行前进行限流校验.
*
* @param joinPoint 切点
*/
@Before("apiLimit()")
public void before(JoinPoint joinPoint) {
// 获取当前请求的方法上的注解中设置的值
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 反射获取当前被调用的方法
Method method = signature.getMethod();
// 获取方法中的注解
ApiLimit apiLimit = method.getDeclaredAnnotation(ApiLimit.class);
int timeout = apiLimit.timeout();
TimeUnit timeUnit = apiLimit.timeUnit();
long millis = timeUnit.toMillis(timeout);
long maxCount = apiLimit.maxCount();
// 获取请求相关信息
String ip = JakartaServletUtil.getClientIP(RequestHolder.getRequest());
// 限制访问次数
String key = API_LIMIT_KEY_PREFIX.concat(ip).concat(method.getName());
AtomicLong atomicLong = apiLimitTimedCache.get(key, false);
if (atomicLong == null) {
apiLimitTimedCache.put(key, new AtomicLong(1), millis);
} else {
if (atomicLong.incrementAndGet() > maxCount) {
throw new BizException(ErrorCode.BIZ_ACCESS_TOO_FREQUENT);
}
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
package im.zhaojun.zfile.core.aspect;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.config.ZFileProperties;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import jakarta.annotation.Resource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 通过注解 {@link DemoDisable} 限制演示系统不可操作的功能.
*
* @author zhaojun
*/
@Aspect
@Component
public class DemoDisableAspect {
@Resource
private ZFileProperties zFileProperties;
/**
* 定义一个切点(通过注解)
*/
@Pointcut("@annotation(im.zhaojun.zfile.core.annotation.DemoDisable)")
public void demoDisable() {
}
/**
* 在标记了 {@link DemoDisable} 注解的方法执行前进行限流校验.
*
* @param joinPoint 切点
*/
@Before("demoDisable()")
public void before(JoinPoint joinPoint) {
if (zFileProperties.isDemoSite()) {
throw new BizException(ErrorCode.DEMO_SITE_DISABLE_OPERATOR);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
package im.zhaojun.zfile.core.config;
import im.zhaojun.zfile.core.httpclient.ZFileOkHttp3ClientHttpRequestFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* restTemplate 相关配置
*
* @author zhaojun
*/
@Configuration
public class RestTemplateConfig {
/**
* OneDrive 请求 RestTemplate.
* 获取 header 中的 storageId 来判断到底是哪个存储源 ID, 在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证.
*/
@Bean
public RestTemplate oneDriveRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new ZFileOkHttp3ClientHttpRequestFactory());
return restTemplate;
}
}

View File

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

View File

@@ -6,6 +6,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.stereotype.Component;
/**
* ZFile 配置类,将配置文件中的 zfile 配置项映射到该类中.
*
* @author zhaojun
*/
@Data
@@ -16,4 +18,20 @@ public class ZFileProperties {
private boolean debug;
private String version;
private boolean isDemoSite;
private OAuth2Properties onedrive = new OAuth2Properties();
private OAuth2Properties onedriveChina = new OAuth2Properties();
private OAuth2Properties gd = new OAuth2Properties();
@Data
public static class OAuth2Properties {
private String clientId;
private String clientSecret;
private String redirectUri;
private String scope;
}
}

View File

@@ -0,0 +1,123 @@
package im.zhaojun.zfile.core.config.datasource;
import cn.hutool.core.io.FileUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.zaxxer.hikari.HikariDataSource;
import im.zhaojun.zfile.core.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.flyway.FlywayProperties;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import java.io.File;
import java.util.List;
/**
* 在 Spring 容器初始化时, 对数据源进行处理.
* <br/>
* 1. 针对 DataSource 进行处理,仅针对 sqlite
* <ul>
* <li>提前创建 sqlite 数据文件所在目录.</li>
* <li>检测到版本更新时(pom.xml -> project.version)自动备份原数据库.</li>
* </ul>
* <br/>
* 2. 针对 Flyway 进行处理,根据数据库类型, 配置不同的 Flyway Migration Location
* <ul>
* <li>SQLite 数据库使用 migration-sqlite 目录.</li>
* <li>MySQL 数据库使用 migration-mysql 目录.</li>
* </ul>
*
* @author zhaojun
*/
@Slf4j
@Component
public class DataSourceBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
public static final String ZFILE_VERSION_PROPERTIES = "zfile.version";
public static final String DRIVE_CLASS_NAME_PROPERTIES = "spring.datasource.driver-class-name";
public static final String DATA_SOURCE_BEAN_NAME = "dataSource";
public static final String SQLITE_DRIVE_CLASS_NAME = "org.sqlite.JDBC";
public static final String MYSQL_DRIVE_CLASS_NAME = "com.mysql.cj.jdbc.Driver";
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 如果更改了数据源类型这里要修改
if (bean instanceof HikariDataSource dataSource && DATA_SOURCE_BEAN_NAME.equals(beanName)) {
processSqliteDataSource(dataSource);
} else if (bean instanceof FlywayProperties flywayProperties) {
processFlywayLocations(flywayProperties);
}
return bean;
}
/**
* 如果是 sqlite 数据库, 提前创建数据库文件所在目录. <br/>
*
* 如果检测到版本更新, 自动备份原数据库文件.
*
* @param dataSource
* 数据源
*/
private void processSqliteDataSource(HikariDataSource dataSource) {
String driverClassName = dataSource.getDriverClassName();
String jdbcUrl = dataSource.getJdbcUrl();
if (StringUtils.equals(driverClassName, SQLITE_DRIVE_CLASS_NAME)) {
String path = jdbcUrl.replace("jdbc:sqlite:", "");
String folderPath = FileUtil.getAbsolutePath(new File(path).getParentFile());
log.info("SQLite 数据库文件所在目录: [{}]", folderPath);
File file = new File(folderPath);
if (!file.exists()) {
log.info("检测到 SQLite 数据库文件所在目录不存在, 已自动创建.");
if (!file.mkdirs()) {
log.error("SQLite 数据库文件创建失败.");
}
} else {
log.info("检测到 SQLite 数据库文件所在目录已存在, 无需自动创建.");
// 更新版本时, 先自动备份数据库文件
String version = SpringUtil.getProperty(ZFILE_VERSION_PROPERTIES);
if (StringUtils.isNotEmpty(version)) {
String backupPath = folderPath + "/zfile-update-" + version + "-backup.db";
if (!FileUtil.exist(path)) {
log.error("检测到 SQLite 数据库文件不存在, 一般为初始化状态,无需备份.");
return;
}
if (FileUtil.exist(backupPath)) {
log.info("检测到 SQLite 数据库备份文件 [{}] 已存在, 无需再次备份.", backupPath);
} else {
FileUtil.copy(path, backupPath, false);
log.info("自动备份 SQLite 数据库文件到: [{}]", backupPath);
}
}
}
}
}
/**
* 根据使用的不同数据库, 配置使用不同的 migration location
*
* @param flywayProperties
* flyway 配置项
*/
private void processFlywayLocations(FlywayProperties flywayProperties) {
String driveClassName = SpringUtil.getProperty(DRIVE_CLASS_NAME_PROPERTIES);
if (SQLITE_DRIVE_CLASS_NAME.equals(driveClassName)) {
flywayProperties.setLocations(List.of("classpath:db/migration-sqlite"));
} else if (MYSQL_DRIVE_CLASS_NAME.equals(driveClassName)) {
flywayProperties.setLocations(List.of("classpath:db/migration-mysql"));
}
}
@Override
public int getOrder() {
return Integer.MIN_VALUE;
}
}

View File

@@ -0,0 +1,73 @@
package im.zhaojun.zfile.core.config.docs;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.HeaderParameter;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Knife4j 参数配置,区分前台功能和管理员功能,并为管理员接口增加统一 token header 配置.
*
* @author zhaojun
*/
@Configuration
public class Knife4jConfiguration {
@Bean
public GroupedOpenApi groupedOpenApi() {
String groupName = "前台功能";
return GroupedOpenApi.builder()
.group(groupName)
.packagesToScan("im.zhaojun.zfile.module")
.pathsToExclude("/admin/**")
.build();
}
@Bean
public GroupedOpenApi groupedOpenApi2() {
String groupName = "管理员功能";
return GroupedOpenApi.builder()
.group(groupName)
.packagesToScan("im.zhaojun.zfile.module")
.pathsToMatch("/admin/**")
.addOperationCustomizer(globalOperationCustomizer())
.build();
}
public OperationCustomizer globalOperationCustomizer() {
return (operation, handlerMethod) -> {
operation.addParametersItem(new HeaderParameter()
.name("zfile-token")
.description("token")
.required(true)
.schema(new StringSchema()));
return operation;
};
}
@Bean
public OpenAPI customOpenAPI() {
Contact contact = new Contact();
contact.setName("zhaojun");
contact.setUrl("https://zfile.vip");
contact.setEmail("873019219@qq.com");
return new OpenAPI()
.info(new Info()
.title("ZFILE 文档")
.description("# 这是 ZFILE Restful 接口文档展示页面")
.termsOfService("https://www.zfile.vip")
.contact(contact)
.version("1.0")
.license(new License()
.name("Apache 2.0")
.url("http://doc.xiaominfo.com")));
}
}

View File

@@ -0,0 +1,25 @@
package im.zhaojun.zfile.core.config.jackson;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
/**
* JSON String 反序列化器, 用于将 JSON 字符串反序列化为 JSON 对象.
*
* @author zhaojun
*/
public class JSONStringDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser p, DeserializationContext context) throws IOException {
JsonNode node = p.getCodec().readTree(p);
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(node);
}
}

View File

@@ -0,0 +1,22 @@
package im.zhaojun.zfile.core.config.jackson;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
/**
* JSON String 序列化器, 用于将 JSON 字符串序列化为 JSON 对象.
*
* @author zhaojun
*/
public class JSONStringSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeRawValue(value);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,121 @@
package im.zhaojun.zfile.core.config.mybatis;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.springframework.core.ResolvableType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
/**
* 自定义 Set 类型处理器, 用于处理数据库 VARCHAR 类型字段和 Java Set 类型属性之间的转换.
* 支持字符串格式为: "[a, b, c]".
*
* @author zhaojun
*/
@MappedJdbcTypes(JdbcType.VARCHAR)
public abstract class CollectionTypeHandler<T> extends BaseTypeHandler<Object> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
if (parameter instanceof Collection collection) {
StringJoiner joiner = new StringJoiner(",");
for (Object o : collection) {
joiner.add(Convert.toStr(o));
}
ps.setString(i, joiner.toString());
} else {
ps.setString(i, Convert.toStr(parameter));
}
}
@Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
String str = rs.getString(columnName);
return convertToEntityAttribute(str);
}
@Override
public Object getNullableResult(ResultSet rs, int columnIndex)
throws SQLException {
String str = rs.getString(columnIndex);
return convertToEntityAttribute(str);
}
@Override
public Object getNullableResult(CallableStatement cs, int columnIndex)
throws SQLException {
String str = cs.getString(columnIndex);
return convertToEntityAttribute(str);
}
private Class<?> collectionClazz;
private Type innerType;
/**
* 构造方法
*/
public CollectionTypeHandler() {
ResolvableType resolvableType = ResolvableType.forClass(getClass());
Type type = resolvableType.as(CollectionTypeHandler.class).getGeneric().getType();
if (type instanceof ParameterizedType parameterizedType) {
collectionClazz = (Class<?>) parameterizedType.getRawType();
// 获取实际类型参数(泛型参数,例如 List<String> 中的 String
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 使用这些信息做进一步操作
for (Type actualTypeArgument : actualTypeArguments) {
innerType = actualTypeArgument;
break;
}
}
}
private Object convertToEntityAttribute(String dbData) {
if (StrUtil.isEmpty(dbData)) {
if (List.class.isAssignableFrom(collectionClazz)) {
return Collections.emptyList();
} else if (Set.class.isAssignableFrom(collectionClazz)) {
return Collections.emptySet();
} else {
return null;
}
}
Collection collection;
if (List.class.isAssignableFrom(collectionClazz)) {
collection = new ArrayList<>();
} else if (Set.class.isAssignableFrom(collectionClazz)) {
collection = new HashSet<>();
} else {
return null;
}
String[] split = dbData.split(",");
for (String s : split) {
if (NumberUtil.isNumber(s)) {
collection.add(Convert.convert(Integer.class, s));
} else {
collection.add(s);
}
}
return collection;
}
}

View File

@@ -0,0 +1,33 @@
package im.zhaojun.zfile.core.config.mybatis;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* mybatis-plus 配置类
*
* @author zhaojun
*/
@Configuration
public class MyBatisPlusConfig {
/**
* mybatis plus 分页插件配置
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(DataSource dataSource) throws SQLException {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
String databaseProductName = dataSource.getConnection().getMetaData().getDatabaseProductName();
DbType dbType = DbType.getDbType(databaseProductName);
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(dbType));
return interceptor;
}
}

View File

@@ -1,4 +1,4 @@
package im.zhaojun.zfile.core.config;
package im.zhaojun.zfile.core.config.mybatis;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.springframework.stereotype.Component;
@@ -7,6 +7,13 @@ import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
/**
* MyBatis 数据库 ID Provider, 用于判断当前数据库类型来执行不同的 SQL 语句. <br>
* 可在 xml 中使用 <code>&lt;if test="_databaseId = 'mysql'"&gt; </code> 来判断数据库类型. <br>
* 也可以在外层使用 <code>&lt;delete id="xxx" databaseId="sqlite"&gt;</code> 来判断数据库类型.
*
* @author zhaojun
*/
@Component
public class MyDatabaseIdProvider implements DatabaseIdProvider {
@@ -31,4 +38,4 @@ public class MyDatabaseIdProvider implements DatabaseIdProvider {
return dbAlias;
}
}
}

View File

@@ -0,0 +1,29 @@
package im.zhaojun.zfile.core.config.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* MyBatis Plus 自动填充配置类
* 用于自动填充 createTime 和 updateTime 字段
*
* @author zhaojun
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}

View File

@@ -0,0 +1,32 @@
package im.zhaojun.zfile.core.config.security;
import cn.dev33.satoken.session.SaSession;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
/**
* Jackson 定制版 SaSession忽略 timeout 等属性的序列化
*
* @author click33
* @since 1.34.0
*/
@JsonIgnoreProperties({"timeout"})
public class SaSessionForJacksonCustomized extends SaSession {
/**
*
*/
private static final long serialVersionUID = -7600983549653130681L;
public SaSessionForJacksonCustomized() {
super();
}
/**
* 构建一个Session对象
* @param id Session的id
*/
public SaSessionForJacksonCustomized(String id) {
super(id);
}
}

View File

@@ -0,0 +1,34 @@
package im.zhaojun.zfile.core.config.security;
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SaToken 权限配置, 配置管理员才能访问管理员功能.
*
* @author zhaojun
*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
/**
* 注册权限校验拦截器, 拦截所有 /admin/** 请求,但不包含 /admin 因为这个是登录页面.
*
* @param registry
* 拦截器注册器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor(handle -> {
SaRouter.match("/admin/**", () -> {
StpUtil.checkLogin();
StpUtil.checkRole("admin");
});
})).addPathPatterns("/**").excludePathPatterns("/admin");
}
}

View File

@@ -0,0 +1,305 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package im.zhaojun.zfile.core.config.security;
import cn.dev33.satoken.dao.SaTokenDao;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Sa-Token 持久层实现 [ Redis存储、Jackson序列化 ]
*
* @author click33
* @since 1.34.0
*/
@Component
@ConditionalOnProperty(name = "spring.data.redis.host")
public class SaTokenDaoRedisJackson implements SaTokenDao {
public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_PATTERN = "yyyy-MM-dd";
public static final String TIME_PATTERN = "HH:mm:ss";
public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_PATTERN);
/**
* ObjectMapper 对象 (以 public 作用域暴露出此对象,方便开发者二次更改配置)
*
* <p> 例如:
* <pre>
* SaTokenDaoRedisJackson redisJackson = (SaTokenDaoRedisJackson) SaManager.getSaTokenDao();
* redisJackson.objectMapper.xxx = xxx;
* </pre>
* </p>
*/
public ObjectMapper objectMapper;
/**
* String 读写专用
*/
public StringRedisTemplate stringRedisTemplate;
/**
* Object 读写专用
*/
public RedisTemplate<String, Object> objectRedisTemplate;
/**
* 标记:是否已初始化成功
*/
public boolean isInit;
@Autowired
public void init(RedisConnectionFactory connectionFactory) {
// 如果已经初始化成功了,就立刻退出,不重复初始化
if(this.isInit) {
return;
}
// 指定相应的序列化方案
StringRedisSerializer keySerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
// 通过反射获取Mapper对象, 增加一些配置, 增强兼容性
try {
Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
field.setAccessible(true);
this.objectMapper = (ObjectMapper) field.get(valueSerializer);
// 配置[忽略未知字段]
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 配置[时间类型转换]
JavaTimeModule timeModule = new JavaTimeModule();
// LocalDateTime序列化与反序列化
timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
// LocalDate序列化与反序列化
timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
// LocalTime序列化与反序列化
timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
this.objectMapper.registerModule(timeModule);
// 重写 SaSession 生成策略
SaStrategy.instance.createSession = (sessionId) -> new SaSessionForJacksonCustomized(sessionId);
} catch (Exception e) {
System.err.println(e.getMessage());
}
// 构建StringRedisTemplate
StringRedisTemplate stringTemplate = new StringRedisTemplate();
stringTemplate.setConnectionFactory(connectionFactory);
stringTemplate.afterPropertiesSet();
// 构建RedisTemplate
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
// 开始初始化相关组件
this.stringRedisTemplate = stringTemplate;
this.objectRedisTemplate = template;
// 打上标记,表示已经初始化成功,后续无需再重新初始化
this.isInit = true;
}
/**
* 获取Value如无返空
*/
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
/**
* 写入Value并设定存活时间 (单位: 秒)
*/
@Override
public void set(String key, String value, long timeout) {
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
stringRedisTemplate.opsForValue().set(key, value);
} else {
stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
}
/**
* 修修改指定key-value键值对 (过期时间不变)
*/
@Override
public void update(String key, String value) {
long expire = getTimeout(key);
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.set(key, value, expire);
}
/**
* 删除Value
*/
@Override
public void delete(String key) {
stringRedisTemplate.delete(key);
}
/**
* 获取Value的剩余存活时间 (单位: 秒)
*/
@Override
public long getTimeout(String key) {
return stringRedisTemplate.getExpire(key);
}
/**
* 修改Value的剩余存活时间 (单位: 秒)
*/
@Override
public void updateTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久那么再次set一次
this.set(key, this.get(key), timeout);
}
return;
}
stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 获取Object如无返空
*/
@Override
public Object getObject(String key) {
return objectRedisTemplate.opsForValue().get(key);
}
/**
* 写入Object并设定存活时间 (单位: 秒)
*/
@Override
public void setObject(String key, Object object, long timeout) {
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
// 判断是否为永不过期
if(timeout == SaTokenDao.NEVER_EXPIRE) {
objectRedisTemplate.opsForValue().set(key, object);
} else {
objectRedisTemplate.opsForValue().set(key, object, timeout, TimeUnit.SECONDS);
}
}
/**
* 更新Object (过期时间不变)
*/
@Override
public void updateObject(String key, Object object) {
long expire = getObjectTimeout(key);
// -2 = 无此键
if(expire == SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
this.setObject(key, object, expire);
}
/**
* 删除Object
*/
@Override
public void deleteObject(String key) {
objectRedisTemplate.delete(key);
}
/**
* 获取Object的剩余存活时间 (单位: 秒)
*/
@Override
public long getObjectTimeout(String key) {
return objectRedisTemplate.getExpire(key);
}
/**
* 修改Object的剩余存活时间 (单位: 秒)
*/
@Override
public void updateObjectTimeout(String key, long timeout) {
// 判断是否想要设置为永久
if(timeout == SaTokenDao.NEVER_EXPIRE) {
long expire = getObjectTimeout(key);
if(expire == SaTokenDao.NEVER_EXPIRE) {
// 如果其已经被设置为永久,则不作任何处理
} else {
// 如果尚未被设置为永久那么再次set一次
this.setObject(key, this.getObject(key), timeout);
}
return;
}
objectRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 搜索数据
*/
@Override
public List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {
Set<String> keys = stringRedisTemplate.keys(prefix + "*" + keyword + "*");
List<String> list = new ArrayList<>(keys);
return SaFoxUtil.searchList(list, start, size, sortType);
}
}

View File

@@ -0,0 +1,44 @@
package im.zhaojun.zfile.core.config.security;
import cn.dev33.satoken.stp.StpInterface;
import cn.hutool.core.convert.Convert;
import im.zhaojun.zfile.module.user.service.UserService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 自定义权限加载接口实现类
*
* @author zhaojun
*/
@Component
public class StpInterfaceImpl implements StpInterface {
private static final List<String> ADMIN_ROLE_LIST = Collections.singletonList("admin");
public static final List<String> EMPTY_ROLE_LIST = Collections.emptyList();
@Resource
private UserService userService;
/**
* 返回一个账号所拥有的权限码集合,这里没用到这个功能,所以返回空集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return Collections.emptyList();
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
boolean isAdmin = userService.isAdmin(Convert.toInt(loginId));
return isAdmin ? ADMIN_ROLE_LIST : EMPTY_ROLE_LIST;
}
}

View File

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

View File

@@ -1,7 +1,9 @@
package im.zhaojun.zfile.core.config;
package im.zhaojun.zfile.core.config.spring;
import cn.hutool.core.util.BooleanUtil;
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;
@@ -26,8 +28,9 @@ public class SpringCacheConfig {
* 使用 TransactionAwareCacheManagerProxy 装饰 ConcurrentMapCacheManager使其支持事务 putevictclear 操作延迟到事务成功提交再执行.
*/
@Bean
@ConditionalOnMissingBean(SaTokenDaoRedisJackson.class)
public CacheManager cacheManager() {
return BooleanUtil.isFalse(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
return BooleanUtils.isNotTrue(dbCacheEnable) ? new NoOpCacheManager() : new TransactionAwareCacheManagerProxy(new ConcurrentMapCacheManager());
}
}

View File

@@ -1,13 +1,14 @@
package im.zhaojun.zfile.core.config;
package im.zhaojun.zfile.core.config.spring;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.baomidou.mybatisplus.annotation.IEnum;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import im.zhaojun.zfile.core.exception.core.SystemException;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import javax.validation.constraints.NotNull;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -70,7 +71,7 @@ public class StringToEnumConverterFactory implements ConverterFactory<String, En
// 获取
T t = enumMap.get(source);
if (t == null) {
throw new IllegalArgumentException("该字符串找不到对应的枚举对象 字符串:" + source);
throw new SystemException("该字符串找不到对应的枚举对象 字符串:" + source);
}
return t;
}
@@ -84,7 +85,7 @@ public class StringToEnumConverterFactory implements ConverterFactory<String, En
try {
method = enumType.getMethod("getValue");
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException(String.format("类:%s 找不到 getValue方法",
throw new SystemException(String.format("类:%s 找不到 getValue方法",
enumType.getName()));
}
} else {

View File

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

View File

@@ -0,0 +1,91 @@
package im.zhaojun.zfile.core.config.totp;
import dev.samstevens.totp.TotpInfo;
import dev.samstevens.totp.code.*;
import dev.samstevens.totp.qr.QrDataFactory;
import dev.samstevens.totp.qr.QrGenerator;
import dev.samstevens.totp.qr.ZxingPngQrGenerator;
import dev.samstevens.totp.recovery.RecoveryCodeGenerator;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import dev.samstevens.totp.time.SystemTimeProvider;
import dev.samstevens.totp.time.TimeProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass({TotpInfo.class})
@EnableConfigurationProperties({TotpProperties.class})
public class TotpAutoConfiguration {
private final TotpProperties props;
@Autowired
public TotpAutoConfiguration(TotpProperties props) {
this.props = props;
}
@Bean
@ConditionalOnMissingBean
public SecretGenerator secretGenerator() {
int length = this.props.getSecret().getLength();
return new DefaultSecretGenerator(length);
}
@Bean
@ConditionalOnMissingBean
public TimeProvider timeProvider() {
return new SystemTimeProvider();
}
@Bean
@ConditionalOnMissingBean
public HashingAlgorithm hashingAlgorithm() {
return HashingAlgorithm.SHA1;
}
@Bean
@ConditionalOnMissingBean
public QrDataFactory qrDataFactory(HashingAlgorithm hashingAlgorithm) {
return new QrDataFactory(hashingAlgorithm, this.getCodeLength(), this.getTimePeriod());
}
@Bean
@ConditionalOnMissingBean
public QrGenerator qrGenerator() {
return new ZxingPngQrGenerator();
}
@Bean
@ConditionalOnMissingBean
public CodeGenerator codeGenerator(HashingAlgorithm algorithm) {
return new DefaultCodeGenerator(algorithm, this.getCodeLength());
}
@Bean
@ConditionalOnMissingBean
public CodeVerifier codeVerifier(CodeGenerator codeGenerator, TimeProvider timeProvider) {
DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
verifier.setTimePeriod(this.getTimePeriod());
verifier.setAllowedTimePeriodDiscrepancy(this.props.getTime().getDiscrepancy());
return verifier;
}
@Bean
@ConditionalOnMissingBean
public RecoveryCodeGenerator recoveryCodeGenerator() {
return new RecoveryCodeGenerator();
}
private int getCodeLength() {
return this.props.getCode().getLength();
}
private int getTimePeriod() {
return this.props.getTime().getPeriod();
}
}

View File

@@ -0,0 +1,85 @@
package im.zhaojun.zfile.core.config.totp;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(
prefix = "totp"
)
public class TotpProperties {
private static final int DEFAULT_SECRET_LENGTH = 32;
private static final int DEFAULT_CODE_LENGTH = 6;
private static final int DEFAULT_TIME_PERIOD = 30;
private static final int DEFAULT_TIME_DISCREPANCY = 1;
private final Secret secret = new Secret();
private final Code code = new Code();
private final Time time = new Time();
public TotpProperties() {
}
public Secret getSecret() {
return this.secret;
}
public Code getCode() {
return this.code;
}
public Time getTime() {
return this.time;
}
public static class Time {
private int period = 30;
private int discrepancy = 1;
public Time() {
}
public int getPeriod() {
return this.period;
}
public void setPeriod(int period) {
this.period = period;
}
public int getDiscrepancy() {
return this.discrepancy;
}
public void setDiscrepancy(int discrepancy) {
this.discrepancy = discrepancy;
}
}
public static class Code {
private int length = 6;
public Code() {
}
public int getLength() {
return this.length;
}
public void setLength(int length) {
this.length = length;
}
}
public static class Secret {
private int length = 32;
public Secret() {
}
public int getLength() {
return this.length;
}
public void setLength(int length) {
this.length = length;
}
}
}

View File

@@ -0,0 +1,18 @@
package im.zhaojun.zfile.core.constant;
/**
* 规则表达式类型常量
*
* @author zhaojun
*/
public class RuleTypeConstant {
public static final String IP = "ip";
public static final String REGEX = "regex";
public static final String ANT_PATH = "antPath";
public static final String SPRING_SIMPLE = "springSimple";
}

View File

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

View File

@@ -0,0 +1,16 @@
package im.zhaojun.zfile.core.constant;
/**
* ZFile 自定义 HTTP 请求头常量
*
* @author zhaojun
*/
public class ZFileHttpHeaderConstant {
public static final String ZFILE_TOKEN = "Zfile-Token";
public static final String AXIOS_REQUEST = "Axios-Request";
public static final String AXIOS_FROM = "Axios-From";
}

View File

@@ -1,17 +1,17 @@
package im.zhaojun.zfile.core.controller;
import cn.hutool.core.util.StrUtil;
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 org.apache.commons.io.IOUtils;
import org.springframework.core.io.ClassPathResource;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
@@ -19,12 +19,15 @@ import java.nio.charset.StandardCharsets;
*
* @author zhaojun
*/
@Slf4j
@Controller
public class FrontIndexController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private WebProperties webProperties;
/**
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
@@ -32,26 +35,64 @@ public class FrontIndexController {
*
* @return 转发到 /index.html
*/
@RequestMapping(value = {"/**/{[path:[^\\.]*}", "/"})
@RequestMapping(value = { "/"})
@ResponseBody
public String redirect() throws IOException {
public String redirect() {
// 读取 resources/static/index.html 文件修改 title 和 favicon 后返回
ClassPathResource resource = new ClassPathResource("static/index.html");
InputStream inputStream = resource.getInputStream();
String content = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
ResourceLoader resourceLoader = new FileSystemResourceLoader();
String[] staticLocations = webProperties.getResources().getStaticLocations();
// 如果 staticLocations 里没有包含 file:static/, 则手动添加
boolean fileStaticExist = false;
for (String staticLocation : staticLocations) {
if (staticLocation.startsWith("file:")) {
fileStaticExist = true;
break;
}
}
if (!fileStaticExist) {
staticLocations = org.apache.commons.lang3.ArrayUtils.add(staticLocations, "file:static/");
}
for (String staticLocation : staticLocations) {
org.springframework.core.io.Resource resource = resourceLoader.getResource(staticLocation + "/index.html");
boolean exists = resource.exists();
if (exists) {
String content;
try {
content = resource.getContentAsString(StandardCharsets.UTF_8);
log.debug("读取 index.html 文件成功, 文件路径: {}", staticLocation);
} catch (Exception e) {
log.error("{} 资源存在但读取 index.html 文件失败.", staticLocation);
return "static index.html read error";
}
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
// 替换为系统设置中的站点名称
String siteName = systemConfig.getSiteName();
if (StringUtils.isNotBlank(siteName)) {
content = content.replace("<title>ZFile</title>", "<title>" + siteName + "</title>");
}
// 替换为系统设置中的 favicon 地址
String faviconUrl = systemConfig.getFaviconUrl();
if (StringUtils.isNotBlank(faviconUrl)) {
content = content.replace("/favicon.svg", faviconUrl);
}
return content;
}
}
return "static index.html not found";
}
@RequestMapping(value = { "/guest"})
@ResponseBody
public String guest() {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String siteName = systemConfig.getSiteName();
if (StrUtil.isNotBlank(siteName)) {
content = content.replace("<title>ZFile</title>", "<title>" + siteName + "</title>");
}
String faviconUrl = systemConfig.getFaviconUrl();
if (StrUtil.isNotBlank(faviconUrl)) {
content = content.replace("/favicon.svg", faviconUrl);
}
return content;
return systemConfig.getGuestIndexHtml();
}
}

View File

@@ -3,9 +3,10 @@ package im.zhaojun.zfile.core.controller;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ZipUtil;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.util.FileResponseUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
@@ -22,7 +23,7 @@ import java.util.Date;
*
* @author zhaojun
*/
@Api(tags = "日志")
@Tag(name = "日志")
@ApiSort(8)
@Slf4j
@RestController
@@ -33,7 +34,8 @@ public class LogController {
private String zfileLogPath;
@GetMapping("/log/download")
@ApiOperation(value = "下载系统日志")
@Operation(summary = "下载系统日志")
@DemoDisable
public ResponseEntity<Resource> downloadLog() {
if (log.isDebugEnabled()) {
log.debug("下载诊断日志");

View File

@@ -0,0 +1,135 @@
package im.zhaojun.zfile.core.exception;
import lombok.Getter;
/**
* 异常信息枚举类
*
* @author zhaojun
*/
@Getter
public enum ErrorCode {
/**
* 系统异常
*/
SYSTEM_ERROR("50000", "系统异常"),
INVALID_STORAGE_SOURCE("50001", "无效或初始化失败的存储源"),
DEMO_SITE_DISABLE_OPERATOR("50002", "演示站点不允许此操作"),
/**
* 业务异常 4xxxx.
* 第二位为 0 时,是系统初始化相关错误
* 第二位为 1 时,是前台(文件管理)错误
* 第二位为 2 时,是登录错误
* 第二位为 3 时,是管理员端错误
*/
BIZ_ERROR("40000", "操作失败"),
BIZ_NOT_FOUND("40400", "NOT FOUND"),
// 第二位为 0 时,是系统初始化相关错误
BIZ_SYSTEM_ALREADY_INIT("40001", "系统已初始化,请勿重复初始化"),
BIZ_SYSTEM_INIT_ERROR("40002", "系统初始化错误"),
// 第二位为 1 时,是前台(文件管理)错误
BIZ_BAD_REQUEST("41000", "请求参数异常"),
BIZ_UNSUPPORTED_PROXY_DOWNLOAD("41001", "该存储源不支持代理下载"),
BIZ_INVALID_SIGNATURE("41002", "签名无效或下载地址已过期"),
BIZ_PREVIEW_FILE_SIZE_EXCEED("41003", "预览文本文件大小超出系统限制"),
BIZ_FILE_NOT_EXIST("41004", "文件不存在"),
BIZ_ACCESS_TOO_FREQUENT("41005", "请求太频繁了,请稍后再试"),
BIZ_UPLOAD_FILE_NOT_EMPTY("41006", "上传文件不能为空"),
BIZ_UPLOAD_FILE_ERROR("41010", "上传文件失败"),
BIZ_UPLOAD_FILE_TIMEOUT_ERROR("41026", "上传文件超时"),
BIZ_EXPIRE_TIME_ILLEGAL("41007", "过期时间不合法"),
BIZ_DELETE_FILE_NOT_EMPTY("41008", "非空文件夹不允许删除"),
BIZ_FILE_PATH_ILLEGAL("41009", "文件名/路径存在安全隐患"),
BIZ_DIRECT_LINK_NOT_ALLOWED("41011", "当前系统不允许使用直链"),
BIZ_SHORT_LINK_NOT_ALLOWED("41012", "当前系统不允许使用短链"),
BIZ_SHORT_LINK_EXPIRED("41013", "短链已失效"),
BIZ_SHORT_LINK_NOT_FOUNT("41014", "短链不存在"),
BIZ_DIRECT_LINK_EXPIRED("41015", "直链已失效"),
BIZ_STORAGE_NOT_SUPPORT_OPERATION("41016", "该存储类型不支持此操作"),
BIZ_STORAGE_NOT_FOUND("41017", "存储源不存在"),
BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION("41018", "非法或未授权的操作"),
BIZ_STORAGE_SOURCE_FILE_FORBIDDEN("41019", "文件目录无访问权限"),
BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_REQUIRED("41020", "此文件夹需要密码"),
BIZ_STORAGE_SOURCE_FOLDER_PASSWORD_ERROR("41021", "密码错误"),
BIZ_INVALID_FILE_NAME("41022", "文件名不合法"),
BIZ_UNSUPPORTED_OPERATION("41023", "不支持的操作"),
BIZ_FTP_CLIENT_POOL_FULL("41024", "FTP 客户端连接池已满"),
BIZ_SFTP_CLIENT_POOL_FULL("41025", "SFTP 客户端连接池已满"),
BIZ_FOLDER_NOT_EXIST("41026", "文件夹不存在"),
BIZ_UPLOAD_FILE_TYPE_NOT_ALLOWED("41027", "不允许上传的文件"),
BIZ_RENAME_FILE_TYPE_NOT_ALLOWED("41028", "不允许重命名到该名称"),
// 第二位为 2 时,是登录错误
BIZ_UNAUTHORIZED("42000", "未登录或未授权"),
BIZ_LOGIN_ERROR("42001", "登录失败, 账号或密码错误"),
BIZ_VERIFY_CODE_ERROR("42002", "验证码错误或已失效"),
// 第二位为 3 时,是管理员端错误
BIZ_ADMIN_ERROR("43000", "操作失败"),
BIZ_USER_NOT_EXIST("43001", "用户不存在"),
BIZ_USER_EXIST("43002", "用户已存在"),
BIZ_PASSWORD_NOT_SAME("43003", "两次密码不一致"),
BIZ_OLD_PASSWORD_ERROR("43004", "旧密码不匹配"),
BIZ_DELETE_BUILT_IN_USER("43005", "不能删除内置用户"),
BIZ_UNSUPPORTED_STORAGE_TYPE("43006", "不支持的存储类型"),
BIZ_STORAGE_KEY_EXIST("43007", "存储源别名已存在"),
BIZ_AUTO_GET_SHARE_POINT_SITES_ERROR("43008", "自动获取 SharePoint 网站列表失败"),
BIZ_ORIGINS_NOT_EMPTY("43009", "请先在 \"站点设置\" 中配置站点域名"),
BIZ_2FA_CODE_ERROR("43010", "两步验证失败"),
BIZ_STORAGE_INIT_ERROR("43011", "存储源初始化失败"),
BIZ_RULE_EXIST("43012", "规则已存在"),
/**
* 通用的无权限异常
*/
NO_FORBIDDEN("30000", "没有权限"),
/**
* 授权校验异常
*/
PRO_AUTH_CODE_EMPTY("20000", "请先去后台 \"基本设置\" 填写 \"授权码\""),
PRO_CHECK_REFERER_EMPTY("20001", "Referer 无效请检查服务端设置20001"), // Referer 无效,请检查服务端设置
PRO_CHECK_TIME_NO_SYNC("20002", "授权校验失败, 服务器时间异常20002"), // 授权校验失败, 服务器时间异常.
PRO_AUTH_CODE_INVALID_ERROR("20003", "授权码无效, 请检查后台 \"站点设置\" 中的 \"授权码\" 20003"),
PRO_CHECK_UNKNOWN_ERROR("20004", "授权验证异常未知异常20098"),
PRO_MSG_ERROR("20005", null);
private String code;
private String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 设置错误码
*
* @param code 错误码
* @return 返回当前枚举
*/
public ErrorCode setCode(String code) {
this.code = code;
return this;
}
/**
* 设置错误信息
*
* @param message 错误信息
* @return 返回当前枚举
*/
public ErrorCode setMessage(String message) {
this.message = message;
return this;
}
}

View File

@@ -0,0 +1,403 @@
package im.zhaojun.zfile.core.exception;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotRoleException;
import im.zhaojun.zfile.core.controller.FrontIndexController;
import im.zhaojun.zfile.core.exception.biz.*;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.core.ErrorPageBizException;
import im.zhaojun.zfile.core.exception.core.SystemException;
import im.zhaojun.zfile.core.exception.status.*;
import im.zhaojun.zfile.core.exception.system.UploadFileFailSystemException;
import im.zhaojun.zfile.core.exception.system.ZFileAuthorizationSystemException;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.RequestHolder;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.sqlite.SQLiteException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* 全局异常处理
*
* @author zhaojun
*/
@ControllerAdvice
@Slf4j
@Order(1)
public class GlobalExceptionHandler {
private static final ThreadLocal<String> exceptionMessage = new ThreadLocal<>();
@Resource
private SystemConfigService systemConfigService;
@Resource
private FrontIndexController frontIndexController;
private static final int MAX_FIND_CAUSE_EXCEPTION_DEPTH = 10;
// ---------------------- status exception start ----------------------
@ExceptionHandler(value = UnauthorizedAccessException.class)
@ResponseBody
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public AjaxJson<?> unauthorizedAccessException() {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getUnauthorizedResult();
}
try {
String unauthorizedUrl = systemConfigService.getUnauthorizedUrl();
RequestHolder.getResponse().sendRedirect(unauthorizedUrl);
} catch (IOException ex) {
return AjaxJson.getUnauthorizedResult();
}
return null;
}
@ExceptionHandler(value = {
NotRoleException.class
})
@ResponseBody
@ResponseStatus(HttpStatus.FORBIDDEN)
public AjaxJson<?> forbiddenAccessException() {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getForbiddenResult();
}
try {
String forbiddenUrl = systemConfigService.getForbiddenUrl();
RequestHolder.getResponse().sendRedirect(forbiddenUrl);
} catch (IOException ex) {
return AjaxJson.getForbiddenResult();
}
return null;
}
@ExceptionHandler(value = ForbiddenAccessException.class)
@ResponseBody
@ResponseStatus(HttpStatus.FORBIDDEN)
public AjaxJson<?> forbiddenAccessException(ForbiddenAccessException e) {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
try {
String forbiddenUrl = systemConfigService.getForbiddenUrl(e.getCode(), e.getMessage());
RequestHolder.getResponse().sendRedirect(forbiddenUrl);
} catch (IOException ex) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
return null;
}
@ExceptionHandler(value = NotFoundAccessException.class)
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public AjaxJson<?> notFoundAccessException(NotFoundAccessException e) {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
try {
String notFoundUrl = systemConfigService.getNotFoundUrl(e.getCode(), e.getMessage());
RequestHolder.getResponse().sendRedirect(notFoundUrl);
} catch (IOException ex) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
return null;
}
/**
* 所有未找到的页面都跳转到首页, 用户解决 vue history 直接访问 404 的问题
* 同时, 读取 index.html 文件, 修改 title 和 favicon 后返回.
*
* @return 转发到 /index.html
*/
@ExceptionHandler(value = NoResourceFoundException.class)
@ResponseBody
public String notFoundAccessException() {
return frontIndexController.redirect();
}
@ExceptionHandler(value = MethodNotAllowedAccessException.class)
@ResponseBody
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public AjaxJson<String> methodNotAllowedAccessException(MethodNotAllowedAccessException e) {
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = BadRequestAccessException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AjaxJson<String> badRequestAccessException(BadRequestAccessException e) {
return new AjaxJson<>(e.getCode(), e.getMessage());
}
// ---------------------- status exception end ----------------------
// ---------------------- biz exception start ----------------------
@ExceptionHandler(value = APIHttpRequestBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> apiHttpRequestBizException(APIHttpRequestBizException e) {
log.warn("请求第三方 API 异常, 请求地址: {}, 响应码: {}, 响应体: {}", e.getUrl(), e.getResponseCode(), e.getResponseBody());
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = FilePathSecurityBizException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public AjaxJson<String> filePathSecurityBizException(FilePathSecurityBizException e) {
log.warn("获取文件路径存在安全风险, 文件路径: {}", e.getPath());
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = GetPreviewTextContentBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> getPreviewTextContentBizException(GetPreviewTextContentBizException e) {
log.warn("获取预览文件内容失败, 文件 url: {}", e.getUrl(), e);
return new AjaxJson<>(e.getCode(), "预览文件内容失败, 请联系管理员.");
}
@ExceptionHandler(value = InitializeStorageSourceBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> initializeStorageSourceBizException(InitializeStorageSourceBizException e) {
log.error("存储源初始化失败, 存储源 ID: {}.", e.getStorageId(), e);
return new AjaxJson<>(e.getCode(), "存储源初始化失败:" + e.getMessage());
}
@ExceptionHandler(value = StorageSourceFileForbiddenAccessBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException e) {
log.warn("尝试访问不被授权的文件/目录, 存储源 ID: {}: 目录: {}", e.getStorageId(), e.getPath());
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = StorageSourceIllegalOperationBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException e) {
log.warn("存储源非法或未授权的操作, 存储源 ID: {}, 操作类型: {}", e.getStorageId(), e.getAction());
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = CorsBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> corsBizException(CorsBizException e) {
log.warn("跨域异常:", e);
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = ErrorPageBizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<?> errorPageBizException(ErrorPageBizException e) {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
try {
String errorPageUrl = systemConfigService.getErrorPageUrl(e.getCode(), e.getMessage());
RequestHolder.getResponse().sendRedirect(errorPageUrl);
} catch (IOException ex) {
return AjaxJson.getError(e.getCode(), e.getMessage());
}
return null;
}
@ExceptionHandler(value = BizException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> bizException(BizException e) {
return new AjaxJson<>(e.getCode(), e.getMessage());
}
// ---------------------- biz exception end ----------------------
// ---------------------- system exception end ----------------------
@ExceptionHandler(value = UploadFileFailSystemException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<String> uploadFileFailSystemException(UploadFileFailSystemException e) {
log.warn("上传文件失败, 存储类型: {}, 上传路径: {}, 输入流可用字节数: {}, 响应码: {}, 响应体: {}",
e.getStorageTypeEnum(), e.getUploadPath(), e.getInputStreamAvailable(), e.getResponseCode(), e.getResponseBody());
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = ZFileAuthorizationSystemException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<?> zfileAuthorizationSystemException(ZFileAuthorizationSystemException e) {
return new AjaxJson<>(e.getCode(), e.getMessage());
}
@ExceptionHandler(value = SystemException.class)
@ResponseBody
@ResponseStatus
public AjaxJson<?> systemException(SystemException e) {
return new AjaxJson<>(e.getCode(), e.getMessage());
}
// ---------------------- system exception end ----------------------
// ---------------------- common exception end ----------------------
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
@ResponseBody
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
public AjaxJson<Map<String, String>> handleValidException(Exception e) {
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
bindingResult = ((BindException) e).getBindingResult();
}
Map<String, String> errorMap = new HashMap<>(16);
Optional.ofNullable(bindingResult)
.map(BindingResult::getFieldErrors)
.ifPresent(fieldErrors -> {
for (FieldError fieldError : fieldErrors) {
errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
});
return new AjaxJson<>(ErrorCode.BIZ_BAD_REQUEST.getCode(), ErrorCode.BIZ_BAD_REQUEST.getMessage(), errorMap);
}
@ExceptionHandler({FileNotFoundException.class})
@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
public AjaxJson<Void> fileNotFound() {
return AjaxJson.getError("文件不存在");
}
/**
* 登录异常拦截器
*/
@ExceptionHandler(NotLoginException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public AjaxJson<?> handlerNotLoginException(NotLoginException e) {
if (RequestHolder.isAxiosRequest()) {
return AjaxJson.getUnauthorizedResult();
}
try {
String domain = systemConfigService.getRealFrontDomain();
if (StringUtils.isBlank(domain)) {
domain = "";
}
String loginUrl = StringUtils.concat(domain, "/login");
RequestHolder.getResponse().sendRedirect(loginUrl);
} catch (IOException ex) {
return AjaxJson.getUnauthorizedResult();
}
return null;
}
@ExceptionHandler
@ResponseBody
@ResponseStatus
public AjaxJson<?> extraExceptionHandler(Exception e) {
ExceptionType exceptionType = getExceptionType(e);
if (exceptionType == ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION) {
log.warn(e.getMessage());
} else if (exceptionType == ExceptionType.OTHER) {
log.error(e.getMessage(), e);
} else if (exceptionType == ExceptionType.SPECIFY_MESSAGE_EXCEPTION) {
if (exceptionMessage.get() != null) {
String message = exceptionMessage.get();
log.error("发生异常: {}", message,e );
exceptionMessage.remove();
return AjaxJson.getError(message);
}
}
if (e.getClass() == Exception.class) {
return AjaxJson.getError("系统异常, 请联系管理员");
} else {
return AjaxJson.getError(e.getMessage());
}
}
private static ExceptionType getExceptionType(Exception e) {
int findCauseCount = 0;
do {
if (e instanceof BizException) {
return ExceptionType.IGNORE_PRINT_STACK_TRACE_EXCEPTION;
} else if (e instanceof ClientAbortException) {
return ExceptionType.IGNORE_EXCEPTION;
} else if (e instanceof SQLiteException && e.getMessage().contains("database is locked")) {
exceptionMessage.set("数据库繁忙,请稍后再试");
return ExceptionType.SPECIFY_MESSAGE_EXCEPTION;
}
e = (Exception) e.getCause();
findCauseCount++;
} while (e != null && findCauseCount < MAX_FIND_CAUSE_EXCEPTION_DEPTH);
return ExceptionType.OTHER;
}
enum ExceptionType {
/**
* 忽略打印异常信息和堆栈信息
*/
IGNORE_EXCEPTION,
/**
* 仅打印异常信息, 不打印堆栈信息
*/
IGNORE_PRINT_STACK_TRACE_EXCEPTION,
/**
* 不打印堆栈信息,但指定异常信息
*/
SPECIFY_MESSAGE_EXCEPTION,
/**
* 其他异常, 打印异常信息和堆栈信息
*/
OTHER;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import lombok.Getter;
/**
* 请求第三方 API 时如果返回非 2xx 状态码, 则抛出此异常. 需记录请求地址, 响应状态码, 响应内容.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#apiHttpRequestBizException(APIHttpRequestBizException)}
*
* @author zhaojun
*/
@Getter
public class APIHttpRequestBizException extends BizException {
private final String url;
private final int responseCode;
private final String responseBody;
public APIHttpRequestBizException(ErrorCode errorCode, String url, int responseCode, String responseBody) {
super(errorCode);
this.url = url;
this.responseCode = responseCode;
this.responseBody = responseBody;
}
}

View File

@@ -0,0 +1,21 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.core.BizException;
import lombok.Getter;
/**
* @author zhaojun
*/
@Getter
public class CorsBizException extends BizException {
public CorsBizException(String message, Throwable cause) {
super(message, cause);
}
@Override
public boolean printExceptionStackTrace() {
return true;
}
}

View File

@@ -0,0 +1,26 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import lombok.Getter;
/**
* 文件路径安全异常, 表示文件路径不合法,如包含了 "./" 或 "../" 等字符来尝试访问非法目录.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#filePathSecurityBizException(FilePathSecurityBizException)}
*
* @author zhaojun
*/
@Getter
public class FilePathSecurityBizException extends BizException {
private final String path;
public FilePathSecurityBizException(String path) {
super(ErrorCode.BIZ_FILE_PATH_ILLEGAL);
this.path = path;
}
}

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import lombok.Getter;
/**
* 获取预览文件内容异常, 可能是目标连接无法访问/文件不存在等原因.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#getPreviewTextContentBizException(GetPreviewTextContentBizException)}
*
* @author zhaojun
*/
@Getter
public class GetPreviewTextContentBizException extends BizException {
/**
* 获取预览文件的 URL
*/
private final String url;
public GetPreviewTextContentBizException(String url, Throwable cause) {
super(cause);
this.url = url;
}
}

View File

@@ -0,0 +1,29 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.core.exception.core.BizException;
import lombok.Getter;
/**
* 初始化存储源时失败产生的异常
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#initializeStorageSourceBizException(InitializeStorageSourceBizException)}
*
* @author zhaojun
*/
@Getter
public class InitializeStorageSourceBizException extends BizException {
private final Integer storageId;
public InitializeStorageSourceBizException(String message, Integer storageId) {
super(message);
this.storageId = storageId;
}
public InitializeStorageSourceBizException(String code, String message, Integer storageId, Throwable cause) {
super(code, message, cause);
this.storageId = storageId;
}
}

View File

@@ -0,0 +1,31 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import lombok.Getter;
/**
* 不存在或初始化失败的存储源异常。
*
* @author zhaojun
*/
@Getter
public class InvalidStorageSourceBizException extends BizException {
private final Integer storageId;
private final String storageKey;
public InvalidStorageSourceBizException(String storageKey) {
super(ErrorCode.INVALID_STORAGE_SOURCE);
this.storageKey = storageKey;
this.storageId = null;
}
public InvalidStorageSourceBizException(Integer storageId) {
super(ErrorCode.INVALID_STORAGE_SOURCE);
this.storageId = storageId;
this.storageKey = null;
}
}

View File

@@ -0,0 +1,27 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import lombok.Getter;
/**
* 访问了禁止访问的存储源文件/目录时抛出此异常.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceFileForbiddenAccessBizException(StorageSourceFileForbiddenAccessBizException)}
*
* @author zhaojun
*/
@Getter
public class StorageSourceFileForbiddenAccessBizException extends BizException {
private final Integer storageId;
private final String path;
public StorageSourceFileForbiddenAccessBizException(Integer storageId, String path) {
super(ErrorCode.BIZ_STORAGE_SOURCE_FILE_FORBIDDEN);
this.storageId = storageId;
this.path = path;
}
}

View File

@@ -0,0 +1,28 @@
package im.zhaojun.zfile.core.exception.biz;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
import lombok.Getter;
/**
* 对存储源进行非法(未授权)的操作产生的异常
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#storageSourceIllegalOperationBizException(StorageSourceIllegalOperationBizException)}
*
* @author zhaojun
*/
@Getter
public class StorageSourceIllegalOperationBizException extends BizException {
private final Integer storageId;
private final FileOperatorTypeEnum action;
public StorageSourceIllegalOperationBizException(Integer storageId, FileOperatorTypeEnum action) {
super(ErrorCode.BIZ_STORAGE_SOURCE_ILLEGAL_OPERATION);
this.storageId = storageId;
this.action = action;
}
}

View File

@@ -0,0 +1,101 @@
package im.zhaojun.zfile.core.exception.core;
import im.zhaojun.zfile.core.exception.ErrorCode;
import lombok.Getter;
/**
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.
*
* @author zhaojun
*/
@Getter
public class BizException extends RuntimeException {
private static final long serialVersionUID = 8312907182931723379L;
/**
* 错误码
*/
private String code;
/**
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
*/
public boolean printExceptionStackTrace() {
return false;
}
/**
* 构造一个没有错误信息的 <code>SystemException</code>
*/
public BizException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public BizException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 SystemException
*
* @param message 错误信息
*/
public BizException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 SystemException
*
* @param code 错误码
* @param message 错误信息
*/
public BizException(String code, String message) {
super(message);
this.code = code;
}
/**
* 使用错误信息和 Throwable 构造 SystemException
*
* @param message 错误信息
* @param cause 错误原因
*/
public BizException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public BizException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* @param errorCode ErrorCode
*/
public BizException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public BizException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
}
}

View File

@@ -0,0 +1,103 @@
package im.zhaojun.zfile.core.exception.core;
import im.zhaojun.zfile.core.exception.ErrorCode;
import lombok.Getter;
/**
* 业务异常,该类异常用户可自行处理,无需记录日志,属于正常业务流程中的异常. 如: 用户名密码错误, 未登录等.<br>
* 使用该类的异常,当该异常被抛出时,会跳转到 500 错误页面(错误码和错误消息可被 {@link #code} 和 {@link #getMessage()} 覆盖),而不是返回 JSON 数据.<br>
* 一般使用该异常得请求不会是 AJAX 请求,而是直接在浏览器中访问的页面请求.
*
* @author zhaojun
*/
@Getter
public class ErrorPageBizException extends RuntimeException {
private static final long serialVersionUID = 8312907182931723379L;
/**
* 错误码
*/
private String code;
/**
* 是否打印堆栈信息,业务异常默认不打印堆栈信息,如果需要打印堆栈信息,可以通过子类覆盖该方法修改返回值为 true.
*/
public boolean printExceptionStackTrace() {
return false;
}
/**
* 构造一个没有错误信息的 <code>SystemException</code>
*/
public ErrorPageBizException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public ErrorPageBizException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 SystemException
*
* @param message 错误信息
*/
public ErrorPageBizException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 SystemException
*
* @param code 错误码
* @param message 错误信息
*/
public ErrorPageBizException(String code, String message) {
super(message);
this.code = code;
}
/**
* 使用错误信息和 Throwable 构造 SystemException
*
* @param message 错误信息
* @param cause 错误原因
*/
public ErrorPageBizException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public ErrorPageBizException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* @param errorCode ErrorCode
*/
public ErrorPageBizException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public ErrorPageBizException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
}
}

View File

@@ -0,0 +1,94 @@
package im.zhaojun.zfile.core.exception.core;
import im.zhaojun.zfile.core.exception.ErrorCode;
import lombok.Getter;
/**
* 系统异常, 该类异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 服务器异常等.
*
* @author zhaojun
*/
@Getter
public class SystemException extends RuntimeException {
private static final long serialVersionUID = 8312907182931723379L;
/**
* 错误码
*/
private String code;
/**
* 构造一个没有错误信息的 <code>SystemException</code>
*/
public SystemException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public SystemException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 SystemException
*
* @param message 错误信息
*/
public SystemException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 SystemException
*
* @param code 错误码
* @param message 错误信息
*/
public SystemException(String code, String message) {
super(message);
this.code = code;
}
/**
* 使用错误信息和 Throwable 构造 SystemException
*
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* @param errorCode ErrorCode
*/
public SystemException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public SystemException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
}
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1,18 @@
package im.zhaojun.zfile.core.exception.status;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.core.exception.core.BizException;
/**
* 错误请求异常, 表示请求参数有误或者服务器无法理解, 一般返回 400 状态码
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#badRequestAccessException(BadRequestAccessException)}
*
* @author zhaojun
*/
public class BadRequestAccessException extends BizException {
public BadRequestAccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,19 @@
package im.zhaojun.zfile.core.exception.status;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
/**
* 禁止访问异常, 表示用户没有权限访问该资源, 一般返回 403 状态码. (已经有身份,如果没有身份,应该是 UnauthorizedAccessException)
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#forbiddenAccessException}
*
* @author zhaojun
*/
public class ForbiddenAccessException extends BizException {
public ForbiddenAccessException(ErrorCode errorCode) {
super(errorCode);
}
}

View File

@@ -0,0 +1,19 @@
package im.zhaojun.zfile.core.exception.status;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.core.exception.core.BizException;
/**
* 错误请求异常, 表示请求方法有误或者服务器无法理解, 一般返回 405 状态码
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#methodNotAllowedAccessException(MethodNotAllowedAccessException)}
*
* @author zhaojun
*/
public class MethodNotAllowedAccessException extends BizException {
public MethodNotAllowedAccessException(ErrorCode errorCode) {
super(errorCode);
}
}

View File

@@ -0,0 +1,19 @@
package im.zhaojun.zfile.core.exception.status;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
/**
* 访问内容不存在异常, 表示用户请求的资源不存在时抛出, 一般返回 404 状态码.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#notFoundAccessException}
*
* @author zhaojun
*/
public class NotFoundAccessException extends BizException {
public NotFoundAccessException(ErrorCode errorCode) {
super(errorCode);
}
}

View File

@@ -0,0 +1,18 @@
package im.zhaojun.zfile.core.exception.status;
import im.zhaojun.zfile.core.exception.core.BizException;
/**
* 禁止访问异常, 表示用户未进行身份认证, 一般返回 401 状态码.
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link im.zhaojun.zfile.core.exception.GlobalExceptionHandler#unauthorizedAccessException}
*
* @author zhaojun
*/
public class UnauthorizedAccessException extends BizException {
public UnauthorizedAccessException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,39 @@
package im.zhaojun.zfile.core.exception.system;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.core.exception.core.SystemException;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import lombok.Getter;
/**
* 上传文件失败系统异常, 该异常用户无法处理,需要记录日志, 属于系统异常. 如: 网络异常, 目标存储源异常等
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#uploadFileFailSystemException(UploadFileFailSystemException)}
*
* @author zhaojun
*/
@Getter
public class UploadFileFailSystemException extends SystemException {
private final StorageTypeEnum storageTypeEnum;
private final String uploadPath;
private final Integer inputStreamAvailable;
private final int responseCode;
private final String responseBody;
public UploadFileFailSystemException(StorageTypeEnum storageTypeEnum, String uploadPath, Integer inputStreamAvailable, int responseCode, String responseBody) {
super(ErrorCode.BIZ_UPLOAD_FILE_ERROR);
this.storageTypeEnum = storageTypeEnum;
this.uploadPath = uploadPath;
this.inputStreamAvailable = inputStreamAvailable;
this.responseCode = responseCode;
this.responseBody = responseBody;
}
}

View File

@@ -0,0 +1,28 @@
package im.zhaojun.zfile.core.exception.system;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.GlobalExceptionHandler;
import im.zhaojun.zfile.core.exception.core.SystemException;
/**
* ZFile 授权异常
* <p/>
* 需要全局异常处理器捕获此异常, 并记录日志. {@link GlobalExceptionHandler#zfileAuthorizationSystemException(ZFileAuthorizationSystemException)}
*
* @author zhaojun
*/
public class ZFileAuthorizationSystemException extends SystemException {
public ZFileAuthorizationSystemException(String code, String message) {
super(code, message);
}
public ZFileAuthorizationSystemException(ErrorCode errorCode) {
super(errorCode);
}
public ZFileAuthorizationSystemException(ErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}

View File

@@ -1,18 +1,20 @@
package im.zhaojun.zfile.core.filter;
import cn.hutool.core.util.ObjectUtil;
import im.zhaojun.zfile.core.constant.ZFileHttpHeaderConstant;
import im.zhaojun.zfile.core.util.StringUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsUtils;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 开启跨域支持. 一般用于开发环境, 或前后端分离部署时开启.
@@ -20,16 +22,24 @@ import java.io.IOException;
* @author zhaojun
*/
@WebFilter(urlPatterns = "/*")
@Order(Integer.MIN_VALUE)
@Component
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (httpServletRequest.getRequestURI().equals("/favicon.ico")) {
return;
}
String header = httpServletRequest.getHeader(HttpHeaders.ORIGIN);
List<String> allowHeaders = Arrays.asList("Origin", "X-Requested-With", "Content-Type", "Accept", ZFileHttpHeaderConstant.ZFILE_TOKEN, ZFileHttpHeaderConstant.AXIOS_REQUEST, ZFileHttpHeaderConstant.AXIOS_FROM);
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, ObjectUtil.defaultIfNull(header, "*"));
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, X-Requested-With, Content-Type, Accept, zfile-token, axios-request");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, StringUtils.join(",", allowHeaders));
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "false");
httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "600");

View File

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

View File

@@ -0,0 +1,80 @@
package im.zhaojun.zfile.core.filter;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.core.util.matcher.IRuleMatcher;
import im.zhaojun.zfile.core.util.matcher.RuleMatcherFactory;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import java.io.IOException;
import java.util.List;
/**
* 检测访问的 IP 和 UA 是否符合系统安全设置中的规则
*
* @author zhaojun
*/
@WebFilter(urlPatterns = "/*")
public class SecurityFilter implements Filter {
private static volatile SystemConfigService systemConfigService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 双重检测锁, 防止多次初始化
if (systemConfigService == null) {
synchronized (this) {
if (systemConfigService == null) {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
}
}
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String accessIpBlocklist = systemConfig.getAccessIpBlocklist();
String accessUaBlocklist = systemConfig.getAccessUaBlocklist();
// 判断当前访问 IP 是否在黑名单中
String currentAccessIp = JakartaServletUtil.getClientIP(httpServletRequest);
if (StringUtils.isNotBlank(accessIpBlocklist) && checkIsDisableIP(accessIpBlocklist, currentAccessIp)) {
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpServletResponse.getWriter().write("disable access.[" + currentAccessIp + "]");
return;
}
// 判断当前访问 User-Agent 是否在黑名单中
String userAgent = httpServletRequest.getHeader(HttpHeaders.USER_AGENT);
if (StringUtils.isNotBlank(accessUaBlocklist) && checkIsDisableUA(accessUaBlocklist, userAgent)) {
httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
httpServletResponse.getWriter().write("disable access.[" + userAgent + "]");
return;
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private boolean checkIsDisableIP(String accessIpBlocklist, String currentAccessIp) {
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.IP);
List<String> ruleList = StringUtils.split(accessIpBlocklist, StringUtils.LF);
return ruleMatcher.matchAny(ruleList, currentAccessIp);
}
private boolean checkIsDisableUA(String accessUaBlocklist, String currentAccessUA) {
IRuleMatcher ruleMatcher = RuleMatcherFactory.getRuleMatcher(RuleTypeConstant.SPRING_SIMPLE);
List<String> ruleList = StringUtils.split(accessUaBlocklist, StringUtils.LF);
return ruleMatcher.matchAny(ruleList, currentAccessUA);
}
}

View File

@@ -1,22 +0,0 @@
package im.zhaojun.zfile.core.httpclient;
import im.zhaojun.zfile.core.httpclient.logging.HttpLoggingInterceptor;
import okhttp3.OkHttpClient;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
/**
* 自建 OkHTTP3 客户端工厂类, 增加日志输出拦截器.
* @author zhaojun
*/
public class ZFileOkHttp3ClientHttpRequestFactory extends OkHttp3ClientHttpRequestFactory {
public ZFileOkHttp3ClientHttpRequestFactory() {
// 使用 OkHttp3 作为底层请求库, 并设置重试机制和日志拦截器
super(new OkHttpClient()
.newBuilder()
.addNetworkInterceptor(new HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEBUG)
.setLevel(HttpLoggingInterceptor.Level.HEADERS))
.build());
}
}

View File

@@ -1,340 +0,0 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.zhaojun.zfile.core.httpclient.logging;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Connection;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http.HttpHeaders;
import okhttp3.internal.platform.Platform;
import okio.Buffer;
import okio.BufferedSource;
import okio.GzipSource;
import java.io.EOFException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import static okhttp3.internal.platform.Platform.INFO;
/**
* 此类代码来源于 logging-interceptor.
* <br>
* An OkHttp interceptor which logs request and response information. Can be applied as an
* {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain
* OkHttpClient#networkInterceptors() network interceptor}. <p> The format of the logs created by
* this class should not be considered stable and may change slightly between releases. If you need
* a stable logging format, use your own interceptor.
* <br>
* @author zhaojun
*/
@Slf4j
public final class HttpLoggingInterceptor implements Interceptor {
private static final Charset UTF8 = StandardCharsets.UTF_8;
public enum Level {
/** No logs. */
NONE,
/**
* Logs request and response lines.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1 (3-byte body)
*
* <-- 200 OK (22ms, 6-byte body)
* }</pre>
*/
BASIC,
/**
* Logs request and response lines and their respective headers.
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
* <-- END HTTP
* }</pre>
*/
HEADERS,
/**
* Logs request and response lines and their respective headers and bodies (if present).
*
* <p>Example:
* <pre>{@code
* --> POST /greeting http/1.1
* Host: example.com
* Content-Type: plain/text
* Content-Length: 3
*
* Hi?
* --> END POST
*
* <-- 200 OK (22ms)
* Content-Type: plain/text
* Content-Length: 6
*
* Hello!
* <-- END HTTP
* }</pre>
*/
BODY
}
public interface Logger {
void log(String message);
/** A {@link Logger} defaults output appropriate for the current platform. */
Logger DEFAULT = message -> Platform.get().log(message, INFO, null);
Logger DEBUG = log::debug;
Logger TRACE = log::trace;
}
public HttpLoggingInterceptor() {
this(Logger.DEFAULT);
}
public HttpLoggingInterceptor(Logger logger) {
this.logger = logger;
}
private final Logger logger;
private volatile Set<String> headersToRedact = Collections.emptySet();
public void redactHeader(String name) {
Set<String> newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newHeadersToRedact.addAll(headersToRedact);
newHeadersToRedact.add(name);
headersToRedact = newHeadersToRedact;
}
private volatile Level level = Level.NONE;
/** Change the level at which this interceptor logs. */
public HttpLoggingInterceptor setLevel(Level level) {
if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
this.level = level;
return this;
}
public Level getLevel() {
return level;
}
@Override public Response intercept(Chain chain) throws IOException {
Level level = this.level;
Request request = chain.request();
if (level == Level.NONE) {
return chain.proceed(request);
}
boolean logBody = level == Level.BODY;
boolean logHeaders = logBody || level == Level.HEADERS;
RequestBody requestBody = request.body();
boolean hasRequestBody = requestBody != null;
Connection connection = chain.connection();
String requestStartMessage = "--> "
+ request.method()
+ ' ' + request.url()
+ (connection != null ? " " + connection.protocol() : "");
if (!logHeaders && hasRequestBody) {
requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
}
logger.log(requestStartMessage);
if (logHeaders) {
if (hasRequestBody) {
// Request body headers are only present when installed as a network interceptor. Force
// them to be included (when available) so there values are known.
if (requestBody.contentType() != null) {
logger.log("Content-Type: " + requestBody.contentType());
}
if (requestBody.contentLength() != -1) {
logger.log("Content-Length: " + requestBody.contentLength());
}
}
Headers headers = request.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
String name = headers.name(i);
// Skip headers from the request body as they are explicitly logged above.
if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
logHeader(headers, i);
}
}
if (!logBody || !hasRequestBody) {
logger.log("--> END " + request.method());
} else if (bodyHasUnknownEncoding(request.headers())) {
logger.log("--> END " + request.method() + " (encoded body omitted)");
} else if (requestBody.isDuplex()) {
logger.log("--> END " + request.method() + " (duplex request body omitted)");
} else {
Buffer buffer = new Buffer();
requestBody.writeTo(buffer);
Charset charset = UTF8;
MediaType contentType = requestBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
logger.log("");
if (isPlaintext(buffer)) {
logger.log(buffer.readString(charset));
logger.log("--> END " + request.method()
+ " (" + requestBody.contentLength() + "-byte body)");
} else {
logger.log("--> END " + request.method() + " (binary "
+ requestBody.contentLength() + "-byte body omitted)");
}
}
}
long startNs = System.nanoTime();
Response response;
try {
response = chain.proceed(request);
} catch (Exception e) {
logger.log("<-- HTTP FAILED: " + e);
throw e;
}
long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
ResponseBody responseBody = response.body();
long contentLength = responseBody.contentLength();
String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
logger.log("<-- "
+ response.code()
+ (response.message().isEmpty() ? "" : ' ' + response.message())
+ ' ' + response.request().url()
+ " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
if (logHeaders) {
Headers headers = response.headers();
for (int i = 0, count = headers.size(); i < count; i++) {
logHeader(headers, i);
}
if (!logBody || !HttpHeaders.hasBody(response)) {
logger.log("<-- END HTTP");
} else if (bodyHasUnknownEncoding(response.headers())) {
logger.log("<-- END HTTP (encoded body omitted)");
} else {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE); // Buffer the entire body.
Buffer buffer = source.getBuffer();
Long gzippedLength = null;
if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
gzippedLength = buffer.size();
try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) {
buffer = new Buffer();
buffer.writeAll(gzippedResponseBody);
}
}
Charset charset = UTF8;
MediaType contentType = responseBody.contentType();
if (contentType != null) {
charset = contentType.charset(UTF8);
}
if (!isPlaintext(buffer)) {
logger.log("");
logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
return response;
}
if (contentLength != 0) {
logger.log("");
logger.log(buffer.clone().readString(charset));
}
if (gzippedLength != null) {
logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
+ gzippedLength + "-gzipped-byte body)");
} else {
logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
}
}
}
return response;
}
private void logHeader(Headers headers, int i) {
String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i);
logger.log(headers.name(i) + ": " + value);
}
/**
* Returns true if the body in question probably contains human readable text. Uses a small sample
* of code points to detect unicode control characters commonly used in binary file signatures.
*/
static boolean isPlaintext(Buffer buffer) {
try {
Buffer prefix = new Buffer();
long byteCount = buffer.size() < 64 ? buffer.size() : 64;
buffer.copyTo(prefix, 0, byteCount);
for (int i = 0; i < 16; i++) {
if (prefix.exhausted()) {
break;
}
int codePoint = prefix.readUtf8CodePoint();
if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
return false;
}
}
return true;
} catch (EOFException e) {
return false; // Truncated UTF-8 sequence.
}
}
private static boolean bodyHasUnknownEncoding(Headers headers) {
String contentEncoding = headers.get("Content-Encoding");
return contentEncoding != null
&& !contentEncoding.equalsIgnoreCase("identity")
&& !contentEncoding.equalsIgnoreCase("gzip");
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.zhaojun.zfile.core.io;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamResource;
import java.io.InputStream;
/**
*
* 自定义 EnsureContentLengthInputStreamResource 可以保证必须实现 InputStream 的 contentLength 方法返回实际的长度.
* 此类相较于 {@link org.springframework.core.io.InputStreamResource} 仅实现了 contentLength 方法.
* <br><br>
* {@link org.springframework.core.io.Resource} implementation for a given {@link InputStream}.
* <p>Should only be used if no other specific {@code Resource} implementation
* is applicable. In particular, prefer {@link ByteArrayResource} or any of the
* file-based {@code Resource} implementations where possible.
*
* <p>In contrast to other {@code Resource} implementations, this is a descriptor
* for an <i>already opened</i> resource - therefore returning {@code true} from
* {@link #isOpen()}. Do not use an {@code InputStreamResource} if you need to
* keep the resource descriptor somewhere, or if you need to read from a stream
* multiple times.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 28.12.2003
* @see ByteArrayResource
* @see org.springframework.core.io.ClassPathResource
* @see org.springframework.core.io.FileSystemResource
* @see org.springframework.core.io.UrlResource
*/
public class EnsureContentLengthInputStreamResource extends InputStreamResource {
private final long contentLength;
/**
* Create a new InputStreamResource.
* @param inputStream the InputStream to use
*/
public EnsureContentLengthInputStreamResource(InputStream inputStream, long contentLength) {
super(inputStream);
this.contentLength = contentLength;
}
@Override
public long contentLength() {
return contentLength;
}
}

View File

@@ -0,0 +1,91 @@
package im.zhaojun.zfile.core.io;
import com.google.common.util.concurrent.RateLimiter;
import jakarta.validation.constraints.NotNull;
import java.io.IOException;
import java.io.InputStream;
/**
* 使用装饰器模式, 限速输入流, 单位为字节/秒.
*
* @author zhaojun
*/
public final class ThrottledInputStream extends InputStream {
private final InputStream originalInputStream;
private final RateLimiter rateLimiter;
public ThrottledInputStream(InputStream originalInputStream, double bytesPerSecond) {
this.originalInputStream = originalInputStream;
this.rateLimiter = RateLimiter.create(bytesPerSecond);
}
@Override
public int read() throws IOException {
rateLimiter.acquire();
return originalInputStream.read();
}
@Override
public int read(@NotNull byte[] b) throws IOException {
return originalInputStream.read(b);
}
@Override
public int read(@NotNull byte[] b, int off, int len) throws IOException {
rateLimiter.acquire(len);
return originalInputStream.read(b, off, len);
}
@Override
public byte[] readAllBytes() throws IOException {
return originalInputStream.readAllBytes();
}
@Override
public byte[] readNBytes(int len) throws IOException {
return originalInputStream.readNBytes(len);
}
@Override
public int readNBytes(byte[] b, int off, int len) throws IOException {
return originalInputStream.readNBytes(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return originalInputStream.skip(n);
}
@Override
public void skipNBytes(long n) throws IOException {
originalInputStream.skipNBytes(n);
}
@Override
public int available() throws IOException {
return originalInputStream.available();
}
@Override
public void close() throws IOException {
originalInputStream.close();
}
@Override
public void mark(int readlimit) {
originalInputStream.mark(readlimit);
}
@Override
public void reset() throws IOException {
originalInputStream.reset();
}
@Override
public boolean markSupported() {
return originalInputStream.markSupported();
}
}

View File

@@ -0,0 +1,57 @@
package im.zhaojun.zfile.core.io;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.OutputStream;
/**
* 使用装饰器模式, 限速输出流, 单位为字节/秒.
*
* @author zhaojun
*/
@Slf4j
public final class ThrottledOutputStream extends OutputStream {
private final OutputStream originalOutputStream;
private final RateLimiter rateLimiter;
public ThrottledOutputStream(OutputStream out, double bytesPerSecond) {
this.originalOutputStream = out;
this.rateLimiter = RateLimiter.create(bytesPerSecond);
}
public void setRate(double bytesPerSecond) {
rateLimiter.setRate(bytesPerSecond);
}
@Override
public void write(int b) throws IOException {
rateLimiter.acquire();
originalOutputStream.write(b);
}
@Override
public void write(byte[] b) throws IOException {
rateLimiter.acquire(b.length);
originalOutputStream.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
rateLimiter.acquire(len);
originalOutputStream.write(b, off, len);
}
@Override
public void flush() throws IOException {
originalOutputStream.flush();
}
@Override
public void close() throws IOException {
originalOutputStream.close();
}
}

View File

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

View File

@@ -1,43 +0,0 @@
package im.zhaojun.zfile.core.service;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
* @author zhaojun
*/
@Service
public class DynamicControllerManager {
@Resource
private RequestMappingHandlerMapping requestMappingHandlerMapping;
private RequestMappingInfo shortLinkMappingInfo;
private Object shortLinkHandler;
private Method shortLinkMethod;
public void initDirectLinkPrefixPath(String path, Object handler, Method method) {
if (shortLinkMappingInfo != null) {
throw new RuntimeException("请勿重复初始化 DirectLinkPrefixPath.");
}
shortLinkMappingInfo = RequestMappingInfo.paths(path + "/{storageKey}/**").build();
shortLinkHandler = handler;
shortLinkMethod = method;
requestMappingHandlerMapping.registerMapping(shortLinkMappingInfo, handler, method);
}
public void changeDirectLinkPrefixPath(String path) {
if (shortLinkMappingInfo != null) {
requestMappingHandlerMapping.unregisterMapping(shortLinkMappingInfo);
}
shortLinkMappingInfo = RequestMappingInfo.paths(path + "/**").build();
requestMappingHandlerMapping.registerMapping(shortLinkMappingInfo, shortLinkHandler, shortLinkMethod);
}
}

View File

@@ -1 +1,107 @@
package im.zhaojun.zfile.core.util;
package im.zhaojun.zfile.core.util;
import im.zhaojun.zfile.core.exception.ErrorCode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;
/**
* ajax 请求返回 JSON 格式数据的封装
*
* @author zhaojun
*/
@Data
@ToString
public class AjaxJson<T> implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
public static final String CODE_SUCCESS = "0"; // 成功状态码
@Schema(name = "业务状态码0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
private final String code;
@Schema(name = "响应消息", example = "ok")
private String msg;
@Schema(name = "响应数据")
private T data;
@Schema(name = "数据总条数,分页情况有效")
private final Long dataCount;
@Schema(name = "跟踪 ID")
private String traceId;
public AjaxJson(String code, String msg) {
if (code == null) {
code = ErrorCode.SYSTEM_ERROR.getCode();
}
this.code = code;
this.msg = msg;
this.dataCount = null;
}
public AjaxJson(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = null;
}
public AjaxJson(String code, String msg, T data, Long dataCount) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = dataCount;
}
// 返回成功
public static AjaxJson<Void> getSuccess() {
return new AjaxJson<>(CODE_SUCCESS, "ok");
}
public static AjaxJson<Void> getSuccess(String msg) {
return new AjaxJson<>(CODE_SUCCESS, msg);
}
public static <T> AjaxJson<T> getSuccess(String msg, T data) {
return new AjaxJson<>(CODE_SUCCESS, msg, data);
}
public static <T> AjaxJson<T> getSuccessData(T data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data);
}
// 返回分页和数据的
public static <T> AjaxJson<T> getPageData(Long dataCount, T data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回错误
public static AjaxJson<Void> getError(String msg) {
return new AjaxJson<>(ErrorCode.SYSTEM_ERROR.getCode(), msg);
}
// 返回未登录
public static AjaxJson<?> getUnauthorizedResult() {
return new AjaxJson<>(ErrorCode.BIZ_UNAUTHORIZED.getCode(), "未登录,请登录后再次访问");
}
// 返回没权限的
public static AjaxJson<?> getForbiddenResult() {
return new AjaxJson<>(ErrorCode.NO_FORBIDDEN.getCode(), "未授权,请登录正确权限账号再试");
}
// 返回未找到的
public static AjaxJson<?> getNotFoundResult() {
return new AjaxJson<>(ErrorCode.BIZ_NOT_FOUND.getCode(), ErrorCode.BIZ_NOT_FOUND.getMessage());
}
public static AjaxJson<?> getError(String code, String msg) {
return new AjaxJson<>(code, msg);
}
}

View File

@@ -0,0 +1,40 @@
package im.zhaojun.zfile.core.util;
/**
* 数组工具类
*
* @author zhaojun
*/
public class ArrayUtils {
/**
* 数组是否为空
*
* @param <T>
* 数组元素类型
*
* @param array
* 数组
*
* @return 是否为空
*/
public static <T> boolean isEmpty(T[] array) {
return array == null || array.length == 0;
}
/**
* 数组是否不为空
*
* @param <T>
* 数组元素类型
*
* @param array
* 数组
*
* @return 是否不为空
*/
public static <T> boolean isNotEmpty(T[] array) {
return !isEmpty(array);
}
}

View File

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

View File

@@ -0,0 +1,727 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.text.StrSplitter;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.List;
/**
* 字符串工具类
*
* @author zhaojun
*/
public class CharSequenceUtil implements CharPool {
/**
* 找不到索引时的返回值
*/
public static final int INDEX_NOT_FOUND = -1;
/**
* 字符串常量:{@code "null"} <br>
* 注意:{@code "null" != null}
*/
public static final String NULL = "null";
/**
* 字符串常量:空字符串 {@code ""}
*/
public static final String EMPTY = "";
/**
* 字符串常量:空格符 {@code " "}
*/
public static final String SPACE = " ";
/**
* 获取 CharSequence 的长度, 如果为 null, 返回 0
*
* @param ch
* 要获取长度的 CharSequence, 可能为 null
*
* @return CharSequence 的长度
*/
public static int length(final @Nullable CharSequence ch) {
return ch == null ? 0 : ch.length();
}
/**
* {@link CharSequence} 转为字符串
*
* @param cs
* {@link CharSequence}
*
* @return 字符串
*/
public static String str(final @Nullable CharSequence cs) {
return null == cs ? null : cs.toString();
}
/**
* 判断 CharSequence 是否为空
*
* @param cs
* {@link CharSequence}
*
* @return 是否为空
*/
public static boolean isEmpty(final @Nullable CharSequence cs) {
return cs == null || cs.isEmpty();
}
/**
* CharSequence 是否不为空
*
* @param cs
* {@link CharSequence}
*
* @return 是否不为空
*/
public static boolean isNotEmpty(final @Nullable CharSequence cs) {
return !isEmpty(cs);
}
/**
* <p>指定字符串数组中的元素,是否全部为空字符串。</p>
* <p>如果指定的字符串数组的长度为 0或者所有元素都是空字符串则返回 true。</p>
* <br>
*
* <p>例:</p>
* <ul>
* <li>{@code CharSequenceUtil.isAllEmpty() // true}</li>
* <li>{@code CharSequenceUtil.isAllEmpty("", null) // true}</li>
* <li>{@code CharSequenceUtil.isAllEmpty("123", "") // false}</li>
* <li>{@code CharSequenceUtil.isAllEmpty("123", "abc") // false}</li>
* <li>{@code CharSequenceUtil.isAllEmpty(" ", "\t", "\n") // false}</li>
* </ul>
*
* @param strs
* 字符串列表
*
* @return 所有字符串是否都为空
*/
public static boolean isAllEmpty(final @Nullable CharSequence... strs) {
if (strs == null) {
return true;
}
for (CharSequence str : strs) {
if (isNotEmpty(str)) {
return false;
}
}
return true;
}
/**
* <p>是否包含空字符串。</p>
* <p>如果指定的字符串数组的长度为 0或者其中的任意一个元素是空字符串则返回 true。</p>
* <br>
*
* <p>例:</p>
* <ul>
* <li>{@code CharSequenceUtil.hasEmpty() // true}</li>
* <li>{@code CharSequenceUtil.hasEmpty("", null) // true}</li>
* <li>{@code CharSequenceUtil.hasEmpty("123", "") // true}</li>
* <li>{@code CharSequenceUtil.hasEmpty("123", "abc") // false}</li>
* <li>{@code CharSequenceUtil.hasEmpty(" ", "\t", "\n") // false}</li>
* </ul>
*
* @param strs
* 字符串列表
*
* @return 是否包含空字符串
*/
public static boolean hasEmpty(final @Nullable CharSequence... strs) {
if (ArrayUtils.isEmpty(strs)) {
return true;
}
for (CharSequence str : strs) {
if (isEmpty(str)) {
return true;
}
}
return false;
}
/**
* <p>指定字符串数组中的元素,是否都不为空字符串。</p>
* <p>如果指定的字符串数组的长度不为 0或者所有元素都不是空字符串则返回 true。</p>
* <br>
*
* <p>例:</p>
* <ul>
* <li>{@code CharSequenceUtil.isAllNotEmpty() // false}</li>
* <li>{@code CharSequenceUtil.isAllNotEmpty("", null) // false}</li>
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "") // false}</li>
* <li>{@code CharSequenceUtil.isAllNotEmpty("123", "abc") // true}</li>
* <li>{@code CharSequenceUtil.isAllNotEmpty(" ", "\t", "\n") // true}</li>
* </ul>
*
* @param args
* 字符串数组
*
* @return 所有字符串是否都不为为空白
*/
public static boolean isAllNotEmpty(final @Nullable CharSequence... args) {
return !hasEmpty(args);
}
/**
* 字符串是否为空白
*
* @param ch
* 要判断的字符串, 可能为 null
*
* @return 是否为空白
*/
public static boolean isBlank(final @Nullable CharSequence ch) {
final int strLen = ch == null ? 0 : ch.length();
if (strLen == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace(ch.charAt(i))) {
return false;
}
}
return true;
}
/**
* 字符串是否不为空白
*
* @param cs
* 字符串
*
* @return 是否不为空白
*/
public static boolean isNotBlank(final @Nullable CharSequence cs) {
return !isBlank(cs);
}
/**
* 比较两个 CharSequence 是否相等, 区分大小写, 如果两个都为 null, 返回 true
*
* @param cs1
* CharSequence 1, 可能为 null
*
* @param cs2
* CharSequence 2, 可能为 null
*
* @return 是否相等
*/
public static boolean equals(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
if (cs1 == cs2) {
return true;
}
if (cs1 == null || cs2 == null) {
return false;
}
if (cs1.length() != cs2.length()) {
return false;
}
if (cs1 instanceof String && cs2 instanceof String) {
return cs1.equals(cs2);
}
// 逐个比较
final int length = cs1.length();
for (int i = 0; i < length; i++) {
if (cs1.charAt(i) != cs2.charAt(i)) {
return false;
}
}
return true;
}
/**
* 比较两个 CharSequence 是否相等, 可以选择是否忽略大小写, 如果两个都为 null, 返回 true
*
* @param cs1
* 字符串 1
*
* @param cs2
* 字符串 2
*
* @param ignoreCase
* 是否忽略大小写
*
* @return 是否相等
*/
public static boolean equals(final @Nullable CharSequence cs1,final @Nullable CharSequence cs2, boolean ignoreCase) {
return ignoreCase ? equalsIgnoreCase(cs1, cs2) : equals(cs1, cs2);
}
/**
* 字符串是否相等, 忽略大小写
*
* @param cs1
* 字符串 1
*
* @param cs2
* 字符串 2
*
* @return 忽略大小写后是否相等
*/
public static boolean equalsIgnoreCase(final @Nullable CharSequence cs1, final @Nullable CharSequence cs2) {
if (cs1 == cs2) {
return true;
}
if (cs1 == null || cs2 == null) {
return false;
}
if (cs1.length() != cs2.length()) {
return false;
}
return cs1.toString().equalsIgnoreCase(cs2.toString());
}
/**
* 切分字符串,如果分隔符不存在则返回原字符串
*
* @param str
* 被切分的字符串
*
* @param separator
* 分隔符
*
* @return 字符串
*/
public static List<String> split(final CharSequence str, final CharSequence separator) {
return split(str, separator, false, false);
}
/**
* 切分字符串
*
* @param str
* 被切分的字符串
*
* @param separator
* 分隔符字符
*
* @param isTrim
* 是否去除切分字符串后每个元素两边的空格
*
* @param ignoreEmpty
* 是否忽略空串
*
* @return 切分后的集合
*/
public static List<String> split(CharSequence str, CharSequence separator, boolean isTrim, boolean ignoreEmpty) {
return split(str, separator, 0, isTrim, ignoreEmpty);
}
/**
* 切分字符串
*
* @param str
* 被切分的字符串
*
* @param separator
* 分隔符字符
*
* @param limit
* 限制分片数,-1 不限制
*
* @param isTrim
* 是否去除切分字符串后每个元素两边的空格
*
* @param ignoreEmpty
* 是否忽略空串
*
* @return 切分后的集合
*/
public static List<String> split(CharSequence str, CharSequence separator, int limit, boolean isTrim, boolean ignoreEmpty) {
final String separatorStr = (null == separator) ? null : separator.toString();
return StrSplitter.split(str, separatorStr, limit, isTrim, ignoreEmpty);
}
/**
* 指定字符串是否在字符串中出现过
*
* @param str
* 字符串
*
* @param searchStr
* 被查找的字符串
*
* @return 是否包含
*/
public static boolean contains(final @Nullable CharSequence str, final @Nullable CharSequence searchStr) {
if (null == str || null == searchStr) {
return false;
}
return str.toString().contains(searchStr);
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串
*
* @param str
* 指定字符串
*
* @param testStrs
* 需要检查的字符串数组
*
* @return 是否包含任意一个字符串
*/
public static boolean containsAny(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
if (isEmpty(str) || ArrayUtils.isEmpty(testStrs)) {
return false;
}
for (CharSequence checkStr : testStrs) {
if (null != checkStr && str.toString().contains(checkStr)) {
return true;
}
}
return false;
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串<br>
* 忽略大小写
*
* @param str
* 指定字符串
*
* @param testStrs
* 需要检查的字符串数组
*
* @return 是否包含任意一个字符串
*/
public static boolean containsAnyIgnoreCase(final @Nullable CharSequence str, final @Nullable CharSequence... testStrs) {
return StringUtils.containsAnyIgnoreCase(str, testStrs);
}
/**
* 以 conjunction 为分隔符将多个对象转换为字符串
*
* @param conjunction
* 分隔符
*
* @param objs
* 数组
*
* @return 连接后的字符串
*/
public static String join(CharSequence conjunction, Object... objs) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < objs.length; i++) {
Object item = objs[i];
sb.append(item);
if (i < objs.length - 1) {
sb.append(conjunction);
}
}
return sb.toString();
}
/**
* 以 conjunction 为分隔符将 Collection 对象转换为字符串
*
* @param conjunction
* 分隔符
*
* @param collection
* 集合
*
* @return 连接后的字符串
*/
public static String join(CharSequence conjunction, Collection<?> collection) {
StringBuilder sb = new StringBuilder();
for (Object item : collection) {
sb.append(item).append(conjunction);
}
if (!sb.isEmpty()) {
sb.delete(sb.length() - conjunction.length(), sb.length());
}
return sb.toString();
}
/**
* 是否以指定字符串开头
*
* @param str
* 被监测字符串
*
* @param prefix
* 开头字符串
*
* @return 是否以指定字符串开头
*/
public static boolean startWith(CharSequence str, CharSequence prefix) {
return startWith(str, prefix, false);
}
/**
* 是否以指定字符串开头,忽略大小写
*
* @param str
* 被监测字符串
*
* @param prefix
* 开头字符串
*
* @return 是否以指定字符串开头
*/
public static boolean startWithIgnoreCase(CharSequence str, CharSequence prefix) {
return startWith(str, prefix, true);
}
/**
* 是否以指定字符串开头<br>
* 如果给定的字符串和开头字符串都为null则返回true否则任意一个值为null返回false
*
* @param str
* 被监测字符串
*
* @param prefix
* 开头字符串
*
* @param ignoreCase
* 是否忽略大小写
*
* @return 是否以指定字符串开头
*/
public static boolean startWith(CharSequence str, CharSequence prefix, boolean ignoreCase) {
return startWith(str, prefix, ignoreCase, false);
}
/**
* 是否以指定字符串开头<br>
* 如果给定的字符串和开头字符串都为 null 则返回 true否则任意一个值为 null 返回 false<br>
* <pre>
* CharSequenceUtil.startWith("123", "123", false, true); -- false
* CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
* CharSequenceUtil.startWith("abc", "abc", true, true); -- false
* </pre>
*
* @param str
* 被监测字符串
*
* @param prefix
* 开头字符串
*
* @param ignoreCase
* 是否忽略大小写
*
* @param ignoreEquals
* 是否忽略字符串相等的情况
*
* @return 是否以指定字符串开头
*/
public static boolean startWith(final @Nullable CharSequence str, final @Nullable CharSequence prefix, boolean ignoreCase, boolean ignoreEquals) {
if (null == str || null == prefix) {
if (ignoreEquals) {
return false;
}
return null == str && null == prefix;
}
boolean isStartWith = str.toString()
.regionMatches(ignoreCase, 0, prefix.toString(), 0, prefix.length());
if (isStartWith) {
return (!ignoreEquals) || (!equals(str, prefix, ignoreCase));
}
return false;
}
/**
* 是否以指定字符串结尾
*
* @param str
* 被监测字符串
*
* @param suffix
* 结尾字符串
*
* @return 是否以指定字符串结尾
*/
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix) {
return endWith(str, suffix, false);
}
/**
* 是否以指定字符串结尾<br>
* 如果给定的字符串和开头字符串都为null则返回true否则任意一个值为null返回false
*
* @param str
* 被监测字符串
*
* @param suffix
* 结尾字符串
*
* @param ignoreCase
* 是否忽略大小写
*
* @return 是否以指定字符串结尾
*/
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase) {
return endWith(str, suffix, ignoreCase, false);
}
/**
* 是否以指定字符串结尾<br>
* 如果给定的字符串和开头字符串都为null则返回true否则任意一个值为null返回false
*
* @param str
* 被监测字符串
*
* @param suffix
* 结尾字符串
*
* @param ignoreCase
* 是否忽略大小写
*
* @param ignoreEquals
* 是否忽略字符串相等的情况
*
* @return 是否以指定字符串结尾
*/
public static boolean endWith(final @Nullable CharSequence str, final @Nullable CharSequence suffix, boolean ignoreCase, boolean ignoreEquals) {
if (null == str || null == suffix) {
if (ignoreEquals) {
return false;
}
return null == str && null == suffix;
}
final int strOffset = str.length() - suffix.length();
boolean isEndWith = str.toString()
.regionMatches(ignoreCase, strOffset, suffix.toString(), 0, suffix.length());
if (isEndWith) {
return (!ignoreEquals) || (!equals(str, suffix, ignoreCase));
}
return false;
}
/**
* 去掉指定前缀
*
* @param str
* 字符串
*
* @param prefix
* 前缀
*
* @return 切掉后的字符串,若前缀不是 preffix 返回原字符串
*/
public static String removePrefix(final @Nullable CharSequence str, final @Nullable CharSequence prefix) {
if (isEmpty(str) || isEmpty(prefix)) {
return str(str);
}
String str2 = str.toString();
String prefix2 = prefix.toString();
if (str2.startsWith(prefix2)) {
return str.subSequence(prefix.length(), str.length()).toString();
}
return str2; // 若前缀不是 prefix返回原字符串
}
/**
* 返回第一个非 {@code null} 元素
*
* @param strs
* 多个元素
*
* @param <T>
* 元素类型
*
* @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null}
*/
@SuppressWarnings("unchecked")
public static <T extends CharSequence> T firstNonNull(T... strs) {
if (ArrayUtils.isNotEmpty(strs)) {
for (T str : strs) {
if (isNotEmpty(str)) {
return str;
}
}
}
return null;
}
/**
* 截取分隔字符串之前的字符串,不包括分隔字符串<br>
* 如果给定的字符串为空串null或""或者分隔字符串为null返回原字符串<br>
* 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下:
*
* <pre>
* CharSequenceUtil.subBefore(null, *, false) = null
* CharSequenceUtil.subBefore("", *, false) = ""
* CharSequenceUtil.subBefore("abc", "a", false) = ""
* CharSequenceUtil.subBefore("abcba", "b", false) = "a"
* CharSequenceUtil.subBefore("abc", "c", false) = "ab"
* CharSequenceUtil.subBefore("abc", "d", false) = "abc"
* CharSequenceUtil.subBefore("abc", "", false) = ""
* CharSequenceUtil.subBefore("abc", null, false) = "abc"
* </pre>
*
* @param string
* 被查找的字符串
*
* @param separator
* 分隔字符串(不包括)
*
* @param isLastSeparator
* 是否查找最后一个分隔字符串多次出现分隔字符串时选取最后一个true为选取最后一个
*
* @return 切割后的字符串
*/
public static String subBefore(final @Nullable CharSequence string, final @Nullable CharSequence separator, boolean isLastSeparator) {
if (isEmpty(string) || separator == null) {
return null == string ? null : string.toString();
}
final String str = string.toString();
final String sep = separator.toString();
if (sep.isEmpty()) {
return EMPTY;
}
final int pos = isLastSeparator ? str.lastIndexOf(sep) : str.indexOf(sep);
if (INDEX_NOT_FOUND == pos) {
return str;
}
if (0 == pos) {
return EMPTY;
}
return str.substring(0, pos);
}
}

View File

@@ -1,5 +1,6 @@
package im.zhaojun.zfile.core.util;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -10,6 +11,14 @@ import java.lang.reflect.Type;
*/
public class ClassUtils {
public static Class<?> forName(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 获取指定类的泛型类型, 只获取第一个泛型类型
*
@@ -24,4 +33,9 @@ public class ClassUtils {
return (Class<?>) actualTypeArgument;
}
public static Class<?> getGenericType(Field field) {
ParameterizedType listType = (ParameterizedType) field.getGenericType();
return (Class<?>) listType.getActualTypeArguments()[0];
}
}

View File

@@ -1,76 +0,0 @@
package im.zhaojun.zfile.core.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author zhaojun
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class CodeMsg {
/**
* 错误码
* <p>
* 均为 5 位数, 如 00000, 10100, 20105 等.
* <br>
* 第一位表示错误类型, 4 为用户请求输入错误, 5 为服务端处理错误, 6 为警告信息
* <br>
* 第二位到第三位为二级类型
* <br>
* 第四位到第五位为具体错误代码, 根据业务场景自行定义
* <p>
* 以上三种类型均不允许重复, 且都需保持递增.
*/
private String code;
/**
* 错误消息
*/
private String msg;
// 通用返回值
public static CodeMsg SUCCESS = new CodeMsg("00000", "success");
public static CodeMsg BAD_REQUEST = new CodeMsg("40000", "非法请求");
public static CodeMsg ERROR = new CodeMsg("50000", "服务端异常");
// -------------- 用户输入级错误 --------------
public static CodeMsg REQUIRED_PASSWORD = new CodeMsg("40100", "请输入密码");
public static CodeMsg PASSWORD_FAULT = new CodeMsg("40101", "密码输入错误");
public static CodeMsg STORAGE_SOURCE_NOT_FOUND = new CodeMsg("40102", "无效的或初始化失败的存储源");
public static CodeMsg STORAGE_SOURCE_FORBIDDEN = new CodeMsg("40103", "无权访问存储源");
public static CodeMsg STORAGE_SOURCE_FILE_FORBIDDEN = new CodeMsg("40104", "无权访问该目录");
public static CodeMsg STORAGE_SOURCE_ILLEGAL_OPERATION = new CodeMsg("40105", "非法操作");
// -------------- 服务端处理错误 --------------
// 初始化相关错误
public static CodeMsg STORAGE_SOURCE_INIT_FAIL = new CodeMsg("50100", "初始化存储源失败");
public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_CONFIG_FAIL = new CodeMsg("50101", "初始化存储源参数失败");
public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_PARAM_FIELD_FAIL = new CodeMsg("50102", "填充存储源字段失败");
// 文件操作相关错误
public static CodeMsg STORAGE_SOURCE_FILE_NEW_FOLDER_FAIL = new CodeMsg("50201", "新建文件夹失败");
public static CodeMsg STORAGE_SOURCE_FILE_DELETE_FAIL = new CodeMsg("50202", "删除失败");
public static CodeMsg STORAGE_SOURCE_FILE_RENAME_FAIL = new CodeMsg("50203", "重命名失败");
public static CodeMsg STORAGE_SOURCE_FILE_GET_UPLOAD_FAIL = new CodeMsg("50204", "获取上传链接失败");
public static CodeMsg STORAGE_SOURCE_FILE_PROXY_UPLOAD_FAIL = new CodeMsg("50205", "文件上传失败");
public static CodeMsg STORAGE_SOURCE_FILE_PROXY_DOWNLOAD_FAIL = new CodeMsg("50206", "文件下载失败");
public static CodeMsg STORAGE_SOURCE_FILE_GET_ITEM_FAIL = new CodeMsg("50207", "文件不存在或请求异常");
public static CodeMsg STORAGE_SOURCE_FILE_DISABLE_PROXY_DOWNLOAD = new CodeMsg("50208", "非法操作, 当前文件不支持此类下载方式");
}

View File

@@ -0,0 +1,131 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.lang.func.Func1;
import javax.annotation.Nullable;
import java.util.*;
public class CollectionUtils {
/**
* 判断集合是否为空
*
* @param collection
* 集合
*
* @return 是否为空
*/
public static boolean isEmpty(@Nullable Collection<?> collection) {
return (collection == null || collection.isEmpty());
}
/**
* 判断集合是否不为空
*
* @param collection
* 集合
*
* @return 是否不为空
*/
public static boolean isNotEmpty(@Nullable Collection<?> collection) {
return !isEmpty(collection);
}
/**
* 从集合中获取第一个元素, 如果集合为空则返回 {@code null}
*
* @param list
* 集合,可能为 {@code null}
*
* @return 第一个元素,如果集合为空则返回 {@code null}
*/
@Nullable
public static <T> T getFirst(@Nullable List<T> list) {
if (isEmpty(list)) {
return null;
}
return list.get(0);
}
/**
* 从集合中获取最后一个元素, 如果集合为空则返回 {@code null}
*
* @param list
* 集合,可能为 {@code null}
*
* @return 最后一个元素,如果集合为空则返回 {@code null}
*/
@Nullable
public static <T> T getLast(@Nullable List<T> list) {
if (isEmpty(list)) {
return null;
}
return list.get(list.size() - 1);
}
/**
* 加入全部
*
* @param <T>
* 集合元素类型
*
* @param collection
* 被加入的集合 {@link Collection}
*
* @param values
* 要加入的内容数组
*
* @return 原集合
*/
public static <T> Collection<T> addAll(Collection<T> collection, T[] values) {
if (null != collection && null != values) {
Collections.addAll(collection, values);
}
return collection;
}
/**
* Iterable 转换为 Map, 根据指定的 keyFunc 函数生成 Key. Value 为 Iterable 中的元素.<br>
* 可以指定将结果放入的 Map, 如不指定则会新建一个 HashMap 返回.
*
* @param <K>
* Map Key 类型
*
* @param <V>
* Map Value 类型
*
* @param values
* 被转换的 Iterable
*
* @param map
* 转换后的 Value 存放的 Map, 如果为 {@code null} 则新建一个 HashMap
*
* @param keyFunc
* 生成 Map 的 Key 的函数
*
* @return 转换后的 Map
*/
public static <K, V> Map<K, V> toMap(final @Nullable Iterable<V> values, final @Nullable Map<K, V> map, final @Nullable Func1<V, K> keyFunc) {
if (values == null || keyFunc == null) {
return Collections.emptyMap();
}
final Map<K, V> result = map == null ? new HashMap<>() : map;
for (V value : values) {
try {
result.put(keyFunc.call(value), value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return result;
}
}

View File

@@ -0,0 +1,52 @@
package im.zhaojun.zfile.core.util;
import com.alibaba.dcm.DnsCacheManipulator;
import com.alibaba.fastjson2.JSONArray;
import org.springframework.lang.Nullable;
public class DnsUtil {
/**
* 通过 HTTP DNS 获取域名对应的 IP 地址
*
* @param domain
* 域名
*
* @return IP 地址数组
*/
public static @Nullable String[] getDomainIpByHttpDns(String domain) {
String jsonArrayStr = cn.hutool.http.HttpUtil.get("http://223.5.5.5/resolve?name=" + domain + "&short=1", 3000);
JSONArray jsonArray = JSONArray.parseArray(jsonArrayStr);
if (!jsonArray.isEmpty()) {
String[] result = new String[jsonArray.size()];
for (int i = 0; i < jsonArray.size(); i++) {
result[i] = jsonArray.getString(i);
}
return result;
} else {
return null;
}
}
/**
* 通过 HTTP DNS 获取域名对应的 IP 地址, 并设置 DNS 缓存.
*
* @param domain
* 域名
*
* @param cacheTime
* 缓存时间, 单位: 毫秒
*
* @return IP 地址数组
*/
public static String[] getDomainIpByHttpDnsAndCache(String domain, int cacheTime) {
String[] domainIpByHttpDns = getDomainIpByHttpDns(domain);
if (domainIpByHttpDns != null) {
// 设置 DNS 缓存
DnsCacheManipulator.setDnsCache(cacheTime, domain, domainIpByHttpDns);
}
return domainIpByHttpDns;
}
}

View File

@@ -8,12 +8,12 @@ import java.util.Comparator;
/**
* 文件比较器
*
* - 文件夹始终比文件排序高
* - 默认按照名称排序
* - 默认排序为升序
* - 按名称排序不区分大小写
*
* <ul>
* <li>文件夹始终比文件排序高</li>
* <li>默认按照名称排序</li>
* <li>默认排序为升序</li>
* <li>按名称排序不区分大小写</li>
* </ul>
* @author zhaojun
*/
public class FileComparator implements Comparator<FileItemResult> {
@@ -52,12 +52,11 @@ public class FileComparator implements Comparator<FileItemResult> {
FileTypeEnum o2Type = o2.getType();
NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();
if (o1Type.equals(o2Type)) {
int result;
switch (sortBy) {
case "time": result = CompareUtil.compare(o1.getTime(), o2.getTime()); break;
case "size": result = CompareUtil.compare(o1.getSize(), o2.getSize()); break;
default: result = naturalOrderComparator.compare(o1.getName(), o2.getName()); break;
}
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;
}

View File

@@ -1,18 +1,17 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
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.ByteArrayResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
/**
* 将文件输出对象
@@ -36,21 +35,18 @@ public class FileResponseUtil {
*/
public static ResponseEntity<Resource> exportSingleThread(File file, String fileName) {
if (!file.exists()) {
ByteArrayResource byteArrayResource = new ByteArrayResource("文件不存在或异常,请联系管理员.".getBytes(StandardCharsets.UTF_8));
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.body(byteArrayResource);
throw new NotFoundAccessException(ErrorCode.BIZ_FILE_NOT_EXIST);
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
if (StrUtil.isEmpty(fileName)) {
if (StringUtils.isEmpty(fileName)) {
fileName = file.getName();
}
headers.setContentDispositionFormData("attachment", StringUtils.encodeAllIgnoreSlashes(fileName));
headers.put(HttpHeaders.CONTENT_DISPOSITION, Collections.singletonList("inline; filename=\"" + StringUtils.encodeAllIgnoreSlashes(fileName) + "\""));
return ResponseEntity
.ok()

View File

@@ -0,0 +1,46 @@
package im.zhaojun.zfile.core.util;
import org.apache.commons.io.FilenameUtils;
/**
* 文件相关工具类
*
* @author zhaojun
*/
public class FileUtils {
public static String getName(final String fileName) {
if (fileName == null) {
return null;
}
int i = fileName.lastIndexOf(CharSequenceUtil.SLASH_CHAR);
if (i >= 0 && i <= fileName.length() - 1) {
return fileName.substring(i + 1);
}
return fileName;
}
public static String getParentPath(final String fileName) {
String fullPathNoEndSeparator = FilenameUtils.getFullPathNoEndSeparator(fileName);
if (fullPathNoEndSeparator == null || fullPathNoEndSeparator.isEmpty()) {
return StringUtils.SLASH;
}
return fullPathNoEndSeparator;
}
public static String getExtension(final String fileName) throws IllegalArgumentException {
if (fileName == null) {
return null;
}
int i = fileName.lastIndexOf('.');
if (i > 0 && i < fileName.length() - 1) {
return fileName.substring(i + 1);
}
return "";
}
}

View File

@@ -1,10 +1,9 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.core.constant.ZFileConstant;
import im.zhaojun.zfile.core.exception.PreviewException;
import im.zhaojun.zfile.core.exception.TextParseException;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.biz.GetPreviewTextContentBizException;
import im.zhaojun.zfile.core.exception.core.BizException;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@@ -19,7 +18,6 @@ import java.net.URLConnection;
@Slf4j
public class HttpUtil {
/**
* 获取 URL 对应的文件内容
*
@@ -32,14 +30,14 @@ public class HttpUtil {
long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB;
if (getRemoteFileSize(url) > maxFileSize) {
throw new PreviewException("预览文件超出大小, 最大支持 " + FileUtil.readableFileSize(maxFileSize));
throw new BizException(ErrorCode.BIZ_PREVIEW_FILE_SIZE_EXCEED);
}
String result;
try {
result = cn.hutool.http.HttpUtil.get(url);
} catch (Exception e) {
throw new TextParseException(StrUtil.format("获取文件内容失败, URL: {}", url), e);
throw new GetPreviewTextContentBizException(url, e);
}
return result == null ? "" : result;

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