ruleList = StringUtils.split(accessUaBlocklist, StringUtils.LF);
return ruleMatcher.matchAny(ruleList, currentAccessUA);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/io/EnsureContentLengthInputStreamResource.java
================================================
/*
* 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 方法.
*
* {@link org.springframework.core.io.Resource} implementation for a given {@link InputStream}.
* 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.
*
*
In contrast to other {@code Resource} implementations, this is a descriptor
* for an already opened 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;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java
================================================
package im.zhaojun.zfile.core.model.request;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Objects;
/**
* 通用分页请求对象,可继承该类增加业务字段.
*
* @author zhaojun
*/
@Data
public class PageQueryRequest {
@Schema(title="分页页数")
private Integer page = 1;
@Schema(title="每页条数")
private Integer limit = 10;
@Schema(title="排序字段")
private String orderBy = "create_date";
@Schema(title="排序顺序")
private String orderDirection = "desc";
public OrderItem getOrderItem() {
boolean asc = Objects.equals(orderDirection, "asc");
return asc ? OrderItem.asc(orderBy) : OrderItem.desc(orderBy);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
================================================
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 implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
public static final String CODE_SUCCESS = "0"; // 成功状态码
@Schema(title = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
private final String code;
@Schema(title = "响应消息", example = "ok")
private String msg;
@Schema(title = "响应数据")
private T data;
@Schema(title = "数据总条数,分页情况有效")
private final Long dataCount;
@Schema(title = "跟踪 ID")
private String traceId;
public AjaxJson(String code, String msg) {
if (code == null) {
code = ErrorCode.SYSTEM_ERROR.getCode();
}
this.code = code;
this.msg = msg;
this.dataCount = null;
}
public AjaxJson(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = null;
}
public AjaxJson(String code, String msg, T data, Long dataCount) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = dataCount;
}
// 返回成功
public static AjaxJson getSuccess() {
return new AjaxJson<>(CODE_SUCCESS, "ok");
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson<>(CODE_SUCCESS, msg);
}
public static AjaxJson getSuccess(String msg, T data) {
return new AjaxJson<>(CODE_SUCCESS, msg, data);
}
public static AjaxJson getSuccessData(T data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data);
}
// 返回分页和数据的
public static AjaxJson getPageData(Long dataCount, T data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回错误
public static AjaxJson 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);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java
================================================
package im.zhaojun.zfile.core.util;
/**
* 数组工具类
*
* @author zhaojun
*/
public class ArrayUtils {
/**
* 数组是否为空
*
* @param
* 数组元素类型
*
* @param array
* 数组
*
* @return 是否为空
*/
public static boolean isEmpty(T[] array) {
return array == null || array.length == 0;
}
/**
* 数组是否不为空
*
* @param
* 数组元素类型
*
* @param array
* 数组
*
* @return 是否不为空
*/
public static boolean isNotEmpty(T[] array) {
return !isEmpty(array);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/CharPool.java
================================================
package im.zhaojun.zfile.core.util;
public interface CharPool {
/**
* CHAR 常量:斜杠 {@code '/'} ASCII 47
*/
char SLASH_CHAR = '/';
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java
================================================
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"}
* 注意:{@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);
}
/**
* 指定字符串数组中的元素,是否全部为空字符串。
* 如果指定的字符串数组的长度为 0,或者所有元素都是空字符串,则返回 true。
*
*
* 例:
*
* - {@code CharSequenceUtil.isAllEmpty() // true}
* - {@code CharSequenceUtil.isAllEmpty("", null) // true}
* - {@code CharSequenceUtil.isAllEmpty("123", "") // false}
* - {@code CharSequenceUtil.isAllEmpty("123", "abc") // false}
* - {@code CharSequenceUtil.isAllEmpty(" ", "\t", "\n") // false}
*
*
* @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;
}
/**
* 是否包含空字符串。
* 如果指定的字符串数组的长度为 0,或者其中的任意一个元素是空字符串,则返回 true。
*
*
* 例:
*
* - {@code CharSequenceUtil.hasEmpty() // true}
* - {@code CharSequenceUtil.hasEmpty("", null) // true}
* - {@code CharSequenceUtil.hasEmpty("123", "") // true}
* - {@code CharSequenceUtil.hasEmpty("123", "abc") // false}
* - {@code CharSequenceUtil.hasEmpty(" ", "\t", "\n") // false}
*
*
* @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;
}
/**
* 指定字符串数组中的元素,是否都不为空字符串。
* 如果指定的字符串数组的长度不为 0,或者所有元素都不是空字符串,则返回 true。
*
*
* 例:
*
* - {@code CharSequenceUtil.isAllNotEmpty() // false}
* - {@code CharSequenceUtil.isAllNotEmpty("", null) // false}
* - {@code CharSequenceUtil.isAllNotEmpty("123", "") // false}
* - {@code CharSequenceUtil.isAllNotEmpty("123", "abc") // true}
* - {@code CharSequenceUtil.isAllNotEmpty(" ", "\t", "\n") // true}
*
*
* @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 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 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 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;
}
/**
* 查找指定字符串是否包含指定字符串列表中的任意一个字符串
* 忽略大小写
*
* @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);
}
/**
* 是否以指定字符串开头
* 如果给定的字符串和开头字符串都为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);
}
/**
* 是否以指定字符串开头
* 如果给定的字符串和开头字符串都为 null 则返回 true,否则任意一个值为 null 返回 false
*
* CharSequenceUtil.startWith("123", "123", false, true); -- false
* CharSequenceUtil.startWith("ABCDEF", "abc", true, true); -- true
* CharSequenceUtil.startWith("abc", "abc", true, true); -- false
*
*
* @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);
}
/**
* 是否以指定字符串结尾
* 如果给定的字符串和开头字符串都为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);
}
/**
* 是否以指定字符串结尾
* 如果给定的字符串和开头字符串都为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
* 元素类型
*
* @return 第一个非空元素,如果给定的数组为空或者都为空,返回{@code null}
*/
@SuppressWarnings("unchecked")
public static T firstNonNull(T... strs) {
if (ArrayUtils.isNotEmpty(strs)) {
for (T str : strs) {
if (isNotEmpty(str)) {
return str;
}
}
}
return null;
}
/**
* 截取分隔字符串之前的字符串,不包括分隔字符串
* 如果给定的字符串为空串(null或"")或者分隔字符串为null,返回原字符串
* 如果分隔字符串为空串"",则返回空串,如果分隔字符串未找到,返回原字符串,举例如下:
*
*
* 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"
*
*
* @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);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
================================================
package im.zhaojun.zfile.core.util;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* Class & 反射相关工具类
*
* @author zhaojun
*/
public class ClassUtils {
public static Class> forName(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 获取指定类的泛型类型, 只获取第一个泛型类型
*
* @param clazz
* 泛型类
*
* @return 泛型类型
*/
public static Class> getClassFirstGenericsParam(Class> clazz) {
Type genericSuperclass = clazz.getGenericSuperclass();
Type actualTypeArgument = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
return (Class>) actualTypeArgument;
}
public static Class> getGenericType(Field field) {
ParameterizedType listType = (ParameterizedType) field.getGenericType();
return (Class>) listType.getActualTypeArguments()[0];
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java
================================================
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 getFirst(@Nullable List list) {
if (isEmpty(list)) {
return null;
}
return list.get(0);
}
/**
* 从集合中获取最后一个元素, 如果集合为空则返回 {@code null}
*
* @param list
* 集合,可能为 {@code null}
*
* @return 最后一个元素,如果集合为空则返回 {@code null}
*/
@Nullable
public static T getLast(@Nullable List list) {
if (isEmpty(list)) {
return null;
}
return list.get(list.size() - 1);
}
/**
* 加入全部
*
* @param
* 集合元素类型
*
* @param collection
* 被加入的集合 {@link Collection}
*
* @param values
* 要加入的内容数组
*
* @return 原集合
*/
public static Collection addAll(Collection collection, T[] values) {
if (null != collection && null != values) {
Collections.addAll(collection, values);
}
return collection;
}
/**
* Iterable 转换为 Map, 根据指定的 keyFunc 函数生成 Key. Value 为 Iterable 中的元素.
* 可以指定将结果放入的 Map, 如不指定则会新建一个 HashMap 返回.
*
* @param
* Map Key 类型
*
* @param
* Map Value 类型
*
* @param values
* 被转换的 Iterable
*
* @param map
* 转换后的 Value 存放的 Map, 如果为 {@code null} 则新建一个 HashMap
*
* @param keyFunc
* 生成 Map 的 Key 的函数
*
* @return 转换后的 Map
*/
public static Map toMap(final @Nullable Iterable values, final @Nullable Map map, final @Nullable Func1 keyFunc) {
if (values == null || keyFunc == null) {
return Collections.emptyMap();
}
final Map 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;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java
================================================
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;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/EnumConvertUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import java.lang.reflect.Field;
/**
* 枚举转换工具类
*
* @author zhaojun
*/
public class EnumConvertUtils {
/**
* 根据枚举 class 和值获取对应的枚举对象
*
* @param clazz
* 枚举类 Class
*
* @param value
* 枚举值
*
* @return 枚举对象
*/
public static Enum> convertStrToEnum(Class> clazz, Object value) {
if (!ClassUtil.isEnum(clazz)) {
return null;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
if (jsonValuePresent || enumValuePresent) {
Object[] enumConstants = clazz.getEnumConstants();
for (Object enumObj : enumConstants) {
if (ObjectUtil.equal(value, ReflectUtil.getFieldValue(enumObj, field))) {
return (Enum>) enumObj;
}
}
}
}
return null;
}
/**
* 转换枚举对象为字符串, 如果枚举对象没有定义 JsonValue 注解, 则使用 EnumValue 注解的值
*
* @param enumObj
* 枚举对象
*
* @return 字符串
*/
public static String convertEnumToStr(Object enumObj) {
Class> clazz = enumObj.getClass();
if (!ClassUtil.isEnum(clazz)) {
return null;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
boolean jsonValuePresent = field.isAnnotationPresent(JsonValue.class);
boolean enumValuePresent = field.isAnnotationPresent(EnumValue.class);
if (jsonValuePresent || enumValuePresent) {
return Convert.toStr(ReflectUtil.getFieldValue(enumObj, field));
}
}
return null;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.comparator.CompareUtil;
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
import im.zhaojun.zfile.module.storage.model.enums.FileTypeEnum;
import java.util.Comparator;
/**
* 文件比较器
*
* - 文件夹始终比文件排序高
* - 默认按照名称排序
* - 默认排序为升序
* - 按名称排序不区分大小写
*
* @author zhaojun
*/
public class FileComparator implements Comparator {
private String sortBy;
private String order;
public FileComparator(String sortBy, String order) {
this.sortBy = sortBy;
this.order = order;
}
/**
* 比较两个文件的大小
*
* @param o1
* 第一个文件
*
* @param o2
* 第二个文件
*
* @return 比较结果
*/
@Override
public int compare(FileItemResult o1, FileItemResult o2) {
if (sortBy == null) {
sortBy = "name";
}
if (order == null) {
order = "asc";
}
FileTypeEnum o1Type = o1.getType();
FileTypeEnum o2Type = o2.getType();
NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();
if (o1Type.equals(o2Type)) {
int result = switch (sortBy) {
case "time" -> CompareUtil.compare(o1.getTime(), o2.getTime());
case "size" -> CompareUtil.compare(o1.getSize(), o2.getSize());
default -> naturalOrderComparator.compare(o1.getName(), o2.getName());
};
return "asc".equals(order) ? result : -result;
}
if (o1Type.equals(FileTypeEnum.FOLDER)) {
return -1;
} else {
return 1;
}
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.status.NotFoundAccessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.File;
import java.nio.charset.StandardCharsets;
/**
* 将文件输出对象
*
* @author zhaojun
*/
@Slf4j
public class FileResponseUtil {
/**
* 文件下载,单线程,不支持断点续传
*
* @param file
* 文件对象
*
* @param fileName
* 要保存为的文件名
*
* @return 文件下载对象
*/
public static ResponseEntity exportSingleThread(File file, String fileName) {
if (!file.exists()) {
throw new NotFoundAccessException(ErrorCode.BIZ_FILE_NOT_EXIST);
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
if (StringUtils.isEmpty(fileName)) {
fileName = file.getName();
}
ContentDisposition contentDisposition = ContentDisposition
.builder("inline")
.filename(fileName, StandardCharsets.UTF_8)
.build();
headers.setContentDisposition(contentDisposition);
return ResponseEntity
.ok()
.headers(headers)
.contentLength(file.length())
.contentType(mediaType)
.body(new InputStreamResource(FileUtil.getInputStream(file)));
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/FileSizeConverter.java
================================================
package im.zhaojun.zfile.core.util;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FileSizeConverter {
private static final long KB_FACTOR = 1024L;
private static final long MB_FACTOR = 1024L * KB_FACTOR;
private static final long GB_FACTOR = 1024L * MB_FACTOR;
private static final long TB_FACTOR = 1024L * GB_FACTOR;
private static final long PB_FACTOR = 1024L * TB_FACTOR;
private static final Pattern FILE_SIZE_PATTERN = Pattern.compile("([\\d.]+)\\s*([a-zA-Z]+)");
public static long convertFileSizeToBytes(String sizeStr) {
if (sizeStr == null || sizeStr.trim().isEmpty()) {
throw new IllegalArgumentException("输入字符串不能为空");
}
Matcher matcher = FILE_SIZE_PATTERN.matcher(sizeStr.trim());
if (!matcher.matches()) {
throw new IllegalArgumentException("无效的文件大小格式: " + sizeStr);
}
String valueStr = matcher.group(1);
String unitStr = matcher.group(2).toUpperCase();
double value;
try {
value = Double.parseDouble(valueStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("无效的数字格式: " + valueStr, e);
}
if (value < 0) {
throw new IllegalArgumentException("文件大小不能为负数: " + valueStr);
}
long multiplier = switch (unitStr) {
case "B" ->
1L;
case "KB", "KIB" ->
KB_FACTOR;
case "MB", "MIB" ->
MB_FACTOR;
case "GB", "GIB" ->
GB_FACTOR;
case "TB", "TIB" ->
TB_FACTOR;
case "PB", "PIB" ->
PB_FACTOR;
default -> throw new IllegalArgumentException("不支持的单位: " + unitStr + " (支持 B, KB, MB, GB, TB, PB)");
};
double bytesDouble = value * multiplier;
if (bytesDouble > Long.MAX_VALUE) {
throw new ArithmeticException("转换后的字节数超过了 Long 类型的最大值: " + bytesDouble);
}
return Math.round(bytesDouble);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/FileUtils.java
================================================
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(StringUtils.trimEndSlashes(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 "";
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java
================================================
package im.zhaojun.zfile.core.util;
import im.zhaojun.zfile.core.constant.ZFileConstant;
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;
import java.net.URL;
import java.net.URLConnection;
/**
* 网络相关工具
*
* @author zhaojun
*/
@Slf4j
public class HttpUtil {
/**
* 获取 URL 对应的文件内容
*
* @param url
* 文件 URL
*
* @return 文件内容
*/
public static String getTextContent(String url) {
long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB;
if (getRemoteFileSize(url) > 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 GetPreviewTextContentBizException(url, e);
}
return result == null ? "" : result;
}
/**
* 获取远程文件大小
*
* @param url
* 文件 URL
*
* @return 文件大小
*/
public static Long getRemoteFileSize(String url) {
long size = 0;
URL urlObject;
try {
urlObject = new URL(url);
URLConnection conn = urlObject.openConnection();
size = conn.getContentLength();
} catch (IOException e) {
e.printStackTrace();
}
return size;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/NaturalOrderComparator.java
================================================
package im.zhaojun.zfile.core.util;
/*
NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java.
Copyright (C) 2003 by Pierre-Luc Paour
Based on the C version by Martin Pool, of which this is more or less a straight conversion.
Copyright (C) 2000 by Martin Pool
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
*/
import java.util.Comparator;
/**
* 类 windows 文件排序算法
*
* @author zhaojun
*/
public class NaturalOrderComparator implements Comparator {
private static final char ZERO_CHAR = '0';
private int compareRight(String a, String b) {
int bias = 0, ia = 0, ib = 0;
// The longest run of digits wins. That aside, the greatest
// value wins, but we can't know that it will until we've scanned
// both numbers to know that they have the same magnitude, so we
// remember it in BIAS.
for (; ; ia++, ib++) {
char ca = charAt(a, ia);
char cb = charAt(b, ib);
if (!isDigit(ca) && !isDigit(cb)) {
return bias;
}
if (!isDigit(ca)) {
return -1;
}
if (!isDigit(cb)) {
return +1;
}
if (ca == 0 && cb == 0) {
return bias;
}
if (bias == 0) {
if (ca < cb) {
bias = -1;
} else if (ca > cb) {
bias = +1;
}
}
}
}
@Override
public int compare(String a, String b) {
int ia = 0, ib = 0;
int nza, nzb;
char ca, cb;
while (true) {
// Only count the number of zeroes leading the last number compared
nza = nzb = 0;
ca = charAt(a, ia);
cb = charAt(b, ib);
// skip over leading spaces or zeros
while (Character.isSpaceChar(ca) || ca == ZERO_CHAR) {
if (ca == ZERO_CHAR) {
nza++;
} else {
// Only count consecutive zeroes
nza = 0;
}
ca = charAt(a, ++ia);
}
while (Character.isSpaceChar(cb) || cb == '0') {
if (cb == '0') {
nzb++;
} else {
// Only count consecutive zeroes
nzb = 0;
}
cb = charAt(b, ++ib);
}
// Process run of digits
if (Character.isDigit(ca) && Character.isDigit(cb)) {
int bias = compareRight(a.substring(ia), b.substring(ib));
if (bias != 0) {
return bias;
}
}
if (ca == 0 && cb == 0) {
// The strings compare the same. Perhaps the caller
// will want to call strcmp to break the tie.
return compareEqual(a, b, nza, nzb);
}
if (ca < cb) {
return -1;
}
if (ca > cb) {
return +1;
}
++ia;
++ib;
}
}
private static boolean isDigit(char c) {
return Character.isDigit(c) || c == '.' || c == ',';
}
private static char charAt(String s, int i) {
return i >= s.length() ? 0 : s.charAt(i);
}
private static int compareEqual(String a, String b, int nza, int nzb) {
if (nza - nzb != 0) {
return nza - nzb;
}
if (a.length() == b.length()) {
return a.compareTo(b);
}
return a.length() - b.length();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/NumberUtils.java
================================================
package im.zhaojun.zfile.core.util;
/**
* 数字工具类
*
* @author zhaojun
*/
public class NumberUtils {
public static boolean isNullOrZero(Integer number) {
return number == null || number == 0;
}
public static boolean isNotNullOrZero(Integer number) {
return number != null && number != 0;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/OnlyOfficeKeyCacheUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.CacheObj;
import im.zhaojun.zfile.module.onlyoffice.model.OnlyOfficeFile;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* OnlyOffice 文件信息与 Key 缓存工具类
*
* @author zhaojun
*/
@Slf4j
public class OnlyOfficeKeyCacheUtils {
/**
* 存储 OnlyOffice 文件信息与 Key 的映射关系. 最多存储 10000 个 Key, 防止内存溢出.
*/
private static final Cache ONLY_OFFICE_FILE_KEY_MAP = CacheUtil.newLRUCache(10000);
/**
* 存储 OnlyOffice Key 与文件信息的映射关系. 最多存储 10000 个 Key, 防止内存溢出.
*/
private static final Cache ONLY_OFFICE_KEY_FILE_MAP = CacheUtil.newLRUCache(10000);
/**
* 存储文件锁, 防止并发操作文件缓存时出现问题.
*/
private static final Cache locks = CacheUtil.newLRUCache(300);
/**
* 获取该文件缓存的 key, 如果不存在则生成一个新的 key 并缓存.
*
* @param onlyOfficeFile
* OnlyOffice 文件信息
*
* @return 该文件唯一标识
*/
public static String getKeyOrPutNew(OnlyOfficeFile onlyOfficeFile, long timeout) {
ReentrantLock lock = getLock(onlyOfficeFile);
try {
boolean getLock = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
if (BooleanUtils.isFalse(getLock)) {
log.warn("{} 尝试获取锁超时, 强制忽略锁直接操作文件.", onlyOfficeFile);
}
try {
if (ONLY_OFFICE_FILE_KEY_MAP.containsKey(onlyOfficeFile)) {
return ONLY_OFFICE_FILE_KEY_MAP.get(onlyOfficeFile);
} else {
String key = RandomStringUtils.randomAlphabetic(10);
ONLY_OFFICE_FILE_KEY_MAP.put(onlyOfficeFile, key);
ONLY_OFFICE_KEY_FILE_MAP.put(key, onlyOfficeFile);
return key;
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Thread was interrupted", e);
}
}
/**
* 清理缓存中的 Key 与文件信息的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用)
*
* @param key
* 文件唯一标识
*/
public static OnlyOfficeFile removeByKey(String key) {
OnlyOfficeFile onlyOfficeFile = ONLY_OFFICE_KEY_FILE_MAP.get(key);
if (onlyOfficeFile == null) {
return null;
}
ONLY_OFFICE_FILE_KEY_MAP.remove(onlyOfficeFile);
ONLY_OFFICE_KEY_FILE_MAP.remove(key);
return onlyOfficeFile;
}
/**
* 清理缓存中的文件信息与 Key 的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用)
*
* @param onlyOfficeFile
* OnlyOffice 文件信息
*/
public static OnlyOfficeFile removeByFile(OnlyOfficeFile onlyOfficeFile) {
String key = ONLY_OFFICE_FILE_KEY_MAP.get(onlyOfficeFile);
if (key == null) {
return null;
}
ONLY_OFFICE_FILE_KEY_MAP.remove(onlyOfficeFile);
ONLY_OFFICE_KEY_FILE_MAP.remove(key);
return onlyOfficeFile;
}
/**
* 清理缓存中的某个文件夹下所有文件信息与 Key 的映射关系.(文件发生了变化, 需要重新生成 OnlyOffice 预览链接时调用)
*
* @param onlyOfficeFile
* OnlyOffice 文件信息
*/
public static List removeByFolder(OnlyOfficeFile onlyOfficeFile) {
List caches = new ArrayList<>();
Iterator> cacheObjIterator = ONLY_OFFICE_FILE_KEY_MAP.cacheObjIterator();
while (cacheObjIterator.hasNext()) {
CacheObj cacheObj = cacheObjIterator.next();
OnlyOfficeFile cacheOnlyOfficeFile = cacheObj.getKey();
if (cacheOnlyOfficeFile.getStorageKey().equals(onlyOfficeFile.getStorageKey())
&& StringUtils.startWith(cacheOnlyOfficeFile.getPathAndName(), onlyOfficeFile.getPathAndName())) {
ONLY_OFFICE_FILE_KEY_MAP.remove(cacheObj.getKey());
ONLY_OFFICE_KEY_FILE_MAP.remove(cacheObj.getValue());
caches.add(cacheOnlyOfficeFile);
}
}
return caches;
}
/**
* 获取文件锁, 防止并发操作文件缓存时出现问题.
*
* @param key
* 文件唯一标识
*
* @return 锁对象
*/
public static ReentrantLock getLock(OnlyOfficeFile key) {
return locks.get(key, true, ReentrantLock::new);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java
================================================
package im.zhaojun.zfile.core.util;
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
/**
* 规则表达式工具类
*
* @author zhaojun
*/
public class PatternMatcherUtils {
private static final Map PATH_MATCHER_MAP = new HashMap<>();
/**
* 兼容模式的 glob 表达式匹配.
* 默认的 glob 表达式是不支持以下情况的:
*
* - pattern: /a/**
* - test1: /a
* - test2: /a/
*
* test1 和 test 2 均无法匹配这种情况, 此方法兼容了这种情况, 即对 test 内容后拼接 "/xx"(其实任意字符都可以), 使其可以匹配上 pattern.
*
注意:但此方法对包含文件名的情况无效, 仅支持 test 为 路径的情况.
*
* @param pattern
* glob 规则表达式
*
* @param test
* 匹配内容
*
* @return 是否匹配.
*/
public static boolean testCompatibilityGlobPattern(String pattern, String test) {
// 如果规则表达式最开始没有 /, 则兼容在最前方加上 /.
if (!StringUtils.startWith(pattern, StringUtils.SLASH)) {
pattern = StringUtils.SLASH + pattern;
}
// 兼容性处理.
test = StringUtils.concat(test, StringUtils.SLASH);
if (StringUtils.endWith(pattern, "/**") || StringUtils.endWith(pattern, "/*")) {
test += "xxx";
}
return testGlobPattern(pattern, test);
}
/**
* 测试密码规则表达式和文件路径是否匹配
*
* @param pattern
* glob 规则表达式
*
* @param test
* 测试字符串
*/
private static boolean testGlobPattern(String pattern, String test) {
// 从缓存取出 PathMatcher, 防止重复初始化
PathMatcher pathMatcher = PATH_MATCHER_MAP.getOrDefault(pattern, FileSystems.getDefault().getPathMatcher("glob:" + pattern));
PATH_MATCHER_MAP.put(pattern, pathMatcher);
return pathMatcher.matches(Paths.get(test)) || StringUtils.equals(pattern, test);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/PlaceholderUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
/**
* 配置文件或模板中的占位符替换工具类
*
* @author zhaojun
*/
@Slf4j
public class PlaceholderUtils {
/**
* Prefix for system property placeholders: "${"
*/
public static final String PLACEHOLDER_PREFIX = "${";
/**
* Suffix for system property placeholders: "}"
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* 解析占位符, 将指定的占位符替换为指定的值. 变量值从 Spring 环境中获取, 如没取到, 则默认为空.
*
* 必须在 Spring 环境下使用, 否则会抛出异常.
*
*
* @param formatStr
* 模板字符串
*
* @return 替换后的字符串
*/
public static String resolvePlaceholdersBySpringProperties(String formatStr) {
String placeholderName = getFirstPlaceholderName(formatStr);
if (StringUtils.isEmpty(placeholderName)) {
return formatStr;
}
String propertyValue = SpringUtil.getProperty(placeholderName);
Map map = new HashMap<>();
map.put(placeholderName, propertyValue);
return resolvePlaceholders(formatStr, map);
}
/**
* 解析占位符, 将指定的占位符替换为指定的值.
*
* @param formatStr
* 模板字符串
*
* @param parameter
* 参数列表
*
* @return 替换后的字符串
*/
public static String resolvePlaceholders(String formatStr, Map parameter) {
if (parameter == null || parameter.isEmpty()) {
return formatStr;
}
StringBuilder sb = new StringBuilder(formatStr);
int startIndex = sb.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = sb.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
String placeholder = sb.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
try {
String propVal = parameter.get(placeholder);
if (propVal != null) {
sb.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
nextIndex = startIndex + propVal.length();
} else {
log.warn("Could not resolve placeholder '{}' in [{}] ", placeholder, formatStr);
}
} catch (Exception ex) {
log.error("Could not resolve placeholder '{}' in [{}]: ", placeholder, formatStr, ex);
}
startIndex = sb.indexOf(PLACEHOLDER_PREFIX, nextIndex);
} else {
startIndex = -1;
}
}
return sb.toString();
}
/**
* 获取模板字符串第一个占位符的名称, 如 "我的名字是: ${name}, 我的年龄是: ${age}", 返回 "name".
*
* @param formatStr
* 模板字符串
*
* @return 占位符名称
*/
public static String getFirstPlaceholderName(String formatStr) {
List list = getPlaceholderNames(formatStr);
if (CollectionUtils.isNotEmpty(list)) {
return list.getFirst();
}
return null;
}
/**
* 获取模板字符串第一个占位符的名称, 如 "我的名字是: ${name}, 我的年龄是: ${age}", 返回 ["name", "age].
*
* @param formatStr
* 模板字符串
*
* @return 占位符名称
*/
public static List getPlaceholderNames(String formatStr) {
if (StringUtils.isEmpty(formatStr)) {
return Collections.emptyList();
}
List placeholderNameList = new ArrayList<>();
StringBuilder sb = new StringBuilder(formatStr);
int startIndex = sb.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = sb.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
String placeholder = sb.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
startIndex = sb.indexOf(PLACEHOLDER_PREFIX, nextIndex);
placeholderNameList.add(placeholder);
} else {
startIndex = -1;
}
}
return placeholderNameList;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/ProxyDownloadUrlUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 代理下载链接工具类
*
* @author zhaojun
*/
@Slf4j
public class ProxyDownloadUrlUtils {
private static SystemConfigService systemConfigService;
private static final String PROXY_DOWNLOAD_LINK_DELIMITER = ":";
private static final Map AES_CACHE = new HashMap<>();
/**
* 服务器代理下载 URL 有效期 (秒).
*/
public static final Integer PROXY_DOWNLOAD_LINK_EFFECTIVE_SECOND = 1800;
/**
* 生成签名:根据系统设置中 AES 密钥对生成签名.
*
* @param storageId
* 存储源 ID
*
* @param pathAndName
* 文件路径及文件名称
*
* @param effectiveSecond
* 有效时间, 单位: 秒
*
* @return 签名
*/
public static String generatorSignature(Integer storageId, String pathAndName, Integer effectiveSecond) {
if (systemConfigService == null) {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
// 如果有效时间为空, 则设置 30 分钟过期
if (effectiveSecond == null || effectiveSecond < 1) {
effectiveSecond = PROXY_DOWNLOAD_LINK_EFFECTIVE_SECOND;
}
// 过期时间的秒数
long second = DateUtil.offsetSecond(DateUtil.date(), effectiveSecond).getTime();
String content = storageId + PROXY_DOWNLOAD_LINK_DELIMITER + pathAndName + PROXY_DOWNLOAD_LINK_DELIMITER + second;
String aesHexKey = systemConfigService.getAesHexKeyOrGenerate();
SymmetricCrypto aes = AES_CACHE.computeIfAbsent(aesHexKey, k -> new SymmetricCrypto(SymmetricAlgorithm.AES, HexUtil.decodeHex(k)));
//加密
return aes.encryptHex(content);
}
public static boolean validSignatureExpired(Integer expectedStorageId, String expectedPathAndName, String signature) {
if (systemConfigService == null) {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
String aesHexKey = systemConfigService.getAesHexKeyOrGenerate();
SymmetricCrypto aes = AES_CACHE.computeIfAbsent(aesHexKey, k -> new SymmetricCrypto(SymmetricAlgorithm.AES, HexUtil.decodeHex(k)));
long currentTimeMillis = System.currentTimeMillis();
String storageId = null;
String pathAndName = null;
String expiredSecond = null;
try {
//解密
String decryptStr = aes.decryptStr(signature);
List split = StringUtils.split(decryptStr, PROXY_DOWNLOAD_LINK_DELIMITER);
storageId = split.get(0);
pathAndName = split.get(1);
expiredSecond = split.get(2);
// 校验存储源 ID 和文件路径及是否过期.
if (StringUtils.equals(storageId, Convert.toStr(expectedStorageId))
&& StringUtils.equals(StringUtils.concat(pathAndName), StringUtils.concat(expectedPathAndName))
&& currentTimeMillis < Convert.toLong(expiredSecond)) {
return true;
}
log.warn("校验链接已过期或不匹配, signature: {}, storageId={}, pathAndName={}, expiredSecond={}, now:={}", signature, storageId, pathAndName, expiredSecond, currentTimeMillis);
} catch (Exception e) {
log.error("校验签名链接异常, signature: {}, storageId={}, pathAndName={}, expiredSecond={}, now:={}", signature, storageId, pathAndName, expiredSecond, currentTimeMillis);
return false;
}
return false;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/RequestHolder.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.extra.servlet.JakartaServletUtil;
import im.zhaojun.zfile.core.constant.ZFileHttpHeaderConstant;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.core.SystemException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 获取 Request 工具类
*
* @author zhaojun
*/
@Slf4j
public class RequestHolder {
/**
* 获取 HttpServletRequest
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
/**
* 获取 HttpServletResponse
*
* @return HttpServletResponse
*/
public static HttpServletResponse getResponse() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
}
/**
* 向 response 写入文件流.
*
* @param inputStream
* 文件输入流
*
* @param fileName
* 文件名称
*
* @param fileSize
* 文件大小
*
* @param isPartialContentFromInputStream
* 表示输入流是否为部分内容。
* 当该变量为 true 时,表示输入流已经根据 range 规则从存储源获取部分内容。
* 在这种情况下,不需要跳过 range start 部分,可以直接从输入流的全部内容复制到输出流。
*
* @param forceDownload
* 是否强制下载
*/
public static void writeFile(InputStream inputStream, String fileName, Long fileSize, boolean isPartialContentFromInputStream, boolean forceDownload) {
if (inputStream == null) {
throw new BizException(ErrorCode.BIZ_FILE_NOT_EXIST);
}
OutputStream outputStream = null;
try (InputStream innerInputStream = inputStream) {
HttpServletResponse response = RequestHolder.getResponse();
ContentDisposition contentDisposition = ContentDisposition
.builder(forceDownload ? "attachment" : "inline")
.filename(fileName, StandardCharsets.UTF_8)
.build();
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString());
if (forceDownload) {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
} else {
response.setContentType(MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM).toString());
}
outputStream = response.getOutputStream();
if (fileSize != null && fileSize > 0) {
String range = RequestHolder.getRequest().getHeader(HttpHeaders.RANGE);
List httpRanges = HttpRange.parseRanges(range);
if (httpRanges.isEmpty()) {
httpRanges = Collections.singletonList(HttpRange.createByteRange(0, fileSize - 1));
} else {
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
}
HttpRange httpRange = CollectionUtils.getFirst(httpRanges);
long startPos = httpRange.getRangeStart(fileSize);
long endPos = httpRange.getRangeEnd(fileSize);
if (response.getStatus() == HttpServletResponse.SC_PARTIAL_CONTENT) {
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startPos + "-" + endPos + StringUtils.SLASH + fileSize);
}
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
response.setContentLengthLong(endPos - startPos + 1);
if (isPartialContentFromInputStream) {
StreamUtils.copy(innerInputStream, outputStream);
} else {
StreamUtils.copyRange(innerInputStream, outputStream, startPos, endPos);
}
return;
}
StreamUtils.copy(innerInputStream, outputStream);
} catch (IOException e) {
boolean isBrokenPipe = e.getMessage().contains("Broken pipe");
boolean isConnectionResetByPeer = e.getMessage().contains("Connection reset by peer");
if (isBrokenPipe || isConnectionResetByPeer) {
if (log.isDebugEnabled()) {
log.debug("skip IOException: {}", e.getMessage());
}
} else {
throw new SystemException(e);
}
} finally {
IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(outputStream);
}
}
public static boolean isAxiosRequest() {
HttpServletRequest request = RequestHolder.getRequest();
String axiosRequest = JakartaServletUtil.getHeaderIgnoreCase(request, ZFileHttpHeaderConstant.AXIOS_REQUEST);
return StringUtils.isNotEmpty(axiosRequest);
}
/**
* 获取请求头中的 Axios-From 字段
*
* @return Axios-From 字段
*/
public static String getAxiosFrom() {
if (RequestContextHolder.getRequestAttributes() == null) {
return null;
}
HttpServletRequest request = RequestHolder.getRequest();
return JakartaServletUtil.getHeaderIgnoreCase(request, ZFileHttpHeaderConstant.AXIOS_FROM);
}
/**
* 获取后端服务地址,如果经过了反向代理,需反向代理正确配置
*/
public static String getRequestServerAddress() {
if (RequestContextHolder.getRequestAttributes() == null) {
return null;
}
HttpServletRequest request = RequestHolder.getRequest();
String forwardedHost = JakartaServletUtil.getHeaderIgnoreCase(request, "X-Forwarded-Host");
String forwardedPort = JakartaServletUtil.getHeaderIgnoreCase(request, "X-Forwarded-Port");
String forwardedProto = JakartaServletUtil.getHeaderIgnoreCase(request, "X-Forwarded-Proto");
String scheme = StringUtils.isBlank(forwardedProto) ? request.getScheme() : forwardedProto;
// 优先使用 X-Forwarded-Host,其次使用 Host 头,最后使用 request.getServerName()
String serverName;
String hostHeader = StringUtils.isNotBlank(forwardedHost) ? forwardedHost : request.getHeader("Host");
if (StringUtils.isNotBlank(hostHeader)) {
// Host 头可能包含端口信息,如 "example.com:8080"
String[] hostParts = hostHeader.split(":");
serverName = hostParts[0];
// 如果 Host 头包含端口且没有显式设置 X-Forwarded-Port,则使用 Host 头中的端口
if (hostParts.length > 1 && StringUtils.isBlank(forwardedPort)) {
forwardedPort = hostParts[1];
}
} else {
serverName = request.getServerName();
}
// 端口处理逻辑
String port;
if (StringUtils.isNotBlank(forwardedPort)) {
port = forwardedPort;
} else if (StringUtils.isNotBlank(forwardedProto)) {
// 如果设置了转发协议但没有设置端口,使用协议默认端口
port = "https".equalsIgnoreCase(forwardedProto) ? "443" : "80";
} else {
port = String.valueOf(request.getServerPort());
}
// 移除默认端口
if ("443".equals(port) && "https".equalsIgnoreCase(scheme)) {
port = "";
}
if ("80".equals(port) && "http".equalsIgnoreCase(scheme)) {
port = "";
}
if (StringUtils.isBlank(port)) {
return scheme + "://" + serverName;
} else {
return scheme + "://" + serverName + ":" + port;
}
}
/**
* 获取当前请求的 Origin 请求头
*
* @return Origin 请求头值
*/
public static String getOriginAddress() {
if (RequestContextHolder.getRequestAttributes() == null) {
return null;
}
HttpServletRequest request = RequestHolder.getRequest();
return JakartaServletUtil.getHeaderIgnoreCase(request, HttpHeaders.ORIGIN);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/RequestUtils.java
================================================
package im.zhaojun.zfile.core.util;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRange;
import org.springframework.util.CollectionUtils;
public class RequestUtils {
public static HttpRange getRequestRange(HttpServletRequest request) {
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
if (rangeHeader == null) {
return null;
}
return CollectionUtils.firstElement(HttpRange.parseRanges(rangeHeader));
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/SizeToStrUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.util.NumberUtil;
/**
* 文件大小或带宽大小转可读单位
*
* @author zhaojun
*/
public class SizeToStrUtils {
/**
* 将文件大小转换为可读单位
*
* @param bytes
* 字节数
*
* @return 文件大小可读单位
*/
public static String bytesToSize(long bytes) {
if (bytes == 0) {
return "0";
}
double k = 1024;
String[] sizes = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
double i = Math.floor(Math.log(bytes) / Math.log(k));
return NumberUtil.round(bytes / Math.pow(k, i), 3) + " " + sizes[(int) i];
}
/**
* 将带宽大小转换为可读单位
*
* @param bps
* 字节数
*
* @return 带宽大小可读单位
*/
public static String bpsToSize(long bps) {
if (bps == 0) {
return "0";
}
double k = 1000;
String[] sizes = new String[]{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
double i = Math.floor(Math.log(bps) / Math.log(k));
return NumberUtil.round(bps / Math.pow(k, i), 3) + " " + sizes[(int) i];
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/SpringMvcUtils.java
================================================
package im.zhaojun.zfile.core.util;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerMapping;
import jakarta.servlet.http.HttpServletRequest;
/**
* @author zhaojun
*/
public class SpringMvcUtils {
public static String getExtractPathWithinPattern() {
HttpServletRequest httpServletRequest = RequestHolder.getRequest();
String path = (String) httpServletRequest.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String) httpServletRequest.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
AntPathMatcher apm = new AntPathMatcher();
return apm.extractPathWithinPattern(bestMatchPattern, path);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/StrPool.java
================================================
package im.zhaojun.zfile.core.util;
public interface StrPool {
/**
* 字符串常量:制表符 {@code "\t"}
*/
String TAB = " ";
/**
* 字符串常量:点 {@code "."}
*/
String DOT = ".";
/**
* 字符串常量:双点 {@code ".."}
* 用途:作为指向上级文件夹的路径,如:{@code "../path"}
*/
String DOUBLE_DOT = "..";
/**
* 字符串常量:斜杠 {@code "/"}
*/
String SLASH = "/";
/**
* 字符串常量:反斜杠 {@code "\\"}
*/
String BACKSLASH = "\\";
/**
* 字符串常量:回车符 {@code "\r"}
* 解释:该字符常用于表示 Linux 系统和 MacOS 系统下的文本换行
*/
String CR = "\r";
/**
* 字符串常量:换行符 {@code "\n"}
*/
String LF = "\n";
/**
* 字符串常量:Windows 换行 {@code "\r\n"}
* 解释:该字符串常用于表示 Windows 系统下的文本换行
*/
String CRLF = "\r\n";
/**
* 字符串常量:下划线 {@code "_"}
*/
String UNDERLINE = "_";
/**
* 字符串常量:减号(连接符) {@code "-"}
*/
String DASHED = "-";
/**
* 字符串常量:逗号 {@code ","}
*/
String COMMA = ",";
/**
* 字符串常量:花括号(左) "{"
*/
String DELIM_START = "{";
/**
* 字符串常量:花括号(右) "}"
*/
String DELIM_END = "}";
/**
* 字符串常量:中括号(左) {@code "["}
*/
String BRACKET_START = "[";
/**
* 字符串常量:中括号(右) {@code "]"}
*/
String BRACKET_END = "]";
/**
* 字符串常量:冒号 {@code ":"}
*/
String COLON = ":";
/**
* 字符串常量:艾特 {@code "@"}
*/
String AT = "@";
/**
* 字符串常量:空 JSON {@code "{}"}
*/
String EMPTY_JSON = "{}";
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/StringUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.LRUCache;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* 字符串相关工具类
*
* @author zhaojun
*/
public class StringUtils extends CharSequenceUtil implements StrPool {
public static final String HTTP = "http";
public static final String PROTOCOL_MARKER = "://";
private static final LRUCache CACHE = CacheUtil.newLRUCache(1000);
/**
* 移除 URL 中的前后的所有 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1/', 返回 'folder1/file1'
* 如 path = '///folder1/file1//', 返回 'folder1/file1'
*/
public static String trimSlashes(String path) {
path = trimStartSlashes(path);
path = trimEndSlashes(path);
return path;
}
/**
* 移除 URL 中的前面的所有 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1', 返回 'folder1/file1'
* 如 path = '//folder1/file1', 返回 'folder1/file1'
*
*/
public static String trimStartSlashes(String path) {
if (isEmpty(path)) {
return path;
}
while (path.startsWith(SLASH)) {
path = path.substring(1);
}
return path;
}
/**
* 移除 URL 中结尾的所有 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1/', 返回 '/folder1/file1'
* 如 path = '/folder1/file1///', 返回 '/folder1/file1'
*/
public static String trimEndSlashes(String path) {
if (isEmpty(path)) {
return path;
}
while (path.endsWith(SLASH)) {
path = path.substring(0, path.length() - 1);
}
return path;
}
/**
* 去除路径中所有重复的 '/',如果最开始的协议头前有 / 也一并去除。
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 '/folder1/file1/'
* 如 path = '/folder1////file1///', 返回 '/folder1/file1/'
*/
public static String removeDuplicateSlashes(String path) {
if (isEmpty(path)) {
return path;
}
return CACHE.get(path, false, () -> {
StringBuilder sb = new StringBuilder(path.length());
int protocolIndex = path.indexOf(PROTOCOL_MARKER);
int pathStartIndex = 0;
// 1. 处理协议部分
if (protocolIndex > -1) {
// 找到协议名称的实际开始位置
int schemeStartIndex = 0;
while (schemeStartIndex < protocolIndex && path.charAt(schemeStartIndex) == '/') {
schemeStartIndex++;
}
sb.append(path, schemeStartIndex, protocolIndex);
sb.append(PROTOCOL_MARKER);
pathStartIndex = protocolIndex + PROTOCOL_MARKER.length();
}
if (pathStartIndex < path.length()) {
char lastChar;
char firstPathChar = path.charAt(pathStartIndex);
sb.append(firstPathChar);
lastChar = firstPathChar;
for (int i = pathStartIndex + 1; i < path.length(); i++) {
char current = path.charAt(i);
if (current != SLASH_CHAR || lastChar != SLASH_CHAR) {
sb.append(current);
lastChar = current;
}
}
}
return sb.toString();
});
}
/**
* 去除路径中所有重复的 '/', 并且去除开头的 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 'folder1/file1/'
* 如 path = '///folder1////file1///', 返回 'folder1/file1/'
*/
public static String removeDuplicateSlashesAndTrimStart(String path) {
path = removeDuplicateSlashes(path);
path = trimStartSlashes(path);
return path;
}
/**
* 去除路径中所有重复的 '/', 并且去除结尾的 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1//file1/', 返回 '/folder1/file1'
* 如 path = '///folder1////file1///', 返回 '/folder1/file1'
*/
public static String removeDuplicateSlashesAndTrimEnd(String path) {
path = removeDuplicateSlashes(path);
path = trimEndSlashes(path);
return path;
}
/**
* 拼接 URL,并去除重复的分隔符 '/',并去除开头的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimStartSlashes(String... strs) {
return trimStartSlashes(concat(strs));
}
/**
* 拼接 URL,并去除重复的分隔符 '/',并去除结尾的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimEndSlashes(String... strs) {
return trimEndSlashes(concat(strs));
}
/**
* 拼接 URL,并去除重复的分隔符 '/',并去除开头和结尾的 '/', 但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concatTrimSlashes(String... strs) {
return trimSlashes(concat(strs));
}
/**
* 拼接 URL,并去除重复的分隔符 '/',但不会影响 http:// 和 https:// 这种头部.
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concat(String... strs) {
StringBuilder sb = new StringBuilder(SLASH);
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
if (isEmpty(str)) {
continue;
}
sb.append(str);
if (i != strs.length - 1) {
sb.append(SLASH_CHAR);
}
}
return removeDuplicateSlashes(sb.toString());
}
/**
* 拼接 URL,并去除重复的分隔符 '/',但不会影响 http:// 和 https:// 这种头部.
*
* @param encodeAllIgnoreSlashes
* 是否 encode 编码 (忽略 /)
*
* @param strs
* 拼接的字符数组
*
* @return 拼接结果
*/
public static String concat(boolean encodeAllIgnoreSlashes, String... strs) {
String res = concat(strs);
if (encodeAllIgnoreSlashes) {
return encodeAllIgnoreSlashes(res);
} else {
return res;
}
}
/**
* 替换 URL 中的 Host 部分,如替换 http://a.com/1.txt 为 https://abc.com/1.txt
*
* @param originUrl
* 原 URL
*
* @param replaceHost
* 替换的 HOST
*
* @return 替换后的 URL
*/
public static String replaceHost(String originUrl, String replaceHost) {
try {
String path = new URL(originUrl).getFile();
return concat(replaceHost, path);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return null;
}
/**
* 编码 URL,默认使用 UTF-8 编码
* URL 的 Fragment URLEncoder
* 默认的编码器针对Fragment,定义如下:
*
*
* fragment = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
*
*
* 具体见:https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
*
* @param url
* 被编码内容
*
* @return 编码后的字符
*/
public static String encode(String url) {
return URLEncodeUtil.encodeFragment(url);
}
/**
* 编码全部字符
*
* @param str
* 被编码内容
*
* @return 编码后的字符
*/
public static String encodeAllIgnoreSlashes(String str) {
if (isEmpty(str)) {
return str;
}
StringBuilder sb = new StringBuilder();
int prevIndex = -1;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == StringUtils.SLASH_CHAR) {
if (prevIndex < i) {
String substring = str.substring(prevIndex + 1, i);
sb.append(URLEncodeUtil.encodeAll(substring));
prevIndex = i;
}
sb.append(c);
}
if (i == str.length() - 1 && prevIndex < i) {
String substring = str.substring(prevIndex + 1, i + 1);
sb.append(URLEncodeUtil.encodeAll(substring));
}
}
return sb.toString();
}
/**
* 解码 URL, 默认使用 UTF8 编码. 不会将 + 转为空格.
*
* @param url
* 被解码内容
*
* @return 解码后的内容
*/
public static String decode(String url) {
return URLUtil.decode(url, StandardCharsets.UTF_8, false);
}
/**
* 移除字符串中所有换行符并去除前后空格
*
* @param str
* URL
*
* @return 移除协议后的 URL
*/
public static String removeAllLineBreaksAndTrim(String str) {
String removeResult = StrUtil.removeAllLineBreaks(str);
return trim(removeResult);
}
/**
* 移除字符串前后空格
*
* @param str
* 字符串
*
* @return 移除前后空格后的字符串
*/
public static String trim(final String str) {
return str == null ? null : str.trim();
}
/**
* 如果给定字符串不是以suffix结尾的,在尾部补充 suffix
*
* @param str 字符串
* @param suffix 后缀
* @return 补充后的字符串
*/
public static String addSuffixIfNot(CharSequence str, CharSequence suffix) {
if (isEmpty(str) || isEmpty(suffix)) {
return str.toString();
}
if (str.toString().endsWith(suffix.toString())) {
return str.toString();
}
return str.toString() + suffix;
}
/**
* 是否包含特定字符,忽略大小写,如果给定两个参数都为{@code null},返回true
*
* @param str 被检测字符串
* @param testStr 被测试是否包含的字符串
* @return 是否包含
*/
public static boolean containsIgnoreCase(CharSequence str, CharSequence testStr) {
if (null == str) {
// 如果被监测字符串和
return null == testStr;
}
return StrUtil.indexOfIgnoreCase(str, testStr) > -1;
}
/**
* 指定范围内查找指定字符
*
* @param str
* 字符串
*
* @param searchChar
* 被查找的字符
*
* @return 位置
*/
public static int indexOf(String str, char searchChar) {
if (isEmpty(str)) {
return INDEX_NOT_FOUND;
}
return str.indexOf(searchChar);
}
/**
* 字符串驼峰转下划线格式
*
* @param param
* 驼峰格式字符串
*
* @return 下划线格式字符串
*/
public static String camelToUnderline(String param) {
if (isEmpty(param)) {
return EMPTY;
}
int len = param.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = param.charAt(i);
if (Character.isUpperCase(c) && i > 0) {
sb.append(UNDERLINE);
}
sb.append(Character.toLowerCase(c));
}
return sb.toString();
}
/**
* 强制给 URL 设置协议
*
* @param url
* URL 地址,可以是带协议的,也可以是不带协议的,写会忽略大小写
*
* @param schema
* 协议,如 http, https, http://, https://
*
* @return 设置协议后的 URL
*/
public static String setSchema(String url, String schema) {
if (StringUtils.isEmpty(url) || StringUtils.isEmpty(schema)) {
return url;
}
if (!schema.endsWith("://")) {
schema += "://";
}
String lowerUrl = url.toLowerCase();
if (lowerUrl.startsWith("http://")) {
url = url.substring(7);
} else if (lowerUrl.startsWith("https://")) {
url = url.substring(8);
}
return schema + url;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/UrlUtils.java
================================================
package im.zhaojun.zfile.core.util;
import cn.hutool.core.util.StrUtil;
/**
* url 相关工具类
*
* @author zhaojun
*/
public class UrlUtils {
/**
* 判断 URL 是否包含协议部分
*
* @param url
* URL 地址
*
* @return 是否包含协议部分
*/
public static boolean hasScheme(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
/**
* 为 URL 拼接参数
*
* @param url
* 原始 URL
*
* @param name
* 参数名称
*
* @param value
* 参数值
*
* @return 拼接后的 URL
*/
public static String concatQueryParam(String url, String name, String value) {
if (StringUtils.contains(url, "?")) {
return url + "&" + name + "=" + value;
} else {
return url + "?" + name + "=" + value;
}
}
/**
* 获取 URL 中的协议部分
*
* @param url
* URL 地址
*
* @return 协议部分
*/
public static String getSchema(String url) {
if (StringUtils.startWithIgnoreCase(url, "http://")) {
return "http";
} else if (StringUtils.startWithIgnoreCase(url, "https://")) {
return "https";
} else {
return "http";
}
}
/**
* 移除 URL 中的协议部分
*
* @param url
* URL 地址
*
* @return 移除协议部分后的 URL
*/
public static String removeScheme(String url) {
if (StringUtils.startWithIgnoreCase(url, "http://")) {
url = url.substring(7);
} else if (StringUtils.startWithIgnoreCase(url, "https://")) {
url = url.substring(8);
}
return url;
}
/**
* 获取 URL 中的域名部分
*
* @param url
* URL 地址
*
* @return 域名部分
*/
public static String getDomain(String url) {
if (!StringUtils.isEmpty(url)) {
//替换指定前缀
String newStr = url.replace("http://", "");
newStr = newStr.replace("https://", "");
int index = StrUtil.indexOf(newStr, '/');
if (index > 0) {
newStr = newStr.substring(0, index);
}
String[] split = newStr.split(":");
if (split.length > 1) {
return split[0];
} else {
return newStr;
}
} else {
return null;
}
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/ZFileAuthUtil.java
================================================
package im.zhaojun.zfile.core.util;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.extra.spring.SpringUtil;
import im.zhaojun.zfile.module.share.context.ShareAccessContext;
import im.zhaojun.zfile.module.user.model.constant.UserConstant;
import im.zhaojun.zfile.module.user.model.entity.User;
import im.zhaojun.zfile.module.user.service.UserService;
/**
* 登录认证工具类
*
* @author zhaojun
*/
public class ZFileAuthUtil {
private static UserService userService;
public static User getCurrentUser() {
if (userService == null) {
userService = SpringUtil.getBean(UserService.class);
}
// 检查是否为分享访问,如果是则返回分享者用户 ID
if (ShareAccessContext.isShareAccess()) {
Integer shareUserId = ShareAccessContext.getShareUserId();
return userService.getById(shareUserId);
}
return userService.getById(StpUtil.getLoginId(UserConstant.ANONYMOUS_ID));
}
public static Integer getCurrentUserId() {
if (userService == null) {
userService = SpringUtil.getBean(UserService.class);
}
// 检查是否为分享访问,如果是则返回分享者用户 ID
if (ShareAccessContext.isShareAccess()) {
return ShareAccessContext.getShareUserId();
}
try {
return StpUtil.getLoginId(UserConstant.ANONYMOUS_ID);
} catch (Exception e) {
return UserConstant.ANONYMOUS_ID;
}
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/AbstractRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher;
import java.util.Collection;
/**
* 抽象规则匹配器, 实现了部分方法, 用于简化规则匹配器的实现.
*
* @author zhaojun
*/
public abstract class AbstractRuleMatcher implements IRuleMatcher {
@Override
public boolean contains(String ruleExpression, String testStr) {
return match(ruleExpression, testStr);
}
@Override
public boolean matchAny(Collection ruleExpressionList, String testStr) {
if (ruleExpressionList == null || ruleExpressionList.isEmpty()) {
return false;
}
for (String ruleExpression : ruleExpressionList) {
if (match(ruleExpression, testStr)) {
return true;
}
}
return false;
}
@Override
public String matchAnyReturnFirst(Collection ruleExpressionList, String testStr) {
if (ruleExpressionList == null || ruleExpressionList.isEmpty()) {
return null;
}
for (String ruleExpression : ruleExpressionList) {
if (match(ruleExpression, testStr)) {
return ruleExpression;
}
}
return null;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/IRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher;
import java.util.Collection;
/**
* 规则匹配器接口
*
* @author zhaojun
*/
public interface IRuleMatcher {
/**
* 匹配规则
*
* @param ruleExpression
* 规则表达式
*
* @param testStr
* 测试字符串
*
* @return 是否匹配
*/
boolean match(String ruleExpression, String testStr);
/**
* 部分匹配规则
*
* @param ruleExpression
* 规则表达式
*
* @param testStr
* 测试字符串
*
* @return 是否部分匹配
*/
boolean contains(String ruleExpression, String testStr);
/**
* 匹配规则, 可以匹配多个规则表达式, 只要有一个匹配成功, 则返回 true
*
* @param ruleExpressionList
* 规则表达式列表
*
* @param testStr
* 测试字符串
*
* @return 是否匹配
*/
boolean matchAny(Collection ruleExpressionList, String testStr);
/**
* 匹配规则, 可以匹配多个规则表达式, 只要有一个匹配成功, 则返回第一个匹配成功的表达式。
*
* @param ruleExpressionList
* 规则表达式列表
*
* @param testStr
* 测试字符串
*
* @return 匹配成功的第一个表达式
*/
String matchAnyReturnFirst(Collection ruleExpressionList, String testStr);
/**
* 获取规则类型
*
* @return 规则类型
*/
String getRuleType();
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/RuleMatcherFactory.java
================================================
package im.zhaojun.zfile.core.util.matcher;
import im.zhaojun.zfile.core.util.matcher.impl.AntPathRuleMatcher;
import im.zhaojun.zfile.core.util.matcher.impl.IpRuleMatcher;
import im.zhaojun.zfile.core.util.matcher.impl.RegexRuleMatcher;
import im.zhaojun.zfile.core.util.matcher.impl.SpringSimpleRuleMatcher;
import java.util.HashMap;
import java.util.Map;
/**
* 规则匹配器工厂, 用于获取规则匹配器实例.
*
* @author zhaojun
*/
public class RuleMatcherFactory {
private static final Map RULE_MATCHER_MAP = new HashMap<>();
static {
IpRuleMatcher ipRuleMatcher = new IpRuleMatcher();
RULE_MATCHER_MAP.put(ipRuleMatcher.getRuleType(), ipRuleMatcher);
RegexRuleMatcher regexRuleMatcher = new RegexRuleMatcher();
RULE_MATCHER_MAP.put(regexRuleMatcher.getRuleType(), regexRuleMatcher);
AntPathRuleMatcher antPathRuleMatcher = new AntPathRuleMatcher();
RULE_MATCHER_MAP.put(antPathRuleMatcher.getRuleType(), antPathRuleMatcher);
SpringSimpleRuleMatcher springSimpleRuleMatcher = new SpringSimpleRuleMatcher();
RULE_MATCHER_MAP.put(springSimpleRuleMatcher.getRuleType(), springSimpleRuleMatcher);
}
public static IRuleMatcher getRuleMatcher(String ruleType) {
if (ruleType == null || ruleType.isEmpty()) {
return null;
}
return RULE_MATCHER_MAP.get(ruleType);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/impl/AntPathRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher.impl;
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
import im.zhaojun.zfile.core.util.matcher.AbstractRuleMatcher;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;
/**
* Ant 路径匹配器, 用于匹配路径规则.
*
* @author zhaojun
*/
@Slf4j
public class AntPathRuleMatcher extends AbstractRuleMatcher {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public boolean match(String ruleExpression, String testStr) {
boolean match = pathMatcher.match(ruleExpression, testStr);
if (log.isDebugEnabled()) {
log.debug("Ant 表达式匹配结果: {}, 规则表达式: {}, 测试值: {}", match, ruleExpression, testStr);
}
return match;
}
@Override
public String getRuleType() {
return RuleTypeConstant.ANT_PATH;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/impl/IpRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher.impl;
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.core.util.matcher.AbstractRuleMatcher;
import lombok.extern.slf4j.Slf4j;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.regex.Pattern;
/**
* IP 匹配器, 用于 IP 规则,支持匹配完整 IP 或 IP 段,同时支持 IPV4 和 IPV6.
*
*
*
* - IPV4 示例:
*
* - 127.0.0.1
* - 192.168.0.0/24
*
*
*
*
*
*
* - IPV6 示例:
*
* - 0:0:0:0:0:0:0:1
* - 0:0:0:0:0:0:0:0/64
*
*
*
*
* @author zhaojun
*/
@Slf4j
public class IpRuleMatcher extends AbstractRuleMatcher {
@Override
public boolean match(String ruleExpression, String testStr) {
try {
InetAddress inetAddress = InetAddress.getByName(testStr);
IpRule rule = createRule(ruleExpression);
boolean match = rule != null && rule.matches(inetAddress);
if (log.isDebugEnabled()) {
log.debug("IP 匹配结果: {}, 规则表达式: {}, 测试值: {}, 校验规则: {}", match, ruleExpression, testStr, rule);
}
return match;
} catch (UnknownHostException e) {
log.error("IP 地址解析失败, ruleExpression: {}, testStr: {}", ruleExpression, testStr);
}
return false;
}
@Override
public String getRuleType() {
return RuleTypeConstant.IP;
}
private IpRule createRule(String ruleExpression) {
if (isValidIpv4(ruleExpression)) {
return new Ipv4Rule(ruleExpression);
} else if (isValidIpv6(ruleExpression)) {
return new Ipv6Rule(ruleExpression);
} else if (isValidIpv4Range(ruleExpression)) {
return new Ipv4RangeRule(ruleExpression);
} else if (isValidIpv6Range(ruleExpression)) {
return new Ipv6RangeRule(ruleExpression);
} else {
return null;
}
}
private boolean isValidIpv4(String ipAddress) {
String ipv4Pattern = "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\."
+ "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";
return Pattern.matches(ipv4Pattern, ipAddress);
}
private boolean isValidIpv6(String ipAddress) {
try {
InetAddress.getByName(ipAddress);
return ipAddress.contains(":");
} catch (UnknownHostException e) {
return false;
}
}
private boolean isValidIpv4Range(String ipRange) {
String ipv4RangePattern = "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$";
return Pattern.matches(ipv4RangePattern, ipRange);
}
private boolean isValidIpv6Range(String ipRange) {
String ipv6RangePattern = "^([0-9A-Fa-f]{0,4}:){1,7}([0-9A-Fa-f]{0,4})?/[0-9]{1,3}$";
return Pattern.matches(ipv6RangePattern, ipRange);
}
private interface IpRule {
boolean matches(InetAddress ipAddress);
String getExpression();
}
private static class Ipv4Rule implements IpRule {
private final String expression;
Ipv4Rule(String expression) {
this.expression = expression;
}
@Override
public boolean matches(InetAddress ipAddress) {
if (ipAddress instanceof Inet6Address) {
return false;
}
return ipAddress.getHostAddress().equals(expression);
}
@Override
public String getExpression() {
return expression;
}
}
private static class Ipv6Rule implements IpRule {
private final String expression;
Ipv6Rule(String expression) {
this.expression = expression;
}
@Override
public boolean matches(InetAddress ipAddress) {
if (ipAddress instanceof Inet6Address) {
return ipAddress.getHostAddress().equals(expression);
}
return false;
}
@Override
public String getExpression() {
return expression;
}
}
private static class Ipv4RangeRule implements IpRule {
private final String expression;
private final int prefixLength;
Ipv4RangeRule(String expression) {
this.expression = expression.substring(0, expression.indexOf('/'));
this.prefixLength = Integer.parseInt(expression.substring(expression.indexOf('/') + 1));
}
@Override
public boolean matches(InetAddress ipAddress) {
if (ipAddress instanceof Inet6Address) {
return false;
}
String[] rangeParts = expression.split("\\.");
byte[] rangeAddrBytes = new byte[4];
for (int i = 0; i < rangeParts.length; i++) {
rangeAddrBytes[i] = (byte) Integer.parseInt(rangeParts[i]);
}
byte[] ipValue = ipAddress.getAddress();
if (ipValue.length != 4) {
return false;
}
for (int i = 0; i < prefixLength / 8; i++) {
if (rangeAddrBytes[i] != ipValue[i]) {
return false;
}
}
int remainingBits = prefixLength % 8;
if (remainingBits != 0) {
int rangeByte = rangeAddrBytes[prefixLength / 8];
int ipByte = ipValue[prefixLength / 8];
int shift = 8 - remainingBits;
int mask = 0xFF >> shift;
return (rangeByte >> shift & mask) == (ipByte >> shift & mask);
}
return true;
}
@Override
public String getExpression() {
return expression + StringUtils.SLASH + prefixLength;
}
}
private static class Ipv6RangeRule implements IpRule {
private final String expression;
private final int prefixLength;
Ipv6RangeRule(String expression) {
this.expression = expression.substring(0, expression.indexOf('/'));
this.prefixLength = Integer.parseInt(expression.substring(expression.indexOf('/') + 1));
}
@Override
public boolean matches(InetAddress ipAddress) {
if (ipAddress instanceof Inet6Address) {
byte[] ipValue = ipAddress.getAddress();
byte[] rangeValue = Inet6AddressConverter.convert(expression);
if (ipValue.length != 16) {
return false;
}
for (int i = 0; i < prefixLength / 8; i++) {
if (rangeValue[i] != ipValue[i]) {
return false;
}
}
int remainingBits = prefixLength % 8;
if (remainingBits != 0) {
int rangeByte = rangeValue[prefixLength / 8];
int ipByte = ipValue[prefixLength / 8];
int shift = 8 - remainingBits;
int mask = 0xFF >> shift;
return (rangeByte >> shift & mask) == (ipByte >> shift & mask);
}
return true;
}
return false;
}
@Override
public String getExpression() {
return expression + StringUtils.SLASH + prefixLength;
}
}
// Utility class to convert IPv6 address to byte array
private static class Inet6AddressConverter {
public static byte[] convert(String ipv6Address) {
byte[] ipAddress = new byte[16];
String[] blocks = ipv6Address.split(":");
for (int i = 0; i < blocks.length; i++) {
String block = blocks[i];
if (!block.isEmpty()) {
ipAddress[i * 2] = (byte) Integer.parseInt(block.substring(0, 2), 16);
ipAddress[i * 2 + 1] = (byte) Integer.parseInt(block.substring(2, 4), 16);
}
}
return ipAddress;
}
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/impl/RegexRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher.impl;
import cn.hutool.core.util.ReUtil;
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
import im.zhaojun.zfile.core.util.matcher.AbstractRuleMatcher;
import lombok.extern.slf4j.Slf4j;
/**
* 正则匹配器
*
* @author zhaojun
*/
@Slf4j
public class RegexRuleMatcher extends AbstractRuleMatcher {
@Override
public boolean match(String ruleExpression, String testStr) {
boolean match = ReUtil.isMatch(ruleExpression, testStr);
if (log.isDebugEnabled()) {
log.debug("正则匹配结果: {}, 规则表达式: {}, 测试值: {}", match, ruleExpression, testStr);
}
return match;
}
@Override
public boolean contains(String ruleExpression, String testStr) {
boolean match = ReUtil.contains(ruleExpression, testStr);
if (log.isDebugEnabled()) {
log.debug("正则部分匹配结果: {}, 规则表达式: {}, 测试值: {}", match, ruleExpression, testStr);
}
return match;
}
@Override
public String getRuleType() {
return RuleTypeConstant.REGEX;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/util/matcher/impl/SpringSimpleRuleMatcher.java
================================================
package im.zhaojun.zfile.core.util.matcher.impl;
import im.zhaojun.zfile.core.constant.RuleTypeConstant;
import im.zhaojun.zfile.core.util.matcher.AbstractRuleMatcher;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
/**
* 使用 {@link org.springframework.util.PatternMatchUtils} 来匹配规则
*
* @author zhaojun
*/
@Slf4j
public class SpringSimpleRuleMatcher extends AbstractRuleMatcher {
@Override
public boolean match(String ruleExpression, String testStr) {
boolean match = PatternMatchUtils.simpleMatch(ruleExpression, testStr);
if (log.isDebugEnabled()) {
log.debug("Spring Simple 规则匹配结果: {}, 规则表达式: {}, 测试值: {}", match, ruleExpression, testStr);
}
return match;
}
@Override
public String getRuleType() {
return RuleTypeConstant.SPRING_SIMPLE;
}
@Override
public boolean contains(String ruleExpression, String testStr) {
return match("*" + ruleExpression + "*", testStr);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/validation/StringListValue.java
================================================
package im.zhaojun.zfile.core.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* 字符串列表值校验注解
*
* @author zhaojun
*/
@Documented
@Constraint(validatedBy = { StringListValueConstraintValidator.class })
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface StringListValue {
String message() default "";
Class>[] groups() default { };
Class extends Payload>[] payload() default { };
String[] vals() default { };
}
================================================
FILE: src/main/java/im/zhaojun/zfile/core/validation/StringListValueConstraintValidator.java
================================================
package im.zhaojun.zfile.core.validation;
import im.zhaojun.zfile.core.util.StringUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 字符串列表值校验器
*
* @author zhaojun
*/
public class StringListValueConstraintValidator implements ConstraintValidator {
private final Set set = new HashSet<>();
/**
* 初始化方法
*
* @param constraintAnnotation
* 校验注解对象
*/
@Override
public void initialize(StringListValue constraintAnnotation) {
String[] vals = constraintAnnotation.vals();
set.addAll(Arrays.asList(vals));
}
/**
* 判断是否校验成功
*
* @param value
* 需要校验的值
*
* @param context
* 校验上下文
*
* @return 是否校验成功
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value)) {
return true;
}
return set.contains(value);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/admin/controller/IpHelperController.java
================================================
package im.zhaojun.zfile.module.admin.controller;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.RequestHolder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author zhaojun
*/
@Tag(name = "IP 地址辅助 Controller")
@Slf4j
@RequestMapping("/admin")
@RestController
public class IpHelperController {
@Resource
private HttpServletRequest httpServletRequest;
@GetMapping("clientIp")
@Operation(summary = "获取客户端IP", description ="获取当前请求的客户端IP地址")
public AjaxJson clientIp() {
String clientIp = JakartaServletUtil.getClientIP(httpServletRequest);
return AjaxJson.getSuccessData(clientIp);
}
@GetMapping("serverAddress")
@Operation(summary = "获取服务器地址", description = "获取当前请求的服务器地址(如果是反向代理过,可能获取到的是反向代理服务器的地址)")
public AjaxJson serverAddress() {
return AjaxJson.getSuccessData(RequestHolder.getRequestServerAddress());
}
@GetMapping("headers")
@Operation(summary = "获取 Headers", description = "获取服务器接收到的请求头信息,可用于排查反向代理配置问题")
public AjaxJson headers() {
Map> headersMap = JakartaServletUtil.getHeadersMap(httpServletRequest);
Map singleValueHeaderMap = headersMap.entrySet().stream()
.collect(java.util.stream.Collectors.toMap(
Map.Entry::getKey,
entry -> String.join(",", entry.getValue())
));
return AjaxJson.getSuccessData(JSON.toJSONString(singleValueHeaderMap, JSONWriter.Feature.PrettyFormat));
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/admin/controller/RuleMatcherTestController.java
================================================
package im.zhaojun.zfile.module.admin.controller;
import im.zhaojun.zfile.core.util.AjaxJson;
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.admin.model.request.TestRuleMatcherRequest;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author zhaojun
*/
@Tag(name = "规则匹配辅助 Controller")
@Slf4j
@RequestMapping("/admin")
@RestController
public class RuleMatcherTestController {
/**
* 根据传入的规则和测试值, 测试规则是否匹配, 规则支持多个, 用换行符分割. 如果匹配, 则返回匹配的规则表达式行.
* @param testRuleMatcherRequest
* 测试规则匹配请求
*
* @return 匹配成功的第一个表达式
*/
@PostMapping("/rule-test")
public AjaxJson testRule(@RequestBody @Valid TestRuleMatcherRequest testRuleMatcherRequest) {
if (testRuleMatcherRequest == null) {
return AjaxJson.getSuccessData(null);
}
String rules = testRuleMatcherRequest.getRules();
String testValue = testRuleMatcherRequest.getTestValue();
if (StringUtils.isBlank(testValue) || StringUtils.isBlank(rules)) {
return AjaxJson.getSuccessData(null);
}
List ruleList = StringUtils.split(rules, StringUtils.LF);
IRuleMatcher ipRuleMatcher = RuleMatcherFactory.getRuleMatcher(testRuleMatcherRequest.getRuleType());
return AjaxJson.getSuccessData(ipRuleMatcher.matchAnyReturnFirst(ruleList, testValue));
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/admin/model/request/TestRuleMatcherRequest.java
================================================
package im.zhaojun.zfile.module.admin.model.request;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
@Data
public class TestRuleMatcherRequest {
@NotBlank(message = "规则类型不能为空")
private String ruleType;
@NotBlank(message = "规则不能为空")
private String rules;
@NotBlank(message = "测试值不能为空")
private String testValue;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/annotation/JSONStringParse.java
================================================
package im.zhaojun.zfile.module.config.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标注是否按 JSON 字符串解析
*
* @author zhaojun
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JSONStringParse {
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/constant/SystemConfigConstant.java
================================================
package im.zhaojun.zfile.module.config.constant;
/**
* 系统设置字段常量.
*
* @author zhaojun
*/
public class SystemConfigConstant {
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String LOGIN_VERIFY_MODE = "loginVerifyMode";
// 这里名称和值不一样是历史遗留问题,最开始设计时弄混了名称,实际使用的是 aes
public static final String AES_HEX_KEY = "rsaHexKey";
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/controller/SettingController.java
================================================
package im.zhaojun.zfile.module.config.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.config.ZFileProperties;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.model.request.*;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
/**
* 站点设定值接口
*
* @author zhaojun
*/
@Tag(name = "站点设置模块")
@ApiSort(2)
@RestController
@RequestMapping("/admin")
public class SettingController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ZFileProperties zFileProperties;
@ApiOperationSupport(order = 1)
@Operation(summary = "获取站点信息", description = "获取站点相关信息,如站点名称,风格样式,是否显示公告,是否显示文档区,自定义 CSS,JS 等参数")
@GetMapping("/config")
public AjaxJson getConfig() {
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
if (zFileProperties != null && zFileProperties.isDemoSite()) {
SystemConfigDTO copy = JSON.parseObject(JSON.toJSONString(systemConfigDTO), SystemConfigDTO.class);
copy.setAuthCode(null);
copy.setRsaHexKey(null);
return AjaxJson.getSuccessData(copy);
}
return AjaxJson.getSuccessData(systemConfigDTO);
}
@ApiOperationSupport(order = 3)
@Operation(summary = "修改站点设置")
@PutMapping("/config/site")
@DemoDisable
public AjaxJson updateSiteSetting(@Valid @RequestBody UpdateSiteSettingRequest settingRequest) {
if (StrUtil.length(settingRequest.getAuthCode()) > 36 && StrUtil.length(settingRequest.getAuthCode()) < 100) {
throw new BizException("授权码长度异常,请检查是否额外复制了空格或特殊字符!");
}
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(settingRequest, systemConfigDTO);
systemConfigService.updateSystemConfig(systemConfigDTO);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 4)
@Operation(summary = "修改显示设置")
@PutMapping("/config/view")
@DemoDisable
public AjaxJson updateViewSetting(@Valid @RequestBody UpdateViewSettingRequest settingRequest) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(settingRequest, systemConfigDTO);
systemConfigService.updateSystemConfig(systemConfigDTO);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 5)
@Operation(summary = "修改登陆安全设置")
@PutMapping("/config/security")
@DemoDisable
public AjaxJson updateSecuritySetting(@Valid @RequestBody UpdateSecuritySettingRequest settingRequest) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(settingRequest, systemConfigDTO);
if (BooleanUtils.isNotTrue(settingRequest.getAdminTwoFactorVerify())) {
systemConfigDTO.setLoginVerifySecret("");
}
systemConfigService.updateSystemConfig(systemConfigDTO);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 6)
@Operation(summary = "修改直链设置")
@PutMapping("/config/link")
@DemoDisable
public AjaxJson updateLinkSetting(@Valid @RequestBody UpdateLinkSettingRequest settingRequest) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(settingRequest, systemConfigDTO);
systemConfigService.updateSystemConfig(systemConfigDTO);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 7)
@Operation(summary = "修改访问控制设置")
@PutMapping("/config/access")
@DemoDisable
public AjaxJson updateSecuritySetting(@Valid @RequestBody UpdateAccessSettingRequest updateAccessSettingRequest) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(updateAccessSettingRequest, systemConfigDTO);
systemConfigService.updateSystemConfig(systemConfigDTO);
return AjaxJson.getSuccess();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/controller/SiteController.java
================================================
package im.zhaojun.zfile.module.config.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.config.ZFileProperties;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.core.util.ZFileAuthUtil;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.model.result.FrontSiteConfigResult;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.storage.annotation.ProCheck;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.model.request.base.FileListConfigRequest;
import im.zhaojun.zfile.module.storage.model.result.StorageSourceConfigResult;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import im.zhaojun.zfile.module.storage.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.module.user.model.constant.UserConstant;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.*;
import java.util.Objects;
/**
* 面向前台的站点基础模块接口
*
* @author zhaojun
*/
@Tag(name = "站点基础模块")
@ApiSort(1)
@Slf4j
@RequestMapping("/api/site")
@RestController
public class SiteController {
@Resource
private ZFileProperties zFileProperties;
@Resource
private StorageSourceService storageSourceService;
@Resource
private SystemConfigService systemConfigService;
@ApiOperationSupport(order = 1)
@Operation(summary = "获取站点全局设置", description = "获取站点全局设置, 包括是否页面布局、列表尺寸、公告、配置信息")
@GetMapping("/config/global")
@ProCheck
public AjaxJson globalConfig() {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
FrontSiteConfigResult frontSiteConfigResult = new FrontSiteConfigResult();
BeanUtils.copyProperties(systemConfig, frontSiteConfigResult);
frontSiteConfigResult.setDebugMode(zFileProperties.isDebug());
boolean guestUser = Objects.equals(ZFileAuthUtil.getCurrentUserId(), UserConstant.ANONYMOUS_ID);
boolean guestIndexNotBlank = StringUtils.isNotBlank(systemConfig.getGuestIndexHtml());
frontSiteConfigResult.setGuest(guestUser && guestIndexNotBlank);
return AjaxJson.getSuccessData(frontSiteConfigResult);
}
@ApiOperationSupport(order = 2)
@Operation(summary = "获取存储源设置", description = "获取某个存储源的设置信息, 包括是否启用, 名称, 存储源类型, 存储源配置信息")
@PostMapping("/config/storage")
public AjaxJson storageList(@Valid @RequestBody FileListConfigRequest fileListConfigRequest) {
StorageSourceConfigResult storageSourceConfigResult = storageSourceService.getStorageConfigSource(fileListConfigRequest);
return AjaxJson.getSuccessData(storageSourceConfigResult);
}
@ApiOperationSupport(order = 3)
@Operation(summary = "获取用户存储源路径", description = "获取用户存储源路径")
@GetMapping("/config/userRootPath/{storageKey}")
public AjaxJson getUserRootPath(@PathVariable("storageKey") String storageKey) {
AbstractBaseFileService> baseFileService = StorageSourceContext.getByStorageKey(storageKey);
if (baseFileService == null || baseFileService.getCurrentUserBasePath() == null) {
return AjaxJson.getSuccessData("");
}
return AjaxJson.getSuccessData(baseFileService.getCurrentUserBasePath());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/event/DirectLinkPrefixModifyHandler.java
================================================
package im.zhaojun.zfile.module.config.event;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import im.zhaojun.zfile.module.link.controller.DirectLinkController;
import im.zhaojun.zfile.module.link.service.DynamicDirectLinkPrefixService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
/**
* 接收系统设置修改事件, 修改直链前缀时, 动态更新直链前缀.
*
* @author zhaojun
*/
@Slf4j
@Component
public class DirectLinkPrefixModifyHandler implements ISystemConfigModifyHandler {
@Resource
private DynamicDirectLinkPrefixService dynamicDirectLinkPrefixService;
@Override
public void modify(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
String oldValue = originalSystemConfig.getValue();
String newValue = newSystemConfig.getValue();
if (StringUtils.equals(oldValue, newValue)) {
log.info("检测到修改了直链前缀, 但是新值和旧值相同, 不做处理.");
return;
}
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(newValue + DirectLinkController.DIRECT_LINK_SUFFIX_PATH).build();
dynamicDirectLinkPrefixService.updateRegisterMappingHandler(SystemConfig.DIRECT_LINK_PREFIX_NAME, requestMappingInfo);
log.info("检测到修改了直链前缀, [{}] -> [{}], 已自动更新直链前缀.", oldValue, newValue);
}
@Override
public boolean matches(String name) {
return SystemConfig.DIRECT_LINK_PREFIX_NAME.equals(name);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/event/ISystemConfigModifyHandler.java
================================================
package im.zhaojun.zfile.module.config.event;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
/**
* 系统设置修改事件
*
* @author zhaojun
*/
public interface ISystemConfigModifyHandler {
/**
* 修改系统设置时会触发此事件
*
*/
void modify(SystemConfig originalSystemConfig, SystemConfig newSystemConfig);
/**
* 判断是否匹配当前处理器
*
* @param name
* 配置项名称
*
* @return 是否匹配
*/
boolean matches(String name);
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/event/SecureLoginEntryModifyHandler.java
================================================
package im.zhaojun.zfile.module.config.event;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import im.zhaojun.zfile.module.user.service.DynamicLoginEntryService;
import im.zhaojun.zfile.module.user.util.LoginEntryPathUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
/**
* 监听安全登录入口配置变更,动态更新登录接口映射。
*
* @author zhaojun
*/
@Slf4j
@Component
public class SecureLoginEntryModifyHandler implements ISystemConfigModifyHandler {
@Resource
private DynamicLoginEntryService dynamicLoginEntryService;
@Override
public void modify(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
String oldPath = LoginEntryPathUtils.resolveLoginPath(originalSystemConfig.getValue());
String newPath = LoginEntryPathUtils.resolveLoginPath(newSystemConfig.getValue());
if (StringUtils.equals(oldPath, newPath)) {
log.info("检测到修改安全登录入口,但实际登录路径未变化,跳过处理。");
return;
}
RequestMappingInfo requestMappingInfo = dynamicLoginEntryService.buildLoginRequestMappingInfo(newSystemConfig.getValue());
dynamicLoginEntryService.updateRegisterMappingHandler(SystemConfig.SECURE_LOGIN_ENTRY_NAME, requestMappingInfo);
log.info("安全登录入口已更新,{} -> {}", oldPath, newPath);
}
@Override
public boolean matches(String name) {
return SystemConfig.SECURE_LOGIN_ENTRY_NAME.equals(name);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandlerChain.java
================================================
package im.zhaojun.zfile.module.config.event;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List;
/**
* @author zhaojun
*/
@Component
public class SystemConfigModifyHandlerChain {
@Resource
private List handlers;
public void execute(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
handlers.stream()
.filter(handler -> handler.matches(originalSystemConfig.getName()))
.forEach(handler -> handler.modify(originalSystemConfig, newSystemConfig));
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/mapper/SystemConfigMapper.java
================================================
package im.zhaojun.zfile.module.config.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 系统配置 Mapper 接口
*
* @author zhaojun
*/
@Mapper
public interface SystemConfigMapper extends BaseMapper {
/**
* 获取所有系统设置
*
* @return 系统设置列表
*/
List findAll();
/**
* 根据系统设置名称获取设置信息
*
* @param name
* 系统设置名称
*
* @return 系统设置信息
*/
SystemConfig findByName(@Param("name")String name);
/**
* 批量保存系统设置
*
* @param list
* 系统设置列表
*
* @return 保存记录数
*/
int saveAll(@Param("list")List list);
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/dto/LinkExpireDTO.java
================================================
package im.zhaojun.zfile.module.config.model.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class LinkExpireDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Integer value;
private String unit;
private Long seconds;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/dto/SystemConfigDTO.java
================================================
package im.zhaojun.zfile.module.config.model.dto;
import com.fasterxml.jackson.annotation.JsonIgnore;
import im.zhaojun.zfile.module.config.annotation.JSONStringParse;
import im.zhaojun.zfile.module.config.model.enums.FileClickModeEnum;
import im.zhaojun.zfile.module.link.model.enums.RefererTypeEnum;
import im.zhaojun.zfile.module.user.model.enums.LoginLogModeEnum;
import im.zhaojun.zfile.module.user.model.enums.LoginVerifyModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 系统设置传输类
*
* @author zhaojun
*/
@Data
@Schema(description = "系统设置类")
public class SystemConfigDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(title = "站点名称", example = "ZFile Site Name")
private String siteName;
@Schema(title = "用户名", example = "admin")
@Deprecated
private String username;
@Schema(title = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
private String avatar;
@Schema(title = "备案号", example = "冀ICP备12345678号-1")
private String icp;
@JsonIgnore
@Deprecated
private String password;
@Schema(title = "自定义 JS")
private String customJs;
@Schema(title = "自定义 CSS")
private String customCss;
@Schema(title = "列表尺寸", description ="large:大,default:中,small:小", example = "default")
private String tableSize;
@Schema(title = "是否显示文档区", example = "true")
private Boolean showDocument;
@Schema(title = "网站公告", example = "ZFile 网站公告")
private String announcement;
@Schema(title = "是否显示网站公告", example = "true")
private Boolean showAnnouncement;
@Schema(title = "页面布局", description ="full:全屏,center:居中", example = "full")
private String layout;
@Schema(title = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
private String mobileLayout;
@Schema(title = "移动端显示文件大小", description = "仅适用列表视图", example = "true")
private Boolean mobileShowSize;
@Schema(title = "是否显示生成直链功能(含直链和路径短链)", example = "true")
private Boolean showLinkBtn;
@Schema(title = "是否显示生成短链功能", example = "true")
private Boolean showShortLink;
@Schema(title = "是否显示生成路径链接功能", example = "true")
private Boolean showPathLink;
@Schema(title = "是否已初始化", example = "true")
private Boolean installed;
@Schema(title = "自定义视频文件后缀格式")
private String customVideoSuffix;
@Schema(title = "自定义图像文件后缀格式")
private String customImageSuffix;
@Schema(title = "自定义音频文件后缀格式")
private String customAudioSuffix;
@Schema(title = "自定义文本文件后缀格式")
private String customTextSuffix;
@Schema(title = "自定义Office后缀格式")
private String customOfficeSuffix;
@Schema(title = "自定义kkFileView后缀格式")
private String customKkFileViewSuffix;
@Schema(title = "直链地址前缀")
private String directLinkPrefix;
@Schema(title = "直链 Referer 防盗链类型")
private RefererTypeEnum refererType;
@Schema(title = "是否记录下载日志", example = "true")
private Boolean recordDownloadLog;
@Schema(title = "直链 Referer 是否允许为空")
private Boolean refererAllowEmpty;
@Schema(title = "直链 Referer 值")
private String refererValue;
/**
* 废弃的字段,改为使用 {@link #adminTwoFactorVerify} 和 {@link #loginVerifySecret} 代替
*/
@Schema(title = "管理员登陆验证方式,目前仅支持 2FA 认证或关闭")
@Deprecated
private LoginVerifyModeEnum loginVerifyMode;
@Schema(title = "登陆验证 Secret")
private String loginVerifySecret;
@Schema(title = "是否启用登陆验证码", example = "true")
private Boolean loginImgVerify;
@Schema(title = "是否为管理员启用双因素认证", example = "true")
private Boolean adminTwoFactorVerify;
@Schema(title = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean rootShowStorage;
@Schema(title = "强制后端地址", description ="强制指定生成直链,短链,获取回调地址时的地址。", example = "http://xxx.example.com")
private String forceBackendAddress;
@Schema(title = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
private String frontDomain;
@Schema(title = "是否在前台显示登陆按钮", example = "true")
private Boolean showLogin;
@Schema(title = "安全登录入口", description = "用于隐藏默认登录地址的安全入口,不包含 '/'", example = "admin")
private String secureLoginEntry;
@Schema(title = "登录日志模式", example = "all")
private LoginLogModeEnum loginLogMode;
@Schema(title = "RAS Hex Key", example = "r2HKbzc1DfvOs5uHhLn7pA==")
private String rsaHexKey;
@Schema(title = "默认文件点击习惯", example = "click")
private FileClickModeEnum fileClickMode;
@Schema(title = "移动端默认文件点击习惯", example = "click")
private FileClickModeEnum mobileFileClickMode;
@Schema(title = "授权码", example = "e619510f-cdcd-f657-6c5e-2d12e9a28ae5")
private String authCode;
@Schema(title = "最大同时上传文件数", example = "5")
private Integer maxFileUploads;
@Schema(title = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
private String onlyOfficeUrl;
@Schema(title = "onlyOffice Secret", example = "X9rBGypwWE86Lca8e4Mo55iHFoiyh9ed")
private String onlyOfficeSecret;
@Schema(title = "kkFileView 在线预览地址", example = "http://kkfile.zfile.vip")
private String kkFileViewUrl;
@Schema(title = "kkFileView 预览方式", example = "iframe/newTab")
private String kkFileViewOpenMode;
@Schema(title = "启用 WebDAV", example = "true")
private Boolean webdavEnable;
@Schema(title = "WebDAV 服务器中转下载", example = "true")
private Boolean webdavProxy;
@Schema(title = "WebDAV 匿名用户访问", example = "true")
private Boolean webdavAllowAnonymous;
@Schema(title = "WebDAV 账号", example = "admin")
private String webdavUsername;
@Schema(title = "WebDAV 密码", example = "123456")
private String webdavPassword;
@Schema(title = "是否允许路径直链可直接访问", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean allowPathLinkAnonAccess;
@Schema(title = "默认最大显示文件数", example = "1000")
private Integer maxShowSize;
@Schema(title = "每次加载更多文件数", example = "50")
private Integer loadMoreSize;
@Schema(title = "默认排序字段", example = "name")
private String defaultSortField;
@Schema(title = "默认排序方向", example = "asc")
private String defaultSortOrder;
@Schema(title = "站点 Home 名称", example = "xxx 的小站")
private String siteHomeName;
@Schema(title = "站点 Home Logo", example = "true")
private String siteHomeLogo;
@Schema(title = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
private String siteHomeLogoLink;
@Schema(title = "站点 Logo 链接打开方式", example = "_blank")
private String siteHomeLogoTargetMode;
@Schema(title = "管理员页面点击 Logo 回到首页打开方式", example = "_blank")
private String siteAdminLogoTargetMode;
@Schema(title = "管理员页面点击版本号打开更新日志", example = "true")
private Boolean siteAdminVersionOpenChangeLog;
@Schema(title = "限制直链下载秒数", example = "_blank")
private Integer linkLimitSecond;
@Schema(title = "限制直链下载次数", example = "_blank")
private Integer linkDownloadLimit;
@Schema(title = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
private String faviconUrl;
@Schema(title = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
@JSONStringParse
private List linkExpireTimes;
@Schema(title = "是否默认记住密码", example = "true")
private Boolean defaultSavePwd;
@Schema(title = "普通下载是否启用确认弹窗", example = "true")
private Boolean enableNormalDownloadConfirm;
@Schema(title = "打包下载是否启用确认弹窗", example = "true")
private Boolean enablePackageDownloadConfirm;
@Schema(title = "批量下载是否启用确认弹窗", example = "true")
private Boolean enableBatchDownloadConfirm;
/**
* 废弃的字段,不再使用悬浮菜单
*/
@Deprecated
@Schema(title = "是否启用 hover 菜单", example = "true")
private Boolean enableHoverMenu;
@Schema(title = "访问 ip 黑名单", example = "162.13.1.0/24\n192.168.1.1")
private String accessIpBlocklist;
@Schema(title = "访问 ua 黑名单", example = "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36*")
private String accessUaBlocklist;
@Schema(title = "匿名用户首页显示内容")
private String guestIndexHtml;
public String getAnnouncement() {
return announcement == null ? "" : announcement;
}
public List getLinkExpireTimes() {
if (linkExpireTimes == null) {
LinkExpireDTO linkExpireDTO = new LinkExpireDTO();
linkExpireDTO.setValue(1);
linkExpireDTO.setUnit("d");
linkExpireDTO.setSeconds(86400L);
linkExpireTimes = new ArrayList<>();
linkExpireTimes.add(linkExpireDTO);
}
return linkExpireTimes;
}
public String getLayout() {
return layout == null ? "full" : layout;
}
public String getMobileLayout() {
return mobileLayout == null ? getLayout() : mobileLayout;
}
/**
* 获取普通下载是否启用确认弹窗配置.
*
* @return 若为空则返回 true
*/
public Boolean getEnableNormalDownloadConfirm() {
return enableNormalDownloadConfirm == null ? Boolean.TRUE : enableNormalDownloadConfirm;
}
/**
* 获取打包下载是否启用确认弹窗配置.
*
* @return 若为空则返回 true
*/
public Boolean getEnablePackageDownloadConfirm() {
return enablePackageDownloadConfirm == null ? Boolean.TRUE : enablePackageDownloadConfirm;
}
/**
* 获取批量下载是否启用确认弹窗配置.
*
* @return 若为空则返回 true
*/
public Boolean getEnableBatchDownloadConfirm() {
return enableBatchDownloadConfirm == null ? Boolean.TRUE : enableBatchDownloadConfirm;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/entity/SystemConfig.java
================================================
package im.zhaojun.zfile.module.config.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 系统设置 entity
*
* @author zhaojun
*/
@Data
@Schema(description = "系统设置")
@TableName(value = "system_config")
public class SystemConfig implements Serializable {
public static final String DIRECT_LINK_PREFIX_NAME = "directLinkPrefix";
public static final String SECURE_LOGIN_ENTRY_NAME = "secureLoginEntry";
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@Schema(title = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "name")
@Schema(title = "系统设置名称", example = "siteName")
private String name;
@TableField(value = "`value`")
@Schema(title = "系统设置值", example = "ZFile 演示站")
private String value;
@TableField(value = "title")
@Schema(title = "系统设置描述", example = "站点名称")
private String title;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/enums/FileClickModeEnum.java
================================================
package im.zhaojun.zfile.module.config.model.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 文件点击模式枚举, 用于控制文件是单击打开还是双击打开
*
* @author zhaojun
*/
@Getter
@AllArgsConstructor
public enum FileClickModeEnum {
/**
* 单击打开文件/文件夹
*/
CLICK("click"),
/**
* 双击打开文件/文件夹
*/
DBCLICK("dbclick");
@EnumValue
@JsonValue
private final String value;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateAccessSettingRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 站点访问控制请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "站点访问控制参数类")
public class UpdateAccessSettingRequest {
@Schema(title = "访问 ip 黑名单", example = "162.13.1.0/24\n192.168.1.1")
private String accessIpBlocklist;
@Schema(title = "访问 ua 黑名单", example = "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36*")
private String accessUaBlocklist;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateLinkSettingRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import im.zhaojun.zfile.module.config.model.dto.LinkExpireDTO;
import im.zhaojun.zfile.module.link.model.enums.RefererTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
/**
* 直链设置请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "直链设置请求参数类")
public class UpdateLinkSettingRequest {
@Schema(title = "是否记录下载日志", example = "true")
private Boolean recordDownloadLog;
@Schema(title = "直链 Referer 防盗链类型")
private RefererTypeEnum refererType;
@Schema(title = "直链 Referer 是否允许为空")
private Boolean refererAllowEmpty;
@Schema(title = "直链 Referer 值")
private String refererValue;
@Schema(title = "直链地址前缀")
@NotBlank(message = "直链地址前缀不能为空")
private String directLinkPrefix;
@Schema(title = "是否显示生成直链功能(含直链和路径短链)", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showLinkBtn;
@Schema(title = "是否显示生成短链功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showShortLink;
@Schema(title = "是否显示生成路径链接功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showPathLink;
@Schema(title = "是否允许路径直链可直接访问", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean allowPathLinkAnonAccess;
@Schema(title = "限制直链下载秒数", example = "_blank")
private Integer linkLimitSecond;
@Schema(title = "限制直链下载次数", example = "_blank")
private Integer linkDownloadLimit;
@Schema(title = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
private List linkExpireTimes;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSecuritySettingRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import im.zhaojun.zfile.module.user.model.enums.LoginLogModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 登陆安全设置请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "登陆安全设置请求参数类")
public class UpdateSecuritySettingRequest {
@Schema(title = "是否在前台显示登陆按钮", example = "true")
private Boolean showLogin;
@Schema(title = "安全登录入口", description = "仅允许字母、数字、短横线和下划线,长度不超过 32", example = "admin")
@Size(max = 32, message = "安全登录入口长度不能超过 32 个字符")
@Pattern(regexp = "^[A-Za-z0-9_-]*$", message = "安全登录入口只能包含字母、数字、短横线和下划线")
private String secureLoginEntry;
@Schema(title = "登录日志模式", example = "all")
private LoginLogModeEnum loginLogMode;
@Schema(title = "是否启用登陆验证码", example = "true")
private Boolean loginImgVerify;
@Schema(title = "是否为管理员启用双因素认证", example = "true")
private Boolean adminTwoFactorVerify;
@Schema(title = "2FA登陆验证 Secret")
private String loginVerifySecret;
@Schema(title = "匿名用户首页显示内容")
private String guestIndexHtml;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSiteSettingRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 站点设置请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "站点设置请求参数类")
public class UpdateSiteSettingRequest {
@Schema(title = "站点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZFile Site Name")
@NotBlank(message = "站点名称不能为空")
private String siteName;
@Schema(title = "强制后端地址", description ="强制指定生成直链,短链,获取回调地址时的地址。", example = "http://xxx.example.com")
private String forceBackendAddress;
@Schema(title = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
private String frontDomain;
@Schema(title = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
private String avatar;
@Schema(title = "备案号", example = "冀ICP备12345678号-1")
private String icp;
@Schema(title = "授权码", example = "e619510f-cdcd-f657-6c5e-2d12e9a28ae5")
private String authCode;
@Schema(title = "最大同时上传文件数", example = "5")
private Integer maxFileUploads;
@Schema(title = "站点 Home 名称", example = "xxx 的小站")
private String siteHomeName;
@Schema(title = "站点 Home Logo", example = "true")
private String siteHomeLogo;
@Schema(title = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
private String siteHomeLogoLink;
@Schema(title = "站点 Logo 链接打开方式", example = "_blank")
private String siteHomeLogoTargetMode;
@Schema(title = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
private String faviconUrl;
@Schema(title = "管理员页面点击 Logo 回到首页打开方式", example = "_blank")
private String siteAdminLogoTargetMode;
@Schema(title = "管理员页面点击版本号打开更新日志", example = "true")
private Boolean siteAdminVersionOpenChangeLog;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateUserNameAndPasswordRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 用户修改密码请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "用户修改密码请求参数类")
public class UpdateUserNameAndPasswordRequest {
@Schema(title = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(title = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateViewSettingRequest.java
================================================
package im.zhaojun.zfile.module.config.model.request;
import im.zhaojun.zfile.module.config.model.enums.FileClickModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 显示设置请求参数类
*
* @author zhaojun
*/
@Data
@Schema(description = "显示设置请求参数类")
public class UpdateViewSettingRequest {
@Schema(title = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean rootShowStorage;
@Schema(title = "页面布局", description ="full:全屏,center:居中", example = "full", requiredMode = Schema.RequiredMode.REQUIRED)
private String layout;
@Schema(title = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
private String mobileLayout;
@Schema(title = "移动端显示文件大小", description = "仅适用列表视图", example = "true")
private Boolean mobileShowSize;
@Schema(title = "列表尺寸", description ="large:大,default:中,small:小", example = "default", requiredMode = Schema.RequiredMode.REQUIRED)
private String tableSize;
@Schema(title = "自定义视频文件后缀格式")
private String customVideoSuffix;
@Schema(title = "自定义图像文件后缀格式")
private String customImageSuffix;
@Schema(title = "自定义音频文件后缀格式")
private String customAudioSuffix;
@Schema(title = "自定义文本文件后缀格式")
private String customTextSuffix;
@Schema(title = "自定义Office后缀格式")
private String customOfficeSuffix;
@Schema(title = "自定义kkFileView后缀格式")
private String customKkFileViewSuffix;
@Schema(title = "是否显示文档区", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showDocument;
@Schema(title = "是否显示网站公告", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showAnnouncement;
@Schema(title = "网站公告", example = "ZFile 网站公告")
private String announcement;
@Schema(title = "自定义 CSS")
private String customCss;
@Schema(title = "自定义 JS")
private String customJs;
@Schema(title = "默认文件点击习惯", example = "click")
private FileClickModeEnum fileClickMode;
@Schema(title = "移动端默认文件点击习惯", example = "click")
private FileClickModeEnum mobileFileClickMode;
@Schema(title = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
private String onlyOfficeUrl;
@Schema(title = "onlyOffice Secret", example = "X9rBGypwWE86Lca8e4Mo55iHFoiyh9ed")
private String onlyOfficeSecret;
@Schema(title = "kkFileView 在线预览地址", example = "http://kkfile.zfile.vip")
private String kkFileViewUrl;
@Schema(title = "kkFileView 预览方式", example = "iframe/newTab")
private String kkFileViewOpenMode;
@Schema(title = "默认最大显示文件数", example = "1000")
private Integer maxShowSize;
@Schema(title = "每次加载更多文件数", example = "50")
private Integer loadMoreSize;
@Schema(title = "默认排序字段", example = "name")
private String defaultSortField;
@Schema(title = "默认排序方向", example = "asc")
private String defaultSortOrder;
@Schema(title = "是否默认记住密码", example = "true")
private Boolean defaultSavePwd;
@Schema(title = "普通下载是否启用确认弹窗", example = "true")
private Boolean enableNormalDownloadConfirm;
@Schema(title = "打包下载是否启用确认弹窗", example = "true")
private Boolean enablePackageDownloadConfirm;
@Schema(title = "批量下载是否启用确认弹窗", example = "true")
private Boolean enableBatchDownloadConfirm;
/**
* 废弃的字段,不再使用悬浮菜单
*/
@Deprecated
@Schema(title = "是否启用 hover 菜单", example = "true")
private Boolean enableHoverMenu;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/model/result/FrontSiteConfigResult.java
================================================
package im.zhaojun.zfile.module.config.model.result;
import im.zhaojun.zfile.module.config.model.dto.LinkExpireDTO;
import im.zhaojun.zfile.module.config.model.enums.FileClickModeEnum;
import im.zhaojun.zfile.module.user.model.enums.LoginLogModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
* 全局站点设置响应类
*
* @author zhaojun
*/
@Data
@Schema(title="全局站点设置响应类")
public class FrontSiteConfigResult {
@Schema(title = "是否已初始化", example = "true")
private Boolean installed;
@Schema(title = "Debug 模式", example = "true", description ="开启 debug 模式后,可重置管理员密码")
private Boolean debugMode;
@Schema(title = "直链地址前缀", example = "true", description ="直链地址前缀, 如 http(s)://ip:port/${直链前缀}/path/filename")
private String directLinkPrefix;
@Schema(title = "站点名称", example = "ZFile Site Name")
private String siteName;
@Schema(title = "备案号", example = "冀ICP备12345678号-1")
private String icp;
@Schema(title = "页面布局", description ="full:全屏,center:居中", example = "full", requiredMode = Schema.RequiredMode.REQUIRED)
private String layout;
@Schema(title = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
private String mobileLayout;
@Schema(title = "移动端显示文件大小", description = "仅适用列表视图", example = "true")
private Boolean mobileShowSize;
@Schema(title = "列表尺寸", description ="large:大,default:中,small:小", example = "default", requiredMode = Schema.RequiredMode.REQUIRED)
private String tableSize;
@Schema(title = "是否显示生成直链功能(含直链和路径短链)", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showLinkBtn;
@Schema(title = "是否显示生成短链功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showShortLink;
@Schema(title = "是否显示生成路径链接功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showPathLink;
@Schema(title = "是否显示文档区", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showDocument;
@Schema(title = "是否显示网站公告", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showAnnouncement;
@Schema(title = "网站公告", example = "ZFile 网站公告")
private String announcement;
@Schema(title = "自定义 JS")
private String customJs;
@Schema(title = "自定义 CSS")
private String customCss;
@Schema(title = "自定义视频文件后缀格式")
private String customVideoSuffix;
@Schema(title = "自定义图像文件后缀格式")
private String customImageSuffix;
@Schema(title = "自定义音频文件后缀格式")
private String customAudioSuffix;
@Schema(title = "自定义文本文件后缀格式")
private String customTextSuffix;
@Schema(title = "自定义Office后缀格式")
private String customOfficeSuffix;
@Schema(title = "自定义kkFileView后缀格式")
private String customKkFileViewSuffix;
@Schema(title = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean rootShowStorage;
@Schema(title = "强制后端地址", description ="强制指定生成直链,短链,获取回调地址时的地址。", example = "http://xxx.example.com")
private String forceBackendAddress;
@Schema(title = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
private String frontDomain;
@Schema(title = "是否在前台显示登陆按钮", example = "true")
private Boolean showLogin;
@Schema(title = "登录日志模式", example = "all")
private LoginLogModeEnum loginLogMode;
@Schema(title = "默认文件点击习惯", example = "click")
private FileClickModeEnum fileClickMode;
@Schema(title = "移动端默认文件点击习惯", example = "click")
private FileClickModeEnum mobileFileClickMode;
@Schema(title = "最大同时上传文件数", example = "5")
private Integer maxFileUploads;
@Schema(title = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
private String onlyOfficeUrl;
@Schema(title = "kkFileView 在线预览地址", example = "http://kkfile.zfile.vip")
private String kkFileViewUrl;
@Schema(title = "kkFileView 预览方式", example = "iframe/newTab")
private String kkFileViewOpenMode;
@Schema(title = "默认最大显示文件数", example = "1000")
private Integer maxShowSize;
@Schema(title = "每次加载更多文件数", example = "50")
private Integer loadMoreSize;
@Schema(title = "默认排序字段", example = "name")
private String defaultSortField;
@Schema(title = "默认排序方向", example = "asc")
private String defaultSortOrder;
@Schema(title = "站点 Home 名称", example = "xxx 的小站")
private String siteHomeName;
@Schema(title = "站点 Home Logo", example = "true")
private String siteHomeLogo;
@Schema(title = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
private String siteHomeLogoLink;
@Schema(title = "站点 Logo 链接打开方式", example = "_blank")
private String siteHomeLogoTargetMode;
@Schema(title = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
private List linkExpireTimes;
@Schema(title = "是否默认记住密码", example = "true")
private Boolean defaultSavePwd;
@Schema(title = "普通下载是否启用确认弹窗", example = "true")
private Boolean enableNormalDownloadConfirm;
@Schema(title = "打包下载是否启用确认弹窗", example = "true")
private Boolean enablePackageDownloadConfirm;
@Schema(title = "批量下载是否启用确认弹窗", example = "true")
private Boolean enableBatchDownloadConfirm;
/**
* 废弃的字段,不再使用悬浮菜单
*/
@Deprecated
@Schema(title = "是否启用 hover 菜单", example = "true")
private Boolean enableHoverMenu;
@Schema(title = "是否是游客", example = "true")
private boolean guest;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/config/service/SystemConfigService.java
================================================
package im.zhaojun.zfile.module.config.service;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.convert.ConvertException;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import im.zhaojun.zfile.core.util.*;
import im.zhaojun.zfile.module.config.annotation.JSONStringParse;
import im.zhaojun.zfile.module.config.constant.SystemConfigConstant;
import im.zhaojun.zfile.module.config.event.SystemConfigModifyHandlerChain;
import im.zhaojun.zfile.module.config.mapper.SystemConfigMapper;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import im.zhaojun.zfile.module.user.model.enums.LoginVerifyModeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.Field;
import java.util.*;
import static im.zhaojun.zfile.module.config.service.SystemConfigService.CACHE_NAME;
/**
* 系统设置 Service
*
* @author zhaojun
*/
@Slf4j
@Service
@CacheConfig(cacheNames = CACHE_NAME)
public class SystemConfigService {
public static final String CACHE_NAME = "systemConfig";
private static final String SERIAL_VERSION_UID_FIELD_NAME = "serialVersionUID";
@Resource
private SystemConfigMapper systemConfigMapper;
@Resource
private CacheManager cacheManager;
@Resource
private SystemConfigModifyHandlerChain systemConfigModifyHandlerChain;
private final Class systemConfigClazz = SystemConfigDTO.class;
public static final List ignoreFieldList = Arrays.asList("domain");
/**
* 获取系统设置, 如果缓存中有, 则去缓存取, 没有则查询数据库并写入到缓存中.
*
* @return 系统设置
*/
@Cacheable(key = "'dto'")
public SystemConfigDTO getSystemConfig() {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
List systemConfigList = systemConfigMapper.findAll();
for (SystemConfig systemConfig : systemConfigList) {
String key = systemConfig.getName();
if (ignoreFieldList.contains(key)) {
if (log.isTraceEnabled()) {
log.trace("从数据库加载字段填充到 DTO 时,忽略字段: {}", key);
}
continue;
}
try {
Field field = systemConfigClazz.getDeclaredField(key);
field.setAccessible(true);
String strVal = systemConfig.getValue();
Class> fieldType = field.getType();
Object convertVal;
if (EnumUtil.isEnum(fieldType)) {
convertVal = EnumConvertUtils.convertStrToEnum(fieldType, strVal);
} else if (field.isAnnotationPresent(JSONStringParse.class)) {
// 如果类是 Collection 类型, 则需要将 JSON 字符串转换为 List
if (Collection.class.isAssignableFrom(fieldType)) {
Class> genericType = ClassUtils.getGenericType(field);
convertVal = JSONArray.parseArray(strVal, genericType);
} else {
// 否则转换为普通对象
convertVal = JSONObject.parseObject(strVal, fieldType);
}
} else {
convertVal = Convert.convert(fieldType, strVal);
}
field.set(systemConfigDTO, convertVal);
} catch (NoSuchFieldException | IllegalAccessException | ConvertException e) {
log.error("通过反射, 将字段 {} 注入 SystemConfigDTO 时出现异常:", key, e);
}
}
return systemConfigDTO;
}
/**
* 更新系统设置, 并清空缓存中的内容.
*
* @param systemConfigDTO
* 系统设置 dto
*/
@Transactional(rollbackFor = Exception.class)
@CacheEvict(allEntries = true)
public synchronized void updateSystemConfig(SystemConfigDTO systemConfigDTO) {
// 获取更新前的值
List systemConfigListInDb = systemConfigMapper.findAll();
Map systemConfigMapInDb = CollectionUtils.toMap(systemConfigListInDb, null, SystemConfig::getName);
// 存储更新后的值
List updateSystemConfigList = new ArrayList<>();
Field[] fields = systemConfigClazz.getDeclaredFields();
for (Field field : fields) {
// 获取数据库中的值对象
String key = field.getName();
if (SERIAL_VERSION_UID_FIELD_NAME.equals(key)) {
continue;
}
SystemConfig systemConfig = systemConfigMapInDb.get(key);
if (systemConfig != null) {
field.setAccessible(true);
Object val = null;
try {
val = field.get(systemConfigDTO);
} catch (IllegalAccessException e) {
log.error("通过反射, 从 SystemConfigDTO 获取字段 {} 时出现异常:", key, e);
}
if (val != null) {
// 如果是枚举类型, 则取 value 值.
if (EnumUtil.isEnum(val)) {
val = EnumConvertUtils.convertEnumToStr(val);
} else if (field.isAnnotationPresent(JSONStringParse.class)) {
val = JSONObject.toJSONString(val);
}
// 如果和原来的值一样, 则跳过
String originVal = systemConfig.getValue();
if (ObjUtil.equals(originVal, val)) {
continue;
}
// 将更新后的值存到更新列表中
SystemConfig updateSystemConfig = new SystemConfig();
updateSystemConfig.setId(systemConfig.getId());
updateSystemConfig.setName(systemConfig.getName());
updateSystemConfig.setValue(Convert.toStr(val));
updateSystemConfig.setTitle(systemConfig.getTitle());
updateSystemConfigList.add(updateSystemConfig);
}
} else {
log.warn("尝试保存系统配置表中不存在字段: {}", key);
}
}
updateSystemConfigList.forEach(systemConfigInForm -> {
SystemConfig systemConfigInDb = systemConfigMapInDb.get(systemConfigInForm.getName());
systemConfigModifyHandlerChain.execute(systemConfigInDb, systemConfigInForm);
systemConfigMapper.updateById(systemConfigInForm);
});
}
/**
* 获取 AES Hex 格式密钥
*
* @return AES Hex 格式密钥
*/
public synchronized String getAesHexKeyOrGenerate() {
SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
String aesHexKey = systemConfigDTO.getRsaHexKey();
if (StringUtils.isEmpty(aesHexKey)) {
byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.AES.getValue()).getEncoded();
aesHexKey = HexUtil.encodeHexStr(key);
SystemConfig loginVerifyModeConfig = systemConfigMapper.findByName(SystemConfigConstant.AES_HEX_KEY);
loginVerifyModeConfig.setValue(aesHexKey);
systemConfigMapper.updateById(loginVerifyModeConfig);
systemConfigDTO.setRsaHexKey(aesHexKey);
Cache cache = cacheManager.getCache(CACHE_NAME);
Optional.ofNullable(cache).ifPresent(cache1 -> cache1.put("dto", systemConfigDTO));
}
return aesHexKey;
}
/**
* 获取前端站点域名
*
* @return 前端站点域名
*/
public String getFrontDomain() {
SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
return systemConfigDTO.getFrontDomain();
}
/**
* 获取实际的前端站点域名
*
* @return 实际的前端站点域名
*/
public String getRealFrontDomain() {
SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
return StringUtils.firstNonNull(systemConfigDTO.getFrontDomain(), getAxiosFromDomainOrSetting(), RequestHolder.getOriginAddress());
}
/**
* 优先级:
* 1. 如果设置了强制后端地址,则使用强制后端地址。
* 2. 如果请求中有 axios-from 参数,则使用该参数。
* 3. 如果没有强制后端地址和 axios-from 参数,则使用请求的服务器地址(如果经过多个代理,可能不是实际的后端地址)。
*
* @return 后端站点地址
*/
public String getAxiosFromDomainOrSetting() {
SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
if (StringUtils.isNotBlank(systemConfigDTO.getForceBackendAddress())) {
return systemConfigDTO.getForceBackendAddress();
} else if (StringUtils.isNotEmpty(RequestHolder.getAxiosFrom())) {
return RequestHolder.getAxiosFrom();
} else {
return RequestHolder.getRequestServerAddress();
}
}
/**
* 获取前端地址下的 401 页面地址.
*
* @return 前端地址下的 401 页面地址.
*
*/
public String getUnauthorizedUrl() {
return getUnauthorizedUrl(null, null);
}
/**
* 获取前端地址下的 401 页面地址. 可以指定 code 和 message.
*
* @param code
* 指定错误码
*
* @param message
* 指定错误信息
*
* @return 前端地址下的 401 页面地址.
*/
public String getUnauthorizedUrl(String code, String message) {
String url = StringUtils.concat(getRealFrontDomain(), "/401");
UrlBuilder urlBuilder = UrlBuilder.of(url);
if (StringUtils.isNotBlank(code)) {
urlBuilder.addQuery("code", code);
}
if (StringUtils.isNotBlank(message)) {
urlBuilder.addQuery("message", message);
}
return urlBuilder.build();
}
/**
* 获取前端地址下的 403 页面地址.
*
* @return 前端地址下的 403 页面地址.
*
*/
public String getForbiddenUrl() {
return getForbiddenUrl(null, null);
}
/**
* 获取前端地址下的 403 页面地址. 可以指定 code 和 message.
*
* @param code
* 指定错误码
*
* @param message
* 指定错误信息
*
* @return 前端地址下的 403 页面地址.
*/
public String getForbiddenUrl(String code, String message) {
String url = StringUtils.concat(getRealFrontDomain(), "/403");
UrlBuilder urlBuilder = UrlBuilder.of(url);
if (StringUtils.isNotBlank(code)) {
urlBuilder.addQuery("code", code);
}
if (StringUtils.isNotBlank(message)) {
urlBuilder.addQuery("message", message);
}
return urlBuilder.build();
}
/**
* 获取前端地址下的 404 页面地址.
*
* @return 前端地址下的 404 页面地址.
*
*/
public String getNotFoundUrl() {
return getNotFoundUrl(null, null);
}
/**
* 获取前端地址下的 404 页面地址. 可以指定 code 和 message.
*
* @param code
* 指定错误码
*
* @param message
* 指定错误信息
*
* @return 前端地址下的 404 页面地址.
*/
public String getNotFoundUrl(String code, String message) {
String url = StringUtils.concat(getRealFrontDomain(), "/404");
UrlBuilder urlBuilder = UrlBuilder.of(url);
if (StringUtils.isNotBlank(code)) {
urlBuilder.addQuery("code", code);
}
if (StringUtils.isNotBlank(message)) {
urlBuilder.addQuery("message", message);
}
return urlBuilder.build();
}
/**
* 获取前端地址下的 500 页面地址. 可以指定 code 和 message.
*
* @param code
* 指定错误码
*
* @param message
* 指定错误信息
*
* @return 前端地址下的 500 页面地址.
*/
public String getErrorPageUrl(String code, String message) {
String url = StringUtils.concat(getRealFrontDomain(), "/500");
UrlBuilder urlBuilder = UrlBuilder.of(url);
if (StringUtils.isNotBlank(code)) {
urlBuilder.addQuery("code", code);
}
if (StringUtils.isNotBlank(message)) {
urlBuilder.addQuery("message", message);
}
return urlBuilder.build();
}
/**
* 重置登录验证模式,去除所有登录额外验证方式.
*/
public void resetLoginVerifyMode() {
SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
systemConfigDTO.setLoginImgVerify(false);
systemConfigDTO.setAdminTwoFactorVerify(false);
systemConfigDTO.setLoginVerifySecret("");
systemConfigDTO.setLoginVerifyMode(LoginVerifyModeEnum.OFF_MODE);
((SystemConfigService)AopContext.currentProxy()).updateSystemConfig(systemConfigDTO);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/filter/controller/StorageSourceFilterController.java
================================================
package im.zhaojun.zfile.module.filter.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.module.filter.model.entity.FilterConfig;
import im.zhaojun.zfile.module.filter.service.FilterConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 存储源过滤器维护接口
*
* @author zhaojun
*/
@Tag(name = "存储源模块-过滤文件")
@ApiSort(6)
@RestController
@RequestMapping("/admin")
public class StorageSourceFilterController {
@Resource
private FilterConfigService filterConfigService;
@ApiOperationSupport(order = 1)
@Operation(summary = "获取存储源过滤文件列表", description ="根据存储源 ID 获取存储源设置的过滤文件列表")
@Parameter(in = ParameterIn.PATH, name = "storageId", description = "存储源 id", required = true, schema = @Schema(type = "integer"))
@GetMapping("/storage/{storageId}/filters")
public AjaxJson> getFilters(@PathVariable Integer storageId) {
return AjaxJson.getSuccessData(filterConfigService.findByStorageId(storageId));
}
@ApiOperationSupport(order = 2)
@Operation(summary = "保存存储源过滤文件列表", description ="保存指定存储源 ID 设置的过滤文件列表")
@Parameter(in = ParameterIn.PATH, name = "storageId", description = "存储源 id", required = true, schema = @Schema(type = "integer"))
@PostMapping("/storage/{storageId}/filters")
@DemoDisable
public AjaxJson saveFilters(@PathVariable Integer storageId, @RequestBody List filter) {
filterConfigService.batchSave(storageId, filter);
return AjaxJson.getSuccess();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/filter/mapper/FilterConfigMapper.java
================================================
package im.zhaojun.zfile.module.filter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import im.zhaojun.zfile.module.filter.model.entity.FilterConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 过滤器配置表 Mapper 接口
*
* @author zhaojun
*/
@Mapper
public interface FilterConfigMapper extends BaseMapper {
/**
* 根据存储源 ID 获取存储源配置列表
*
* @param storageId
* 存储源 ID
*
* @return 存储源过滤器配置列表
*/
List findByStorageId(@Param("storageId") Integer storageId);
/**
* 根据存储源 ID 删除过滤器配置
*
* @param storageId
* 存储源 ID
*
* @return 删除条数
*/
int deleteByStorageId(@Param("storageId") Integer storageId);
/**
* 获取所有类型为禁止访问的过滤规则
*
* @param storageId
* 存储 ID
*
* @return 禁止访问的过滤规则列表
*/
List findByStorageIdAndInaccessible(@Param("storageId")Integer storageId);
/**
* 获取所有类型为禁止下载的过滤规则
*
* @param storageId
* 存储 ID
*
* @return 禁止下载的过滤规则列表
*/
List findByStorageIdAndDisableDownload(@Param("storageId")Integer storageId);
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/filter/model/entity/FilterConfig.java
================================================
package im.zhaojun.zfile.module.filter.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import im.zhaojun.zfile.module.filter.model.enums.FilterConfigHiddenModeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 存储源过滤配置 entity
*
* @author zhaojun
*/
@Data
@Schema(title="存储源过滤配置")
@TableName(value = "filter_config")
public class FilterConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@Schema(title = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "storage_id")
@Schema(title = "存储源 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer storageId;
@TableField(value = "expression")
@Schema(title = "过滤表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "/*.png")
private String expression;
@TableField(value = "description")
@Schema(title = "表达式描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "用来辅助记忆表达式")
private String description;
@TableField(value = "mode")
@Schema(title = "模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "隐藏模式,仅隐藏: hidden, 隐藏后不可访问: inaccessible, 隐藏后不可下载: disable_download")
private FilterConfigHiddenModeEnum mode;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/filter/model/enums/FilterConfigHiddenModeEnum.java
================================================
package im.zhaojun.zfile.module.filter.model.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 文件夹隐藏模式枚举
*
* @author zhaojun
*/
@Getter
@AllArgsConstructor
public enum FilterConfigHiddenModeEnum {
/**
* 仅隐藏
*/
HIDDEN("hidden"),
/**
* 隐藏并不可访问 (针对目录)
*/
INACCESSIBLE("inaccessible"),
/**
* 隐藏并不可访问 (针对文件)
*/
DISABLE_DOWNLOAD("disable_download");
@EnumValue
@JsonValue
private final String value;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/filter/service/FilterConfigService.java
================================================
package im.zhaojun.zfile.module.filter.service;
import im.zhaojun.zfile.core.util.CollectionUtils;
import im.zhaojun.zfile.core.util.FileUtils;
import im.zhaojun.zfile.core.util.PatternMatcherUtils;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.filter.mapper.FilterConfigMapper;
import im.zhaojun.zfile.module.filter.model.entity.FilterConfig;
import im.zhaojun.zfile.module.storage.event.StorageSourceCopyEvent;
import im.zhaojun.zfile.module.storage.event.StorageSourceDeleteEvent;
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
import im.zhaojun.zfile.module.user.service.UserStorageSourceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.BeanUtils;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 存储源过滤规则 Service
*
* @author zhaojun
*/
@Slf4j
@Service
@CacheConfig(cacheNames = "filterConfig")
public class FilterConfigService {
@Resource
private FilterConfigMapper filterConfigMapper;
@Resource
private UserStorageSourceService userStorageSourceService;
/**
* 根据存储源 ID 获取存储源配置列表
*
* @param storageId
* 存储源 ID
*
* @return 存储源过滤规则配置列表
*/
@Cacheable(key = "'filter-base-' + #storageId",
condition = "#storageId != null")
public List findByStorageId(Integer storageId) {
return filterConfigMapper.findByStorageId(storageId);
}
/**
* 获取所有类型为禁止访问的过滤规则
*
* @param storageId
* 存储 ID
*
* @return 禁止访问的过滤规则列表
*/
@Cacheable(key = "'filter-inaccessible-' + #storageId",
condition = "#storageId != null")
public List findByStorageIdAndInaccessible(Integer storageId) {
return filterConfigMapper.findByStorageIdAndInaccessible(storageId);
}
/**
* 获取所有类型为禁止下载的过滤规则
*
* @param storageId
* 存储 ID
*
* @return 禁止下载的过滤规则列表
*/
@Cacheable(key = "'filter-disable-download-' + #storageId",
condition = "#storageId != null")
public List findByStorageIdAndDisableDownload(Integer storageId) {
return filterConfigMapper.findByStorageIdAndDisableDownload(storageId);
}
/**
* 批量保存存储源过滤规则配置, 会先删除之前的所有配置(在事务中运行)
*
* @param storageId
* 存储源 ID
*
* @param filterConfigList
* 存储源过滤规则配置列表
*/
@Transactional(rollbackFor = Exception.class)
public void batchSave(Integer storageId, List filterConfigList) {
((FilterConfigService) AopContext.currentProxy()).deleteByStorageId(storageId);
log.info("更新存储源 ID 为 {} 的过滤规则 {} 条", storageId, filterConfigList.size());
for (FilterConfig filterConfig : filterConfigList) {
filterConfig.setId(null);
filterConfig.setStorageId(storageId);
filterConfigMapper.insert(filterConfig);
if (log.isDebugEnabled()) {
log.debug("新增过滤规则, 存储源 ID: {}, 表达式: {}, 描述: {}, 隐藏模式: {}",
filterConfig.getStorageId(), filterConfig.getExpression(),
filterConfig.getDescription(), filterConfig.getMode().getValue());
}
}
}
/**
* 根据存储源 ID 删除所有过滤规则配置
*
* @param storageId
* 存储源 ID
*/
@Caching(evict = {
@CacheEvict(key = "'filter-base-' + #storageId"),
@CacheEvict(key = "'filter-inaccessible-' + #storageId"),
@CacheEvict(key = "'filter-disable-download-' + #storageId")
})
public int deleteByStorageId(Integer storageId) {
int deleteSize = filterConfigMapper.deleteByStorageId(storageId);
log.info("删除存储源 ID 为 {} 的过滤规则 {} 条", storageId, deleteSize);
return deleteSize;
}
/**
* 监听存储源删除事件,根据存储源 id 删除相关的过滤条件设置
*
* @param storageSourceDeleteEvent
* 存储源删除事件
*/
@EventListener
public void onStorageSourceDelete(StorageSourceDeleteEvent storageSourceDeleteEvent) {
Integer storageId = storageSourceDeleteEvent.getId();
int updateRows = ((FilterConfigService) AopContext.currentProxy()).deleteByStorageId(storageId);
if (log.isDebugEnabled()) {
log.debug("删除存储源 [id {}, name: {}, type: {}] 时,关联删除存储源过滤条件设置 {} 条",
storageId,
storageSourceDeleteEvent.getName(),
storageSourceDeleteEvent.getType().getDescription(),
updateRows);
}
}
/**
* 判断访问的路径是否是不允许访问的
*
* @param storageId
* 存储源 ID
*
* @param path
* 请求路径
*
*/
public boolean checkFileIsInaccessible(Integer storageId, String path) {
List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageIdAndInaccessible(storageId);
return testPattern(storageId, filterConfigList, path);
}
/**
* 指定存储源下的文件名称, 根据过滤表达式判断是否会显示, 如果符合任意一条表达式, 表示隐藏则返回 true, 反之表示不隐藏则返回 false.
*
* @param storageId
* 存储源 ID
*
* @param fileName
* 文件名
*
* @return 是否是隐藏文件夹
*/
public boolean checkFileIsHidden(Integer storageId, String fileName) {
List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageId(storageId);
return testPattern(storageId, filterConfigList, fileName);
}
/**
* 指定存储源下的文件名称, 根据过滤表达式判断文件名和所在路径是否禁止下载, 如果符合任意一条表达式, 则返回 true, 反之则返回 false.
*
* @param storageId
* 存储源 ID
*
* @param fileName
* 文件名
*
* @return 是否显示
*/
public boolean checkFileIsDisableDownload(Integer storageId, String fileName) {
List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageIdAndDisableDownload(storageId);
String filePath = FileUtils.getParentPath(fileName);
if (StringUtils.isEmpty(filePath)) {
return testPattern(storageId, filterConfigList, fileName);
} else {
return testPattern(storageId, filterConfigList, fileName) || testPattern(storageId, filterConfigList, filePath);
}
}
/**
* 根据规则表达式和测试字符串进行匹配,如测试字符串和其中一个规则匹配上,则返回 true,反之返回 false。
*
* @param patternList
* 规则列表
*
* @param test
*
* 测试字符串
*
* @return 是否显示
*/
private boolean testPattern(Integer storageId, List patternList, String test) {
// 如果规则列表为空, 则表示不需要过滤, 直接返回 false
if (CollectionUtils.isEmpty(patternList)) {
if (log.isDebugEnabled()) {
log.debug("过滤规则列表为空, 存储源 ID: {}, 测试字符串: {}", storageId, test);
}
return false;
}
// 判断是否需要忽略文件隐藏校验
boolean isIgnoreHidden = userStorageSourceService.hasCurrentUserStorageOperatorPermission(storageId, FileOperatorTypeEnum.IGNORE_HIDDEN);
if (isIgnoreHidden) {
if (log.isDebugEnabled()) {
log.debug("权限配置忽略过滤规则, 存储源 ID: {}, 测试字符串: {}", storageId, test);
}
return false;
}
// 校验表达式
for (FilterConfig filterConfig : patternList) {
String expression = filterConfig.getExpression();
if (StringUtils.isEmpty(expression)) {
if (log.isDebugEnabled()) {
log.debug("存储源 {} 过滤文件测试表达式: {}, 测试字符串: {}, 表达式为空,跳过该规则校验", storageId, expression, test);
}
continue;
}
try {
boolean match = PatternMatcherUtils.testCompatibilityGlobPattern(expression, test);
if (log.isDebugEnabled()) {
log.debug("存储源 {} 过滤文件测试表达式: {}, 测试字符串: {}, 匹配结果: {}", storageId, expression, test, match);
}
if (match) {
return true;
}
} catch (Exception e) {
log.error("存储源 {} 过滤文件测试表达式: {}, 测试字符串: {}, 匹配异常,跳过该规则.", storageId, expression, test, e);
}
}
return false;
}
/**
* 监听存储源复制事件, 复制存储源的过滤条件设置到新的存储源
*
* @param storageSourceCopyEvent
* 存储源复制事件
*/
@EventListener
public void onStorageSourceCopy(StorageSourceCopyEvent storageSourceCopyEvent) {
Integer fromId = storageSourceCopyEvent.getFromId();
Integer newId = storageSourceCopyEvent.getNewId();
List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageId(fromId);
filterConfigList.forEach(filterConfig -> {
FilterConfig newFilterConfig = new FilterConfig();
BeanUtils.copyProperties(filterConfig, newFilterConfig);
newFilterConfig.setId(null);
newFilterConfig.setStorageId(newId);
filterConfigMapper.insert(newFilterConfig);
});
log.info("复制存储源 ID 为 {} 的存储源过滤条件设置到存储源 ID 为 {} 成功, 共 {} 条", fromId, newId, filterConfigList.size());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/install/controller/InstallController.java
================================================
package im.zhaojun.zfile.module.install.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.module.install.model.request.InstallSystemRequest;
import im.zhaojun.zfile.module.install.service.InstallService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
/**
* 系统初始化接口
*
* @author zhaojun
*/
@Tag(name = "初始化模块")
@RestController
@RequestMapping("/api")
public class InstallController {
@Resource
private InstallService installService;
@GetMapping("/install/status")
@ApiOperationSupport(order = 1)
@Operation(summary = "获取系统初始化状态", description = "根据管理员用户名是否存在判断系统已初始化, 已初始化返回 true, 未初始化返回 false")
public AjaxJson isInstall() {
return AjaxJson.getSuccessData(installService.getSystemIsInstalled());
}
@ApiOperationSupport(order = 2)
@Operation(summary = "初始化系统", description = "根据管理员用户名是否存在判断系统已初始化, 已初始化返回 true, 未初始化返回 false")
@PostMapping("/install")
@DemoDisable
public AjaxJson install(@RequestBody InstallSystemRequest installSystemRequest) {
installService.install(installSystemRequest);
return AjaxJson.getSuccess();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/install/model/request/InstallSystemRequest.java
================================================
package im.zhaojun.zfile.module.install.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 系统初始化请求参数
*
* @author zhaojun
*/
@Data
@Schema(description = "系统初始化请求类")
public class InstallSystemRequest {
@Schema(title = "站点名称", example = "ZFile Site Name")
private String siteName;
@Schema(title = "用户名", example = "admin")
private String username;
@Schema(title = "密码", example = "123456")
private String password;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/install/service/InstallService.java
================================================
package im.zhaojun.zfile.module.install.service;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.exception.core.SystemException;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.install.model.request.InstallSystemRequest;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.user.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class InstallService {
@Resource
private SystemConfigService systemConfigService;
@Resource
private UserService userService;
@Transactional(rollbackFor = Exception.class)
public void install(InstallSystemRequest installSystemRequest) {
if (getSystemIsInstalled()) {
throw new BizException(ErrorCode.BIZ_SYSTEM_ALREADY_INIT);
}
boolean updateFlag = userService.initAdminUser(installSystemRequest.getUsername(),
installSystemRequest.getPassword());
if (!updateFlag) {
throw new SystemException(ErrorCode.BIZ_SYSTEM_INIT_ERROR);
}
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
systemConfigDTO.setSiteName(installSystemRequest.getSiteName());
systemConfigDTO.setInstalled(true);
systemConfigService.updateSystemConfig(systemConfigDTO);
}
/**
* 获取系统是否已初始化
*
* @return 管理员名称
*/
public Boolean getSystemIsInstalled() {
return systemConfigService.getSystemConfig().getInstalled();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/aspect/LinkRateLimiterAspect.java
================================================
package im.zhaojun.zfile.module.link.aspect;
import cn.hutool.extra.servlet.JakartaServletUtil;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.link.cache.LinkRateLimiterCache;
import im.zhaojun.zfile.module.storage.annotation.LinkRateLimiter;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 校验直链访问频率.
*
* 校验所有标注了 {@link LinkRateLimiter} 的注解
*
* @author zhaojun
*/
@Aspect
@Component
@Slf4j
public class LinkRateLimiterAspect {
@Resource
private HttpServletRequest httpServletRequest;
@Resource
private SystemConfigService systemConfigService;
@Resource
private LinkRateLimiterCache linkRateLimiterCache;
/**
* 校验直链访问频率.
*
* @param point
* 连接点
*
* @return 方法运行结果
*/
@Around(value = "@annotation(im.zhaojun.zfile.module.storage.annotation.LinkRateLimiter)")
public Object around(ProceedingJoinPoint point) throws Throwable {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
Integer linkLimitSecond = systemConfig.getLinkLimitSecond();
Integer linkDownloadLimit = systemConfig.getLinkDownloadLimit();
// 如果未设置直链限制, 则不进行校验
if (linkLimitSecond == null || linkDownloadLimit == null || linkLimitSecond == 0 || linkDownloadLimit == 0) {
return point.proceed();
}
String clientIP = JakartaServletUtil.getClientIP(httpServletRequest);
if (linkRateLimiterCache.containsKey(clientIP)) {
AtomicInteger atomicInteger = linkRateLimiterCache.get(clientIP, false);
if (atomicInteger.incrementAndGet() > linkDownloadLimit) {
throw new BizException(ErrorCode.BIZ_ACCESS_TOO_FREQUENT);
}
} else {
linkRateLimiterCache.put(clientIP, new AtomicInteger(1), linkLimitSecond * 1000);
}
return point.proceed();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/aspect/RefererCheckAspect.java
================================================
package im.zhaojun.zfile.module.link.aspect;
import im.zhaojun.zfile.core.util.CollectionUtils;
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 im.zhaojun.zfile.module.link.model.enums.RefererTypeEnum;
import im.zhaojun.zfile.module.storage.annotation.RefererCheck;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* 校验 referer 防盗链.
*
* 校验所有标注了 {@link RefererCheck} 的注解
*
* @author zhaojun
*/
@Aspect
@Component
@Slf4j
public class RefererCheckAspect {
@Resource
private HttpServletRequest httpServletRequest;
@Resource
private HttpServletResponse httpServletResponse;
@Resource
private SystemConfigService systemConfigService;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 校验 referer 防盗链.
*
* @param point
* 连接点
*
* @return 方法运行结果
*/
@Around(value = "@annotation(im.zhaojun.zfile.module.storage.annotation.RefererCheck)")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 获取配置的 referer 类型
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
RefererTypeEnum refererType = systemConfig.getRefererType();
// 如果未开启 referer 防盗链则跳过.
if (refererType == RefererTypeEnum.OFF) {
return point.proceed();
}
// 获取当前请求 referer
String referer = httpServletRequest.getHeader(HttpHeaders.REFERER);
String requestUrl = httpServletRequest.getRequestURI();
// 获取 Forbidden 页面地址
String forbiddenUrl = systemConfigService.getForbiddenUrl();
// 如果 referer 不允许为空,且当前 referer 为空,则校验
Boolean refererAllowEmpty = systemConfig.getRefererAllowEmpty();
if (!refererAllowEmpty && StringUtils.isEmpty(referer)) {
log.warn("请求路径 {}, referer 不允许为空,当前请求 referer 为空,禁止访问.", requestUrl);
httpServletResponse.sendRedirect(forbiddenUrl);
return null;
} else if (refererAllowEmpty && StringUtils.isEmpty(referer)) { // 如果 referer 允许为空,且当前 referer 为空,则跳过校验
return point.proceed();
}
// 获取允许的 referer 地址
String refererValue = systemConfig.getRefererValue();
List refererValueList = StringUtils.split(refererValue, StringUtils.LF);
// 如果是白名单模式,则校验当前 referer, 如果未在允许的列表中,则禁止访问.
if (refererType == RefererTypeEnum.WHITE_LIST && containsPathMatcher(refererValueList, referer) == null) {
log.warn("请求路径 {}, referer 为白名单模式,当前请求 referer {} 未在白名单中,禁止访问.", requestUrl, referer);
httpServletResponse.sendRedirect(forbiddenUrl);
return null;
}
// 如果是黑名单模式,则校验当前 referer 是否在列表中,则禁止访问.
if (refererType == RefererTypeEnum.BLACK_LIST && containsPathMatcher(refererValueList, referer) != null) {
log.warn("请求路径 {}, referer 为黑名单模式,当前请求 referer {} 在黑名单中,禁止访问.", requestUrl, referer);
httpServletResponse.sendRedirect(forbiddenUrl);
return null;
}
return point.proceed();
}
/**
* 校验 value 是否在 Ant 表达式列表中.
*
* @param patternList
* Ant 表达式列表
*
* @param value
* 要校验的值
*
* @return 返回匹配到的规则项,如果没有匹配到,则返回 null.
*/
public String containsPathMatcher(Collection patternList, String value) {
if (CollectionUtils.isEmpty(patternList)) {
return null;
}
for (String pattern : patternList) {
if (pathMatcher.match(pattern, value)) {
return pattern;
}
}
return null;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/cache/LinkRateLimiterCache.java
================================================
package im.zhaojun.zfile.module.link.cache;
import cn.hutool.cache.impl.CacheObj;
import cn.hutool.cache.impl.TimedCache;
import im.zhaojun.zfile.module.link.model.dto.CacheInfo;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 直链/短链访问频率限制缓存
*/
@Service
public class LinkRateLimiterCache {
/**
* cache 在 put 时不指定 timeout, 则使用默认的 timeout. (单位: 毫秒)
*/
public static final Integer DEFAULT_TIME_OUT = 60_000;
private final TimedCache timedCache = new TimedCache<>(DEFAULT_TIME_OUT);
public boolean containsKey(String key) {
return timedCache.containsKey(key);
}
public AtomicInteger get(String key, boolean isUpdateLastAccess) {
return timedCache.get(key, isUpdateLastAccess);
}
public void put(String key, AtomicInteger object, long timeout) {
timedCache.put(key, object, timeout);
}
public List> getCacheInfo() {
List> cacheInfoList = new ArrayList<>();
Iterator> cacheObjIterator = timedCache.cacheObjIterator();
while (cacheObjIterator.hasNext()) {
CacheObj next = cacheObjIterator.next();
CacheInfo cacheInfo = new CacheInfo<>();
cacheInfo.setKey(next.getKey());
cacheInfo.setValue(next.getValue());
cacheInfo.setTtl(next.getTtl());
cacheInfo.setExpiredTime(next.getExpiredTime());
cacheInfoList.add(cacheInfo);
}
return cacheInfoList;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/controller/DirectLinkController.java
================================================
package im.zhaojun.zfile.module.link.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.SpringMvcUtils;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.link.model.request.BatchGenerateLinkRequest;
import im.zhaojun.zfile.module.link.model.result.BatchGenerateLinkResponse;
import im.zhaojun.zfile.module.link.service.DynamicDirectLinkPrefixService;
import im.zhaojun.zfile.module.link.service.LinkDownloadService;
import im.zhaojun.zfile.module.storage.annotation.StoragePermissionCheck;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
import im.zhaojun.zfile.module.storage.service.base.AbstractBaseFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 短链接口
*
* @author zhaojun
*/
@Tag(name = "短链")
@ApiSort(5)
@Controller
@Slf4j
public class DirectLinkController {
@Resource
private LinkDownloadService linkDownloadService;
@Resource
private SystemConfigService systemConfigService;
@Resource
private DynamicDirectLinkPrefixService dynamicDirectLinkPrefixService;
public static final String DIRECT_LINK_SUFFIX_PATH = "/{storageKey}/**";
@EventListener(ApplicationReadyEvent.class)
public void init() throws NoSuchMethodException {
String directLinkPrefix = systemConfigService.getSystemConfig().getDirectLinkPrefix();
Method directLinkMethod = DirectLinkController.class.getMethod("directLink", String.class);
RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(directLinkPrefix + DIRECT_LINK_SUFFIX_PATH).build();
dynamicDirectLinkPrefixService.registerMappingHandlerMapping(SystemConfig.DIRECT_LINK_PREFIX_NAME, requestMappingInfo, this, directLinkMethod);
}
/**
* 路径直链处理方法,会根据 URL 中的存储源 key 和文件路径, 获取到文件,判断文件是否有短链,没有则生成,然后跳转到短链.
*
* @param storageKey
* 存储源 key
*/
public ResponseEntity> directLink(@PathVariable("storageKey") String storageKey) throws IOException {
// 获取直链全路径
String filePath = SpringMvcUtils.getExtractPathWithinPattern();
// 如果路径不是以 / 开头, 则补充上
if (StringUtils.isNotEmpty(filePath) && filePath.charAt(0) != StringUtils.SLASH_CHAR) {
filePath = StringUtils.SLASH + filePath;
}
return linkDownloadService.handlerDirectLink(storageKey, filePath);
}
@PostMapping("/api/path-link/batch/generate")
@ResponseBody
@ApiOperationSupport(order = 1)
@Operation(summary = "生成路径直链", description ="对指定存储源的某文件路径生成路径直链")
@StoragePermissionCheck(action = FileOperatorTypeEnum.LINK)
public AjaxJson> generatorShortLink(@RequestBody @Valid BatchGenerateLinkRequest batchGenerateLinkRequest) {
List result = new ArrayList<>();
// 获取站点域名
String serverAddress = systemConfigService.getAxiosFromDomainOrSetting();
String directLinkPrefix = systemConfigService.getSystemConfig().getDirectLinkPrefix();
String storageKey = batchGenerateLinkRequest.getStorageKey();
AbstractBaseFileService> baseFileService = StorageSourceContext.getByStorageKey(storageKey);
if (baseFileService == null) {
throw new BizException(ErrorCode.BIZ_STORAGE_NOT_FOUND);
}
String currentUserBasePath = baseFileService.getCurrentUserBasePath();
for (String path : batchGenerateLinkRequest.getPaths()) {
// 拼接全路径地址.
String fullPath = StringUtils.concat(serverAddress, directLinkPrefix, storageKey, currentUserBasePath, path);
result.add(new BatchGenerateLinkResponse(fullPath));
}
return AjaxJson.getSuccessData(result);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkController.java
================================================
package im.zhaojun.zfile.module.link.controller;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import im.zhaojun.zfile.module.link.model.request.BatchGenerateLinkRequest;
import im.zhaojun.zfile.module.link.model.result.BatchGenerateLinkResponse;
import im.zhaojun.zfile.module.link.service.LinkDownloadService;
import im.zhaojun.zfile.module.link.service.ShortLinkService;
import im.zhaojun.zfile.module.storage.annotation.StoragePermissionCheck;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 短链接口
*
* @author zhaojun
*/
@Tag(name = "直短链模块")
@ApiSort(5)
@Controller
@Slf4j
public class ShortLinkController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ShortLinkService shortLinkService;
@Resource
private StorageSourceService storageSourceService;
@Resource
private LinkDownloadService linkDownloadService;
@PostMapping("/api/short-link/batch/generate")
@ResponseBody
@ApiOperationSupport(order = 1)
@Operation(summary = "生成短链", description ="对指定存储源的某文件路径生成短链")
@StoragePermissionCheck(action = FileOperatorTypeEnum.SHORT_LINK)
public AjaxJson> generatorShortLink(@RequestBody @Valid BatchGenerateLinkRequest batchGenerateLinkRequest) {
List result = new ArrayList<>();
// 获取站点域名
String domain = systemConfigService.getAxiosFromDomainOrSetting();
Long expireTime = batchGenerateLinkRequest.getExpireTime();
String storageKey = batchGenerateLinkRequest.getStorageKey();
Integer storageId = storageSourceService.findIdByKey(storageKey);
for (String path : batchGenerateLinkRequest.getPaths()) {
// 拼接全路径地址.
String currentUserBasePath = StorageSourceContext.getByStorageId(storageId).getCurrentUserBasePath();
String fullPath = StringUtils.concat(currentUserBasePath, path);
ShortLink shortLink = shortLinkService.generatorShortLink(storageId, fullPath, expireTime);
String shortUrl = StringUtils.removeDuplicateSlashes(domain + "/s/" + shortLink.getShortKey());
result.add(new BatchGenerateLinkResponse(shortUrl));
}
return AjaxJson.getSuccessData(result);
}
@GetMapping("/s/{key}")
@ApiOperationSupport(order = 2)
@Operation(summary = "跳转短链", description ="根据短链 key 跳转(302 重定向)到对应的直链.")
@Parameter(in = ParameterIn.PATH, name = "key", description = "短链 key", required = true, schema = @Schema(type = "string"))
public ResponseEntity> parseShortKey(@PathVariable String key) throws IOException {
return linkDownloadService.handlerShortLink(key);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkManagerController.java
================================================
package im.zhaojun.zfile.module.link.controller;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.link.cache.LinkRateLimiterCache;
import im.zhaojun.zfile.module.link.convert.ShortLinkConvert;
import im.zhaojun.zfile.module.link.model.dto.CacheInfo;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import im.zhaojun.zfile.module.link.model.request.BatchDeleteRequest;
import im.zhaojun.zfile.module.link.model.request.QueryShortLinkLogRequest;
import im.zhaojun.zfile.module.link.model.request.ShortLinkResult;
import im.zhaojun.zfile.module.link.service.ShortLinkService;
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 直链管理接口
*
* @author zhaojun
*/
@Tag(name = "直链管理")
@ApiSort(7)
@Controller
@RequestMapping("/admin")
public class ShortLinkManagerController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ShortLinkService shortLinkService;
@Resource
private StorageSourceService storageSourceService;
@Resource
private ShortLinkConvert shortLinkConvert;
@Resource
private LinkRateLimiterCache linkRateLimiterCache;
@ApiOperationSupport(order = 1)
@GetMapping("/link/list")
@Operation(summary = "搜索短链")
@ResponseBody
public AjaxJson> list(QueryShortLinkLogRequest queryObj) {
Page resultPage = getShortLinkResultPage(queryObj);
String serverAddress = systemConfigService.getAxiosFromDomainOrSetting();
resultPage.getRecords().forEach(shortLinkResult -> {
shortLinkResult.setShortLink(StringUtils.concat(serverAddress, "s", shortLinkResult.getShortKey()));
});
return AjaxJson.getPageData(resultPage.getTotal(), resultPage.getRecords());
}
@ApiOperationSupport(order = 2)
@DeleteMapping("/link/delete/{id}")
@Operation(summary = "删除短链")
@Parameter(in = ParameterIn.PATH, name = "id", description = "短链 id", required = true, schema = @Schema(type = "integer"))
@ResponseBody
@DemoDisable
public AjaxJson deleteById(@PathVariable Integer id) {
shortLinkService.removeById(id);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 3)
@PostMapping("/link/delete/batch")
@ResponseBody
@Operation(summary = "批量删除短链")
@DemoDisable
public AjaxJson batchDelete(@RequestBody BatchDeleteRequest batchDeleteRequest) {
shortLinkService.removeBatchByIds(batchDeleteRequest.getIds());
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 4)
@GetMapping("/link/export")
@ResponseBody
@Operation(summary = "导出短链")
public void exportExcel(QueryShortLinkLogRequest queryObj, HttpServletResponse response) throws IOException {
Page shortLinkResultPage = getShortLinkResultPage(queryObj);
ExcelWriter writer = ExcelUtil.getWriter(true);
writer.addHeaderAlias("id", "ID");
writer.addHeaderAlias("storageName", "存储源名称");
writer.addHeaderAlias("storageTypeStr", "存储源类型");
writer.addHeaderAlias("shortKey", "短链 key");
writer.addHeaderAlias("url", "文件路径");
writer.addHeaderAlias("createDate", "创建时间");
writer.addHeaderAlias("expireDate", "过期时间");
writer.setOnlyAlias(true);
writer.write(shortLinkResultPage.getRecords(), true);
writer.setColumnWidth(0, 8);
writer.setColumnWidth(1, 30);
writer.setColumnWidth(2, 15);
writer.setColumnWidth(3, 15);
writer.setColumnWidth(4, 50);
writer.setColumnWidth(5, 15);
writer.setColumnWidth(6, 15);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
ServletOutputStream out=response.getOutputStream();
writer.flush(out, true);
writer.close();
IoUtil.close(out);
}
@ApiOperationSupport(order = 5)
@GetMapping("/link/limit/info")
@ResponseBody
@Operation(summary = "获取直链访问限制信息")
public AjaxJson>> getLinkLimitInfo() {
return AjaxJson.getSuccessData(linkRateLimiterCache.getCacheInfo());
}
@NotNull
private Page getShortLinkResultPage(QueryShortLinkLogRequest queryObj) {
// 分页和排序
boolean asc = Objects.equals(queryObj.getOrderDirection(), "asc");
OrderItem orderItem = asc ? OrderItem.asc(queryObj.getOrderBy()) : OrderItem.desc(queryObj.getOrderBy());
Page pages = new Page(queryObj.getPage(), queryObj.getLimit())
.addOrder(orderItem);
// 搜索条件
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper()
.eq(StringUtils.isNotEmpty(queryObj.getStorageId()), ShortLink::getStorageId, queryObj.getStorageId())
.like(StringUtils.isNotEmpty(queryObj.getKey()), ShortLink::getShortKey, queryObj.getKey())
.like(StringUtils.isNotEmpty(queryObj.getUrl()), ShortLink::getUrl, queryObj.getUrl())
.ge(ObjUtil.isNotEmpty(queryObj.getDateFrom()), ShortLink::getCreateDate, queryObj.getDateFrom())
.le(ObjUtil.isNotEmpty(queryObj.getDateTo()), ShortLink::getCreateDate, queryObj.getDateTo());
// 执行查询
Page selectResult = shortLinkService.selectPage(pages, queryWrapper);
// 转换为结果集
Map cache = new HashMap<>();
Stream shortLinkResultList = selectResult.getRecords().stream().map(shortLink -> {
Integer shortLinkStorageId = shortLink.getStorageId();
StorageSource storageSource = cache.getOrDefault(shortLinkStorageId, storageSourceService.findById(shortLinkStorageId));
cache.put(shortLinkStorageId, storageSource);
return shortLinkConvert.entityToResultList(shortLink, storageSource);
});
Page resultPage = new Page<>();
resultPage.setTotal(selectResult.getTotal());
resultPage.setRecords(shortLinkResultList.collect(Collectors.toList()));
return resultPage;
}
@ApiOperationSupport(order = 6)
@DeleteMapping("/link/deleteExpireLink")
@Operation(summary = "删除过期短链")
@ResponseBody
@DemoDisable
public AjaxJson deleteExpireLink() {
int updateRows = shortLinkService.deleteExpireLink();
return AjaxJson.getSuccessData(updateRows);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/convert/ShortLinkConvert.java
================================================
package im.zhaojun.zfile.module.link.convert;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
import im.zhaojun.zfile.module.link.model.request.ShortLinkResult;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import org.springframework.stereotype.Component;
/**
* 直链实体类器
*
* @author zhaojun
*/
@Component
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ShortLinkConvert {
@Mapping(source = "shortLink.id", target = "id")
@Mapping(source = "storageSource.name", target = "storageName")
@Mapping(source = "storageSource.type", target = "storageType")
ShortLinkResult entityToResultList(ShortLink shortLink, StorageSource storageSource);
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/dto/DynamicRegisterMappingHandlerDTO.java
================================================
package im.zhaojun.zfile.module.link.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import java.lang.reflect.Method;
@Data
@AllArgsConstructor
public class DynamicRegisterMappingHandlerDTO {
private RequestMappingInfo requestMappingInfo;
private Object object;
private Method method;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/event/DeleteExpireLinkEvent.java
================================================
package im.zhaojun.zfile.module.link.event;
import lombok.Data;
@Data
public class DeleteExpireLinkEvent {
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/mapper/ShortLinkMapper.java
================================================
package im.zhaojun.zfile.module.link.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import jakarta.annotation.Nullable;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
/**
* 短链接配置表 Mapper 接口
*
* @author zhaojun
*/
@Mapper
public interface ShortLinkMapper extends BaseMapper {
/**
* 根据短链接 key 查询短链接
*
* @param key
* 短链接 key
*
* @return 短链接信息
*/
ShortLink findByKey(@Param("key")String key);
/**
* 根据存储源 ID 删除所有数据
*
* @param storageId
* 存储源 ID
*/
int deleteByStorageId(@Param("storageId") Integer storageId);
/**
* 根据存储源 ID 和 URL 查询短链接
*/
ShortLink findByStorageIdAndUrl(@Param("storageId") Integer storageId,
@Param("url") String url,
@Nullable @Param("expireDate") Date expireDate);
/**
* 删除过期的短链接
*
* @return 删除的行数
*/
int deleteExpireLink();
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/dto/CacheInfo.java
================================================
package im.zhaojun.zfile.module.link.model.dto;
import lombok.Data;
import java.util.Date;
@Data
public class CacheInfo {
private K key;
private V value;
private Date expiredTime;
private Long ttl;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/entity/ShortLink.java
================================================
package im.zhaojun.zfile.module.link.model.entity;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 短链信息 entity
*
* @author zhaojun
*/
@Data
@Schema(description = "短链信息")
@TableName(value = "short_link")
public class ShortLink implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 永久直链失效时间为 -1
*/
public static final Long PERMANENT_EXPIRE_TIME = -1L;
/**
* 永久直链失效日期为 9999-12-31
*/
public static final Date PERMANENT_EXPIRE_DATE = DateUtil.parseDate("9999-12-31");
@TableId(value = "id", type = IdType.AUTO)
@Schema(title = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "storage_id")
@Schema(title = "存储源 ID", example = "1")
private Integer storageId;
@TableField(value = "short_key")
@Schema(title = "短链 key", example = "voldd3")
private String shortKey;
@TableField(value = "url")
@Schema(title = "短链 url", example = "/directlink/1/test02.png")
private String url;
@TableField(value = "create_date")
@Schema(title = "创建时间", example = "2021-11-22 10:05")
private Date createDate;
@TableField(value = "expire_date")
@Schema(title = "过期时间", example = "2021-11-23 10:05")
private Date expireDate;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/enums/RefererTypeEnum.java
================================================
package im.zhaojun.zfile.module.link.model.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* Referer 防盗链类型枚举
*
* @author zhaojun
*/
@Getter
@AllArgsConstructor
public enum RefererTypeEnum {
/**
* 不启用 Referer 防盗链
*/
OFF("off"),
/**
* 启用白名单模式
*/
WHITE_LIST("white_list"),
/**
* 启用黑名单模式
*/
BLACK_LIST("black_list");
@EnumValue
@JsonValue
private final String value;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/BatchDeleteRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import lombok.Data;
import java.util.List;
/**
* @author zhaojun
*/
@Data
public class BatchDeleteRequest {
private List ids;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/BatchGenerateLinkRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
* 批量生成直链请求类
* @author zhaojun
*/
@Data
@Schema(description = "批量生成直链请求类")
public class BatchGenerateLinkRequest {
@NotBlank(message = "存储源 key 不能为空")
private String storageKey;
@NotEmpty(message = "生成的文件路径不能为空")
private List paths;
/**
* 有效期, 单位: 秒
*/
@NotNull(message = "过期时间不能为空")
private Long expireTime;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/QueryDownloadLogRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import cn.hutool.core.date.DateUtil;
import im.zhaojun.zfile.core.model.request.PageQueryRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import java.util.List;
/**
* 查询下载日志请求参数
*
* @author zhaojun
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class QueryDownloadLogRequest extends PageQueryRequest {
@Schema(title="文件路径")
private String path;
@Schema(title="存储源 key")
private String storageKey;
@Schema(title="链接类型")
private String linkType;
@Schema(title="短链 key")
private String shortKey;
@Schema(title="访问时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private List searchDate;
@Schema(title="访问 ip")
private String ip;
@Schema(title="访问 user_agent")
private String userAgent;
@Schema(title="访问 referer")
private String referer;
@Schema(title="排序字段")
private String orderBy = "create_time";
public Date getDateFrom() {
if (searchDate == null || searchDate.isEmpty()) {
return null;
}
return DateUtil.beginOfDay(searchDate.getFirst());
}
public Date getDateTo() {
if (searchDate == null || searchDate.isEmpty()) {
return null;
}
return DateUtil.endOfDay(searchDate.getLast());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/QueryLoginLogRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import cn.hutool.core.date.DateUtil;
import im.zhaojun.zfile.core.model.request.PageQueryRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import java.util.List;
/**
* 查询下载日志请求参数
*
* @author zhaojun
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class QueryLoginLogRequest extends PageQueryRequest {
@Schema(title="用户名")
private String username;
@Schema(title="密码")
private String password;
@Schema(title="IP")
private String ip;
@Schema(title="User-Agent")
private String userAgent;
@Schema(title="来源")
private String referer;
@Schema(title="登录结果")
private String result;
@Schema(title="访问时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private List searchDate;
@Schema(title="排序字段")
private String orderBy = "create_time";
public Date getDateFrom() {
if (searchDate == null) {
return null;
}
return DateUtil.beginOfDay(searchDate.getFirst());
}
public Date getDateTo() {
if (searchDate == null) {
return null;
}
return DateUtil.endOfDay(searchDate.getLast());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/QueryShortLinkLogRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import cn.hutool.core.date.DateUtil;
import im.zhaojun.zfile.core.model.request.PageQueryRequest;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
import java.util.List;
/**
* @author zhaojun
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class QueryShortLinkLogRequest extends PageQueryRequest {
@Schema(title="短链 key")
private String key;
@Schema(title="存储源 id")
private String storageId;
@Schema(title="短链文件路径")
private String url;
@Schema(title="访问时间")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private List searchDate;
public Date getDateFrom() {
if (searchDate == null) {
return null;
}
return DateUtil.beginOfDay(searchDate.getFirst());
}
public Date getDateTo() {
if (searchDate == null) {
return null;
}
return DateUtil.endOfDay(searchDate.getLast());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkResult.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import com.fasterxml.jackson.annotation.JsonIgnore;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* 短链结果类
*
* @author zhaojun
*/
@Data
public class ShortLinkResult {
@Schema(title = "短链 id", example = "1")
private Integer id;
@Schema(title = "存储源名称", example = "我的本地存储")
private String storageName;
@Schema(title = "存储源类型")
private StorageTypeEnum storageType;
@JsonIgnore
private String storageTypeStr;
public String getStorageTypeStr() {
return storageType.getDescription();
}
@Schema(title = "短链 key", example = "voldd3")
private String shortKey;
@Schema(title = "文件 url", example = "/directlink/1/test02.png")
private String url;
@Schema(title = "创建时间", example = "2021-11-22 10:05")
private Date createDate;
@Schema(title = "过期时间", example = "2021-11-23 10:05")
private Date expireDate;
@Schema(title="短链地址")
private String shortLink;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkSearchRequest.java
================================================
package im.zhaojun.zfile.module.link.model.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/**
* 短链接搜索请求参数
*
* @author zhaojun
*/
@Data
@Schema(description = "搜索存储源中文件请求类")
public class ShortLinkSearchRequest {
@Schema(title = "存储源 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotBlank(message = "存储源 id 不能为空")
private Integer storageId;
@Schema(title = "存储源 key", example = "local")
private String key;
@Schema(title = "文件 url/路径", example = "/a")
private String url;
@Schema(title = "开始时间", example = "2022-01-01 00:00:00")
private String dateFrom;
@Schema(title = "结束时间", example = "2022-12-31 23:59:59")
private String dateTo;
@Schema(title = "页码", example = "1")
private Integer page;
@Schema(title = "每页数量", example = "10")
private Integer limit;
@Schema(title = "排序字段", example = "id")
private String orderBy;
@Schema(title = "排序方式", example = "desc")
private String orderDirection;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/model/result/BatchGenerateLinkResponse.java
================================================
package im.zhaojun.zfile.module.link.model.result;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author zhaojun
*/
@Data
@Schema(description = "批量生成直链结果类")
@AllArgsConstructor
public class BatchGenerateLinkResponse {
private String address;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/service/DynamicDirectLinkPrefixService.java
================================================
package im.zhaojun.zfile.module.link.service;
import im.zhaojun.zfile.module.link.dto.DynamicRegisterMappingHandlerDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 动态请求映射服务,用于在线注册、修改、注销 @RequestMapping 注解的方法.
*
* @author zhaojun
*/
@Slf4j
@Service
public class DynamicDirectLinkPrefixService {
@Resource
private RequestMappingHandlerMapping requestMappingHandlerMapping;
public static final Map REGISTER_MAPPING = new ConcurrentHashMap<>();
public void registerMappingHandlerMapping(String key, RequestMappingInfo requestMappingInfo, Object controllerObj, Method directLinkMethod) {
requestMappingHandlerMapping.registerMapping(requestMappingInfo, controllerObj, directLinkMethod);
REGISTER_MAPPING.put(key, new DynamicRegisterMappingHandlerDTO(requestMappingInfo, controllerObj, directLinkMethod));
}
public void updateRegisterMappingHandler(String key, RequestMappingInfo requestMappingInfo) {
synchronized (key.intern()) {
DynamicRegisterMappingHandlerDTO dynamicRegisterMappingHandlerDTO = REGISTER_MAPPING.get(key);
if (dynamicRegisterMappingHandlerDTO != null) {
requestMappingHandlerMapping.unregisterMapping(dynamicRegisterMappingHandlerDTO.getRequestMappingInfo());
requestMappingHandlerMapping.registerMapping(requestMappingInfo, dynamicRegisterMappingHandlerDTO.getObject(), dynamicRegisterMappingHandlerDTO.getMethod());
REGISTER_MAPPING.put(key, new DynamicRegisterMappingHandlerDTO(requestMappingInfo, dynamicRegisterMappingHandlerDTO.getObject(), dynamicRegisterMappingHandlerDTO.getMethod()));
}
}
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/service/LinkDownloadService.java
================================================
package im.zhaojun.zfile.module.link.service;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.biz.InvalidStorageSourceBizException;
import im.zhaojun.zfile.core.exception.core.ErrorPageBizException;
import im.zhaojun.zfile.core.exception.status.ForbiddenAccessException;
import im.zhaojun.zfile.core.exception.status.NotFoundAccessException;
import im.zhaojun.zfile.core.util.FileUtils;
import im.zhaojun.zfile.core.util.HttpUtil;
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 im.zhaojun.zfile.module.filter.service.FilterConfigService;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import im.zhaojun.zfile.module.log.model.entity.DownloadLog;
import im.zhaojun.zfile.module.log.service.DownloadLogService;
import im.zhaojun.zfile.module.storage.annotation.LinkRateLimiter;
import im.zhaojun.zfile.module.storage.annotation.RefererCheck;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import im.zhaojun.zfile.module.storage.service.base.AbstractBaseFileService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;
/**
* @author zhaojun
*/
@Slf4j
@Service
public class LinkDownloadService {
@Resource
private StorageSourceService storageSourceService;
@Resource
private DownloadLogService downloadLogService;
@Resource
private SystemConfigService systemConfigService;
@Resource
private ShortLinkService shortLinkService;
@Resource
private FilterConfigService filterConfigService;
private final Set expireKeySet = new HashSet<>();
@RefererCheck
@LinkRateLimiter
public ResponseEntity> handlerDirectLink(String storageKey, String filePath) {
// 检查系统是否允许直链
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
if (BooleanUtils.isNotTrue(systemConfigDTO.getShowPathLink())) {
throw new ForbiddenAccessException(ErrorCode.BIZ_DIRECT_LINK_NOT_ALLOWED);
}
return handlerDownloadGetUrl(storageKey, filePath, null, DownloadLog.DOWNLOAD_TYPE_DIRECT_LINK);
}
@RefererCheck
@LinkRateLimiter
public ResponseEntity> handlerShortLink(String shortKey) throws IOException {
// 从缓存中判断是否短链是否过期
if (expireKeySet.contains(shortKey)) {
throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_EXPIRED);
}
// 判断是否允许生成短链.
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
if (BooleanUtils.isNotTrue(systemConfigDTO.getShowShortLink())) {
throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_NOT_ALLOWED);
}
// 判断短链是否存在
ShortLink shortLink = shortLinkService.findByKey(shortKey);
if (shortLink == null) {
throw new NotFoundAccessException(ErrorCode.BIZ_SHORT_LINK_NOT_FOUNT);
}
// 判断短链是否过期
if (shortLink.getExpireDate() != null) {
DateTime now = DateUtil.date();
boolean isExpire = now.isAfter(shortLink.getExpireDate());
if (isExpire) {
expireKeySet.add(shortKey);
throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_EXPIRED);
}
}
// 获取实际文件路径,下载并记录日志
Integer storageId = shortLink.getStorageId();
String storageKey = storageSourceService.findStorageKeyById(storageId);
String filePath = shortLink.getUrl();
return handlerDownloadGetUrl(storageKey, filePath, shortKey, DownloadLog.DOWNLOAD_TYPE_SHORT_LINK);
}
/**
* 处理指定存储源的下载请求
*
* @param storageKey
* 存储源 key
*
* @param filePath
* 文件路径
*
* @param shortKey
* 短链接 key
*
* @param downloadType
* 下载类型, 直链下载(directLink)或短链下载(shortLink)
*/
private ResponseEntity> handlerDownloadGetUrl(String storageKey, String filePath, String shortKey, String downloadType) {
String fileAlias = StringUtils.equals(downloadType, DownloadLog.DOWNLOAD_TYPE_DIRECT_LINK) ? filePath : shortKey;
// 获取存储源 Service
AbstractBaseFileService> fileService;
try {
fileService = StorageSourceContext.getByStorageKey(storageKey);
} catch (InvalidStorageSourceBizException e) {
throw new ErrorPageBizException("无效的或初始化失败的存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.", e);
}
if (fileService == null) {
throw new ErrorPageBizException("未找到存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.");
}
StorageSource storageSource = storageSourceService.findByStorageKey(storageKey);
Boolean enable = storageSource.getEnable();
if (!enable) {
throw new ErrorPageBizException("未启用的存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.");
}
// 检查是否访问了禁止下载的目录
if (filterConfigService.checkFileIsDisableDownload(storageSource.getId(), filePath)) {
// 获取 Forbidden 页面地址
return ResponseEntity.status(302)
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private")
.header(HttpHeaders.PRAGMA, "no-cache")
.header(HttpHeaders.EXPIRES, "0")
.header(HttpHeaders.LOCATION, systemConfigService.getForbiddenUrl())
.build();
}
// 获取文件下载链接
String downloadUrl;
try {
downloadUrl = fileService.getDownloadUrl(filePath);
} catch (NotFoundAccessException e) {
throw e;
} catch (Exception e) {
throw new ErrorPageBizException("获取存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.", e);
}
// 判断下载链接是否为空
if (StringUtils.isEmpty(downloadUrl)) {
throw new ErrorPageBizException("获取存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接为空, 无法下载.");
}
// 记录下载日志.
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
Boolean recordDownloadLog = systemConfig.getRecordDownloadLog();
if (BooleanUtils.isTrue(recordDownloadLog)) {
DownloadLog downloadLog = new DownloadLog(downloadType, filePath, storageKey, shortKey);
downloadLogService.save(downloadLog);
}
// 判断下载链接是否为 m3u8 格式, 如果是则返回 m3u8 内容.
if (StringUtils.equalsIgnoreCase(FileUtil.extName(filePath), "m3u8")) {
String textContent = HttpUtil.getTextContent(downloadUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/vnd.apple.mpegurl;charset=utf-8"));
ContentDisposition contentDisposition = ContentDisposition
.builder("attachment")
.filename(FileUtils.getName(filePath), StandardCharsets.UTF_8)
.build();
headers.setContentDisposition(contentDisposition);
return ResponseEntity.ok()
.headers(headers)
.body(textContent);
}
return ResponseEntity.status(302)
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private")
.header(HttpHeaders.PRAGMA, "no-cache")
.header(HttpHeaders.EXPIRES, "0")
.header(HttpHeaders.LOCATION, downloadUrl)
.build();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/link/service/ShortLinkService.java
================================================
package im.zhaojun.zfile.module.link.service;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import im.zhaojun.zfile.core.exception.ErrorCode;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.module.config.model.dto.LinkExpireDTO;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.link.event.DeleteExpireLinkEvent;
import im.zhaojun.zfile.module.link.mapper.ShortLinkMapper;
import im.zhaojun.zfile.module.link.model.entity.ShortLink;
import im.zhaojun.zfile.module.storage.event.StorageSourceDeleteEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
* 短链 Service
*
* @author zhaojun
*/
@Service
@Slf4j
@CacheConfig(cacheNames = "shortLink")
public class ShortLinkService {
@Resource
private ShortLinkMapper shortLinkMapper;
@Resource
private SystemConfigService systemConfigService;
@Resource
private ApplicationEventPublisher applicationEventPublisher;
/**
* 根据短链接 key 查询短链接
*
* @param key
* 短链接 key
*
* @return 短链接信息
*/
@Cacheable(key = "#key", unless = "#result == null", condition = "#key != null")
public ShortLink findByKey(String key) {
return shortLinkMapper.findByKey(key);
}
/**
* 根据存储源 ID 和 URL 查询短链接
*
* @param storageId
* 存储源 ID
*
* @param url
* 文件路径
*
* @return 短链接信息
*/
public @Nullable ShortLink findByStorageIdAndUrl(Integer storageId, String url, @Nullable Date expireDate) {
return shortLinkMapper.findByStorageIdAndUrl(storageId, url, expireDate);
}
/**
* 为存储源指定路径生成短链接, 保证生成的短连接 key 是不同的 (如果是永久链接, 则不会重新生成)
*
* @param storageId
* 存储源 id
*
* @param fullPath
* 存储源路径
*
* @return 生成后的短链接信息
*/
public ShortLink generatorShortLink(Integer storageId, String fullPath, Long expireTime) {
boolean validate = checkExpireDateIsValidate(expireTime);
if (!validate) {
throw new BizException(ErrorCode.BIZ_EXPIRE_TIME_ILLEGAL);
}
// 永久链接不在重复生成
if (Objects.equals(expireTime, ShortLink.PERMANENT_EXPIRE_TIME)) {
ShortLink shortLink = findByStorageIdAndUrl(storageId, fullPath, ShortLink.PERMANENT_EXPIRE_DATE);
if (shortLink != null) {
return shortLink;
}
}
ShortLink shortLink;
String randomKey;
int generateCount = 0;
do {
// 获取短链
randomKey = RandomUtil.randomString(6);
shortLink = ((ShortLinkService) AopContext.currentProxy()).findByKey(randomKey);
generateCount++;
} while (shortLink != null);
shortLink = new ShortLink();
shortLink.setStorageId(storageId);
shortLink.setUrl(fullPath);
shortLink.setCreateDate(new Date());
shortLink.setShortKey(randomKey);
if (expireTime == -1) {
shortLink.setExpireDate(ShortLink.PERMANENT_EXPIRE_DATE);
} else {
shortLink.setExpireDate(new Date(System.currentTimeMillis() + expireTime * 1000L));
}
if (log.isDebugEnabled()) {
log.debug("生成直/短链: 存储源 ID: {}, 文件路径: {}, 短链 key {}, 随机生成直链冲突次数: {}",
shortLink.getStorageId(), shortLink.getUrl(), shortLink.getShortKey(), generateCount);
}
shortLinkMapper.insert(shortLink);
return shortLink;
}
@CacheEvict(allEntries = true)
public int deleteExpireLink() {
applicationEventPublisher.publishEvent(new DeleteExpireLinkEvent());
int deleteSize = shortLinkMapper.deleteExpireLink();
log.info("删除过期直/短链 {} 条", deleteSize);
return deleteSize;
}
@CacheEvict(allEntries = true)
public void removeById(Integer id) {
log.info("删除 id 为 {} 的直/短链", id);
shortLinkMapper.deleteById(id);
}
@Transactional(rollbackFor = Exception.class)
@CacheEvict(allEntries = true)
public void removeBatchByIds(List ids) {
log.info("批量删除直/短链,id 集合为 {}", ids);
shortLinkMapper.deleteBatchIds(ids);
}
@CacheEvict(allEntries = true)
public int deleteByStorageId(Integer storageId) {
int deleteSize = shortLinkMapper.deleteByStorageId(storageId);
log.info("删除存储源 ID 为 {} 的短链 {} 条", storageId, deleteSize);
return deleteSize;
}
/**
* 监听存储源删除事件,根据存储源 id 删除相关的短链
*
* @param storageSourceDeleteEvent
* 存储源删除事件
*/
@EventListener
public void onStorageSourceDelete(StorageSourceDeleteEvent storageSourceDeleteEvent) {
Integer storageId = storageSourceDeleteEvent.getId();
int updateRows = ((ShortLinkService) AopContext.currentProxy()).deleteByStorageId(storageId);
if (log.isDebugEnabled()) {
log.debug("删除存储源 [id {}, name: {}, type: {}] 时,关联删除存储源短链 {} 条",
storageId,
storageSourceDeleteEvent.getName(),
storageSourceDeleteEvent.getType().getDescription(),
updateRows);
}
}
public Page selectPage(Page pages, Wrapper queryWrapper) {
return shortLinkMapper.selectPage(pages, queryWrapper);
}
private boolean checkExpireDateIsValidate(Long expires) {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
List linkExpireTimeList = systemConfig.getLinkExpireTimes();
for (LinkExpireDTO linkExpireDTO : linkExpireTimeList) {
if (linkExpireDTO.getSeconds().equals(expires)) {
return true;
}
}
return false;
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/controller/DownloadLogManagerController.java
================================================
package im.zhaojun.zfile.module.log.controller;
import cn.hutool.core.util.ObjUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.annotation.DemoDisable;
import im.zhaojun.zfile.core.util.AjaxJson;
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 im.zhaojun.zfile.module.link.model.request.BatchDeleteRequest;
import im.zhaojun.zfile.module.link.model.request.QueryDownloadLogRequest;
import im.zhaojun.zfile.module.log.convert.DownloadLogConvert;
import im.zhaojun.zfile.module.log.model.entity.DownloadLog;
import im.zhaojun.zfile.module.log.model.result.DownloadLogResult;
import im.zhaojun.zfile.module.log.service.DownloadLogService;
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
/**
* 直链下载日志接口
*
* @author zhaojun
*/
@Tag(name = "直链日志管理")
@ApiSort(7)
@Controller
@RequestMapping("/admin/download/log")
public class DownloadLogManagerController {
@Resource
private StorageSourceService storageSourceService;
@Resource
private DownloadLogConvert downloadLogConvert;
@Resource
private DownloadLogService downloadLogService;
@Resource
private SystemConfigService systemConfigService;
@ApiOperationSupport(order = 1)
@GetMapping("/list")
@Operation(summary = "直链下载日志")
@ResponseBody
public AjaxJson> list(QueryDownloadLogRequest queryDownloadLogRequest) {
// 分页和排序
boolean asc = Objects.equals(queryDownloadLogRequest.getOrderDirection(), "asc");
OrderItem orderItem = asc ? OrderItem.asc(queryDownloadLogRequest.getOrderBy()) : OrderItem.desc(queryDownloadLogRequest.getOrderBy());
Page pages = new Page(queryDownloadLogRequest.getPage(), queryDownloadLogRequest.getLimit())
.addOrder(orderItem);
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper()
.eq(StringUtils.isNotEmpty(queryDownloadLogRequest.getStorageKey()), DownloadLog::getStorageKey, queryDownloadLogRequest.getStorageKey())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getPath()), DownloadLog::getPath, queryDownloadLogRequest.getPath())
.isNotNull("shortLink".equals(queryDownloadLogRequest.getLinkType()), DownloadLog::getShortKey)
.isNull("directLink".equals(queryDownloadLogRequest.getLinkType()), DownloadLog::getShortKey)
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getShortKey()), DownloadLog::getShortKey, queryDownloadLogRequest.getShortKey())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getIp()), DownloadLog::getIp, queryDownloadLogRequest.getIp())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getReferer()), DownloadLog::getReferer, queryDownloadLogRequest.getReferer())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getUserAgent()), DownloadLog::getUserAgent, queryDownloadLogRequest.getUserAgent())
.ge(ObjUtil.isNotEmpty(queryDownloadLogRequest.getDateFrom()), DownloadLog::getCreateTime, queryDownloadLogRequest.getDateFrom())
.le(ObjUtil.isNotEmpty(queryDownloadLogRequest.getDateTo()), DownloadLog::getCreateTime, queryDownloadLogRequest.getDateTo());
Page selectResult = downloadLogService.selectPage(pages, queryWrapper);
Map cache = new HashMap<>();
String serverAddress = systemConfigService.getAxiosFromDomainOrSetting();
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String directLinkPrefix = systemConfig.getDirectLinkPrefix();
Stream shortLinkResultList = selectResult.getRecords().stream().map(model -> {
String storageKey = model.getStorageKey();
StorageSource storageSource = cache.computeIfAbsent(storageKey, (key) -> storageSourceService.findByStorageKey(key));
DownloadLogResult downloadLogResult = downloadLogConvert.entityToResultList(model, storageSource);
if (StringUtils.isNotBlank(downloadLogResult.getShortKey())) {
downloadLogResult.setShortLink(StringUtils.concat(serverAddress, "s", downloadLogResult.getShortKey()));
} else {
downloadLogResult.setPathLink(StringUtils.concat(serverAddress, directLinkPrefix, downloadLogResult.getStorageKey(), downloadLogResult.getPath()));
}
return downloadLogResult;
});
return AjaxJson.getPageData(selectResult.getTotal(), shortLinkResultList);
}
@ApiOperationSupport(order = 2)
@DeleteMapping("/delete/{id}")
@Operation(summary = "删除直链")
@Parameter(in = ParameterIn.PATH, name = "id", description = "直链 id", required = true, schema = @Schema(type = "integer"))
@ResponseBody
@DemoDisable
public AjaxJson deleteById(@PathVariable Integer id) {
downloadLogService.removeById(id);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 3)
@PostMapping("/delete/batch")
@ResponseBody
@Operation(summary = "批量删除直链")
@DemoDisable
public AjaxJson batchDelete(@RequestBody BatchDeleteRequest batchDeleteRequest) {
List ids = batchDeleteRequest.getIds();
downloadLogService.removeBatchByIds(ids);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 4)
@PostMapping("/delete/batch/query")
@ResponseBody
@Operation(summary = "根据查询条件批量删除直链")
@DemoDisable
public AjaxJson batchDeleteBySearchParams(@RequestBody QueryDownloadLogRequest queryDownloadLogRequest) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper()
.eq(StringUtils.isNotEmpty(queryDownloadLogRequest.getStorageKey()), DownloadLog::getStorageKey, queryDownloadLogRequest.getStorageKey())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getPath()), DownloadLog::getPath, queryDownloadLogRequest.getPath())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getShortKey()), DownloadLog::getShortKey, queryDownloadLogRequest.getShortKey())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getIp()), DownloadLog::getIp, queryDownloadLogRequest.getIp())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getReferer()), DownloadLog::getReferer, queryDownloadLogRequest.getReferer())
.like(StringUtils.isNotEmpty(queryDownloadLogRequest.getUserAgent()), DownloadLog::getUserAgent, queryDownloadLogRequest.getUserAgent())
.ge(ObjUtil.isNotEmpty(queryDownloadLogRequest.getDateFrom()), DownloadLog::getCreateTime, queryDownloadLogRequest.getDateFrom())
.le(ObjUtil.isNotEmpty(queryDownloadLogRequest.getDateTo()), DownloadLog::getCreateTime, queryDownloadLogRequest.getDateTo());
downloadLogService.deleteByQueryWrapper(queryWrapper);
return AjaxJson.getSuccess();
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/controller/LoginLogController.java
================================================
package im.zhaojun.zfile.module.log.controller;
import cn.hutool.core.util.ObjUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.core.util.StringUtils;
import im.zhaojun.zfile.module.link.model.request.QueryLoginLogRequest;
import im.zhaojun.zfile.module.log.model.entity.LoginLog;
import im.zhaojun.zfile.module.log.service.LoginLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
import java.util.Objects;
/**
* 用户登录日志接口
*
* @author zhaojun
*/
@Tag(name = "登录日志管理")
@ApiSort(7)
@Controller
@RequestMapping("/admin/login/log")
public class LoginLogController {
@Resource
private LoginLogService loginLogService;
@ApiOperationSupport(order = 1)
@GetMapping("/list")
@Operation(summary = "登录日志列表")
@ResponseBody
public AjaxJson> list(QueryLoginLogRequest queryLoginLogRequest) {
// 分页和排序
boolean asc = Objects.equals(queryLoginLogRequest.getOrderDirection(), "asc");
OrderItem orderItem = asc ? OrderItem.asc(queryLoginLogRequest.getOrderBy()) : OrderItem.desc(queryLoginLogRequest.getOrderBy());
Page pages = new Page(queryLoginLogRequest.getPage(), queryLoginLogRequest.getLimit())
.addOrder(orderItem);
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper()
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getUsername()), LoginLog::getUsername, queryLoginLogRequest.getUsername())
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getPassword()), LoginLog::getPassword, queryLoginLogRequest.getPassword())
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getIp()), LoginLog::getIp, queryLoginLogRequest.getIp())
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getUserAgent()), LoginLog::getUserAgent, queryLoginLogRequest.getUserAgent())
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getReferer()), LoginLog::getReferer, queryLoginLogRequest.getReferer())
.like(StringUtils.isNotEmpty(queryLoginLogRequest.getResult()), LoginLog::getResult, queryLoginLogRequest.getResult())
.ge(ObjUtil.isNotEmpty(queryLoginLogRequest.getDateFrom()), LoginLog::getCreateTime, queryLoginLogRequest.getDateFrom())
.le(ObjUtil.isNotEmpty(queryLoginLogRequest.getDateTo()), LoginLog::getCreateTime, queryLoginLogRequest.getDateTo());
Page selectResult = loginLogService.selectPage(pages, queryWrapper);
return AjaxJson.getPageData(selectResult.getTotal(), selectResult.getRecords());
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/convert/DownloadLogConvert.java
================================================
package im.zhaojun.zfile.module.log.convert;
import im.zhaojun.zfile.module.log.model.entity.DownloadLog;
import im.zhaojun.zfile.module.storage.model.entity.StorageSource;
import im.zhaojun.zfile.module.log.model.result.DownloadLogResult;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.springframework.stereotype.Component;
/**
* 下载日志实体类转换器
*
* @author zhaojun
*/
@Component
@Mapper(componentModel = "spring")
public interface DownloadLogConvert {
@Mapping(source = "downloadLog.id", target = "id")
@Mapping(source = "storageSource.name", target = "storageName")
@Mapping(source = "storageSource.type", target = "storageType")
DownloadLogResult entityToResultList(DownloadLog downloadLog, StorageSource storageSource);
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/mapper/DownloadLogMapper.java
================================================
package im.zhaojun.zfile.module.log.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import im.zhaojun.zfile.module.log.model.entity.DownloadLog;
import org.apache.ibatis.annotations.Mapper;
/**
* 下载日志 Mapper 接口
*
* @author zhaojun
*/
@Mapper
public interface DownloadLogMapper extends BaseMapper {
/**
* 根据存储源 KEY 删除所有数据
*
* @param storageKey
* 存储源 KEY
*/
int deleteByStorageKey(String storageKey);
/**
* 删除过期的短链下载日志
*
* @return 删除的行数
*/
int deleteExpireShortLinkLog();
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/mapper/LoginLogMapper.java
================================================
package im.zhaojun.zfile.module.log.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import im.zhaojun.zfile.module.log.model.entity.LoginLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface LoginLogMapper extends BaseMapper {
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/model/entity/DownloadLog.java
================================================
package im.zhaojun.zfile.module.log.model.entity;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import im.zhaojun.zfile.core.util.RequestHolder;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpHeaders;
import java.io.Serializable;
import java.util.Date;
/**
* 文件下载日志 entity
*
* @author zhaojun
*/
@Data
@Tag(name ="文件下载日志")
@TableName(value = "`download_log`")
@NoArgsConstructor
public class DownloadLog implements Serializable {
public static final String DOWNLOAD_TYPE_DIRECT_LINK = "directLink";
public static final String DOWNLOAD_TYPE_SHORT_LINK = "shortLink";
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
@Schema(title = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "`download_type`")
@Schema(title="下载类型", example = "directLink", allowableValues = "directLink, shortLink")
private String downloadType;
@TableField(value = "`path`")
@Schema(title="文件路径")
private String path;
@TableField(value = "`storage_key`")
@Schema(title="存储源 key")
private String storageKey;
@TableField(value = "`create_time`")
@Schema(title="访问时间")
private Date createTime;
@TableField(value = "`ip`")
@Schema(title="访问 ip")
private String ip;
@TableField(value = "short_key")
@Schema(title = "短链 key", example = "voldd3")
private String shortKey;
@TableField(value = "`user_agent`")
@Schema(title="访问 user_agent")
private String userAgent;
@TableField(value = "`referer`")
@Schema(title="访问 referer")
private String referer;
public DownloadLog(String downloadType, String path, String storageKey, String shortKey) {
this.downloadType = downloadType;
this.path = path;
this.storageKey = storageKey;
this.shortKey = shortKey;
this.createTime = new Date();
HttpServletRequest request = RequestHolder.getRequest();
this.ip = JakartaServletUtil.getClientIP(request);
this.referer = request.getHeader(HttpHeaders.REFERER);
this.userAgent = request.getHeader(HttpHeaders.USER_AGENT);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/model/entity/LoginLog.java
================================================
package im.zhaojun.zfile.module.log.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName(value = "login_log")
public class LoginLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Integer id;
@TableField(value = "username")
private String username;
@TableField(value = "`password`")
private String password;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@TableField(value = "ip")
private String ip;
@TableField(value = "user_agent")
private String userAgent;
@TableField(value = "referer")
private String referer;
@TableField(value = "`result`")
private String result;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/model/result/DownloadLogResult.java
================================================
package im.zhaojun.zfile.module.log.model.result;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
/**
* 下载日志结果类
*
* @author zhaojun
*/
@Data
public class DownloadLogResult {
@Schema(title="")
private Integer id;
@Schema(title="文件路径")
private String path;
@Schema(title = "存储源类型")
private StorageTypeEnum storageType;
@Schema(title = "存储源名称", example = "我的本地存储")
private String storageName;
@Schema(title = "存储源Key", example = "local")
private String storageKey;
@Schema(title="访问时间")
private Date createTime;
@Schema(title="访问 ip")
private String ip;
@Schema(title = "短链 Key")
private String shortKey;
@Schema(title="访问 user_agent")
private String userAgent;
@Schema(title="访问 referer")
private String referer;
@Schema(title="短链地址")
private String shortLink;
@Schema(title="直链地址")
private String pathLink;
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/service/DownloadLogService.java
================================================
package im.zhaojun.zfile.module.log.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import im.zhaojun.zfile.module.link.event.DeleteExpireLinkEvent;
import im.zhaojun.zfile.module.log.mapper.DownloadLogMapper;
import im.zhaojun.zfile.module.log.model.entity.DownloadLog;
import im.zhaojun.zfile.module.storage.event.StorageSourceDeleteEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 下载日志 Service
*
* @author zhaojun
*/
@Slf4j
@Service
public class DownloadLogService {
@Resource
private DownloadLogMapper downloadLogMapper;
public void save(DownloadLog downloadLog) {
downloadLogMapper.insert(downloadLog);
}
public Page selectPage(Page pages, Wrapper queryWrapper) {
return downloadLogMapper.selectPage(pages, queryWrapper);
}
public void removeById(Integer id) {
downloadLogMapper.deleteById(id);
}
@Transactional(rollbackFor = Exception.class)
public void removeBatchByIds(List ids) {
downloadLogMapper.deleteBatchIds(ids);
}
public void deleteByQueryWrapper(Wrapper queryWrapper) {
downloadLogMapper.delete(queryWrapper);
}
public int deleteByStorageKey(String storageKey) {
int deleteSize = downloadLogMapper.deleteByStorageKey(storageKey);
log.info("删除存储源 ID 为 {} 的直/短链下载日志 {} 条", storageKey, deleteSize);
return deleteSize;
}
/**
* 监听存储源删除事件,根据存储源 id 删除相关的下载日志
*
* @param storageSourceDeleteEvent
* 存储源删除事件
*/
@EventListener
public void onStorageSourceDelete(StorageSourceDeleteEvent storageSourceDeleteEvent) {
String storageKey = storageSourceDeleteEvent.getKey();
int updateRows = ((DownloadLogService) AopContext.currentProxy()).deleteByStorageKey(storageKey);
if (log.isDebugEnabled()) {
log.debug("删除存储源 [id {}, key: {}, name: {}, type: {}] 时,关联删除存储源直/短链下载日志 {} 条",
storageSourceDeleteEvent.getId(),
storageKey,
storageSourceDeleteEvent.getName(),
storageSourceDeleteEvent.getType().getDescription(),
updateRows);
}
}
/**
* 删除过期下载日志
*
* @return 删除的条数
*/
public int deleteExpireShortLinkLog() {
return downloadLogMapper.deleteExpireShortLinkLog();
}
@EventListener(classes = DeleteExpireLinkEvent.class)
public void deleteExpireShortLinkLog(DeleteExpireLinkEvent event) {
int updateRows = deleteExpireShortLinkLog();
log.info("删除过期短链关联删除日志 {} 条", updateRows);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/log/service/LoginLogService.java
================================================
package im.zhaojun.zfile.module.log.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import im.zhaojun.zfile.module.log.mapper.LoginLogMapper;
import im.zhaojun.zfile.module.log.model.entity.LoginLog;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
@Service
public class LoginLogService {
@Resource
private LoginLogMapper loginLogMapper;
public void save(LoginLog loginLog) {
loginLogMapper.insert(loginLog);
}
public Page selectPage(Page pages, Wrapper queryWrapper) {
return loginLogMapper.selectPage(pages, queryWrapper);
}
}
================================================
FILE: src/main/java/im/zhaojun/zfile/module/onlyoffice/controller/OnlyOfficeController.java
================================================
package im.zhaojun.zfile.module.onlyoffice.controller;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTUtil;
import com.alibaba.fastjson2.JSONObject;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import im.zhaojun.zfile.module.onlyoffice.model.OnlyOfficeCallback;
import im.zhaojun.zfile.core.exception.biz.InvalidStorageSourceBizException;
import im.zhaojun.zfile.core.exception.core.BizException;
import im.zhaojun.zfile.core.util.*;
import im.zhaojun.zfile.module.onlyoffice.model.OnlyOfficeFile;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.storage.annotation.CheckPassword;
import im.zhaojun.zfile.module.storage.context.StorageSourceContext;
import im.zhaojun.zfile.module.storage.model.enums.FileOperatorTypeEnum;
import im.zhaojun.zfile.module.storage.model.request.base.FileItemRequest;
import im.zhaojun.zfile.module.storage.model.result.FileItemResult;
import im.zhaojun.zfile.module.storage.service.StorageSourceService;
import im.zhaojun.zfile.module.storage.service.base.AbstractBaseFileService;
import im.zhaojun.zfile.module.storage.service.base.AbstractProxyTransferService;
import im.zhaojun.zfile.module.user.model.entity.User;
import im.zhaojun.zfile.module.user.service.UserStorageSourceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.beans.Beans;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@Tag(name = "OnlyOffice 相关接口")
@RestController
@RequestMapping("/onlyOffice")
public class OnlyOfficeController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private StorageSourceService storageSourceService;
@Resource
private UserStorageSourceService userStorageSourceService;
private static final String CALLBACK_ERROR_MSG = "{\"error\":1}";
private static final String CALLBACK_SUCCESS_MSG = "{\"error\":0}";
public static final List SUPPORTED_STATUS = List.of(2, 3, 6, 7);
@ApiOperationSupport(order = 3)
@Operation(summary = "OnlyOffice 预览文件", description = "根据传入的文件信息, 生成 OnlyOffice 预览所需的 JSON 数据.")
@PostMapping("/config/token")
@CheckPassword(storageKeyFieldExpression = "[0].storageKey",
pathFieldExpression = "[0].path",
pathIsDirectory = false,
passwordFieldExpression = "[0].password")
public AjaxJson getPreviewFileJSONInfo(@Valid @RequestBody FileItemRequest fileItemRequest) {
// 根据存储策略获取文件信息(下载地址), 会校验权限.
Pair pair = getFileInfo(fileItemRequest);
FileItemResult fileInfo = pair.getKey();
Boolean hasUploadPermission = pair.getRight();
// 为 OnlyOffice 获取或生成文件 Key.
OnlyOfficeFile onlyOfficeFile = new OnlyOfficeFile(fileItemRequest.getStorageKey(), fileItemRequest.getPath());
String key = OnlyOfficeKeyCacheUtils.getKeyOrPutNew(onlyOfficeFile, 3000);
JSONObject onlyOfficePayload = createOnlyOfficePayload(fileInfo, key, hasUploadPermission);
return AjaxJson.getSuccessData(onlyOfficePayload);
}
private Pair getFileInfo(FileItemRequest fileItemRequest) {
String storageKey = fileItemRequest.getStorageKey();
Integer storageId = storageSourceService.findIdByKey(storageKey);
if (storageId == null) {
throw new InvalidStorageSourceBizException(storageKey);
}
// 处理请求参数默认值
fileItemRequest.handleDefaultValue();
// 获取文件信息
AbstractBaseFileService> fileService = StorageSourceContext.getByStorageId(storageId);
try {
FileItemResult fileItem = fileService.getFileItem(fileItemRequest.getPath());
if (fileItem == null) {
throw new BizException("文件不存在");
}
String currentUserBasePath = fileService.getCurrentUserBasePath();
fileItemRequest.setPath(StringUtils.concat(currentUserBasePath, fileItemRequest.getPath()));
boolean hasUploadPermission = userStorageSourceService.hasCurrentUserStorageOperatorPermission(storageId, FileOperatorTypeEnum.UPLOAD);
return Pair.of(fileItem, hasUploadPermission);
} catch (Exception e) {
throw new BizException("获取文件信息失败: " + e.getMessage());
}
}
/**
* 生成 OnlyOffice 预览所需的 JSON 数据. 配置参考: