newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
- newHeadersToRedact.addAll(headersToRedact);
- newHeadersToRedact.add(name);
- headersToRedact = newHeadersToRedact;
- }
-
- private volatile Level level = Level.NONE;
-
- /** Change the level at which this interceptor logs. */
- public HttpLoggingInterceptor setLevel(Level level) {
- if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
- this.level = level;
- return this;
- }
-
- public Level getLevel() {
- return level;
- }
-
- @Override public Response intercept(Chain chain) throws IOException {
- Level level = this.level;
-
- Request request = chain.request();
- if (level == Level.NONE) {
- return chain.proceed(request);
- }
-
- boolean logBody = level == Level.BODY;
- boolean logHeaders = logBody || level == Level.HEADERS;
-
- RequestBody requestBody = request.body();
- boolean hasRequestBody = requestBody != null;
-
- Connection connection = chain.connection();
- String requestStartMessage = "--> "
- + request.method()
- + ' ' + request.url()
- + (connection != null ? " " + connection.protocol() : "");
- if (!logHeaders && hasRequestBody) {
- requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
- }
- logger.log(requestStartMessage);
-
- if (logHeaders) {
- if (hasRequestBody) {
- // Request body headers are only present when installed as a network interceptor. Force
- // them to be included (when available) so there values are known.
- if (requestBody.contentType() != null) {
- logger.log("Content-Type: " + requestBody.contentType());
- }
- if (requestBody.contentLength() != -1) {
- logger.log("Content-Length: " + requestBody.contentLength());
- }
- }
-
- Headers headers = request.headers();
- for (int i = 0, count = headers.size(); i < count; i++) {
- String name = headers.name(i);
- // Skip headers from the request body as they are explicitly logged above.
- if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
- logHeader(headers, i);
- }
- }
-
- if (!logBody || !hasRequestBody) {
- logger.log("--> END " + request.method());
- } else if (bodyHasUnknownEncoding(request.headers())) {
- logger.log("--> END " + request.method() + " (encoded body omitted)");
- } else if (requestBody.isDuplex()) {
- logger.log("--> END " + request.method() + " (duplex request body omitted)");
- } else {
- Buffer buffer = new Buffer();
- requestBody.writeTo(buffer);
-
- Charset charset = UTF8;
- MediaType contentType = requestBody.contentType();
- if (contentType != null) {
- charset = contentType.charset(UTF8);
- }
-
- logger.log("");
- if (isPlaintext(buffer)) {
- logger.log(buffer.readString(charset));
- logger.log("--> END " + request.method()
- + " (" + requestBody.contentLength() + "-byte body)");
- } else {
- logger.log("--> END " + request.method() + " (binary "
- + requestBody.contentLength() + "-byte body omitted)");
- }
- }
- }
-
- long startNs = System.nanoTime();
- Response response;
- try {
- response = chain.proceed(request);
- } catch (Exception e) {
- logger.log("<-- HTTP FAILED: " + e);
- throw e;
- }
- long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
-
- ResponseBody responseBody = response.body();
- long contentLength = responseBody.contentLength();
- String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length";
- logger.log("<-- "
- + response.code()
- + (response.message().isEmpty() ? "" : ' ' + response.message())
- + ' ' + response.request().url()
- + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')');
-
- if (logHeaders) {
- Headers headers = response.headers();
- for (int i = 0, count = headers.size(); i < count; i++) {
- logHeader(headers, i);
- }
-
- if (!logBody || !HttpHeaders.hasBody(response)) {
- logger.log("<-- END HTTP");
- } else if (bodyHasUnknownEncoding(response.headers())) {
- logger.log("<-- END HTTP (encoded body omitted)");
- } else {
- BufferedSource source = responseBody.source();
- source.request(Long.MAX_VALUE); // Buffer the entire body.
- Buffer buffer = source.getBuffer();
-
- Long gzippedLength = null;
- if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) {
- gzippedLength = buffer.size();
- try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) {
- buffer = new Buffer();
- buffer.writeAll(gzippedResponseBody);
- }
- }
-
- Charset charset = UTF8;
- MediaType contentType = responseBody.contentType();
- if (contentType != null) {
- charset = contentType.charset(UTF8);
- }
-
- if (!isPlaintext(buffer)) {
- logger.log("");
- logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
- return response;
- }
-
- if (contentLength != 0) {
- logger.log("");
- logger.log(buffer.clone().readString(charset));
- }
-
- if (gzippedLength != null) {
- logger.log("<-- END HTTP (" + buffer.size() + "-byte, "
- + gzippedLength + "-gzipped-byte body)");
- } else {
- logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
- }
- }
- }
-
- return response;
- }
-
- private void logHeader(Headers headers, int i) {
- String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i);
- logger.log(headers.name(i) + ": " + value);
- }
-
- /**
- * Returns true if the body in question probably contains human readable text. Uses a small sample
- * of code points to detect unicode control characters commonly used in binary file signatures.
- */
- static boolean isPlaintext(Buffer buffer) {
- try {
- Buffer prefix = new Buffer();
- long byteCount = buffer.size() < 64 ? buffer.size() : 64;
- buffer.copyTo(prefix, 0, byteCount);
- for (int i = 0; i < 16; i++) {
- if (prefix.exhausted()) {
- break;
- }
- int codePoint = prefix.readUtf8CodePoint();
- if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
- return false;
- }
- }
- return true;
- } catch (EOFException e) {
- return false; // Truncated UTF-8 sequence.
- }
- }
-
- private static boolean bodyHasUnknownEncoding(Headers headers) {
- String contentEncoding = headers.get("Content-Encoding");
- return contentEncoding != null
- && !contentEncoding.equalsIgnoreCase("identity")
- && !contentEncoding.equalsIgnoreCase("gzip");
- }
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/io/EnsureContentLengthInputStreamResource.java b/src/main/java/im/zhaojun/zfile/core/io/EnsureContentLengthInputStreamResource.java
new file mode 100644
index 0000000..3eb59ad
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/io/EnsureContentLengthInputStreamResource.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.zhaojun.zfile.core.io;
+
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.InputStreamResource;
+
+import java.io.InputStream;
+
+/**
+ *
+ * 自定义 EnsureContentLengthInputStreamResource 可以保证必须实现 InputStream 的 contentLength 方法返回实际的长度.
+ * 此类相较于 {@link org.springframework.core.io.InputStreamResource} 仅实现了 contentLength 方法.
+ *
+ * {@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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/io/ThrottledInputStream.java b/src/main/java/im/zhaojun/zfile/core/io/ThrottledInputStream.java
new file mode 100644
index 0000000..2d8223f
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/io/ThrottledInputStream.java
@@ -0,0 +1,91 @@
+package im.zhaojun.zfile.core.io;
+
+import com.google.common.util.concurrent.RateLimiter;
+import jakarta.validation.constraints.NotNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * 使用装饰器模式, 限速输入流, 单位为字节/秒.
+ *
+ * @author zhaojun
+ */
+public final class ThrottledInputStream extends InputStream {
+
+ private final InputStream originalInputStream;
+ private final RateLimiter rateLimiter;
+
+ public ThrottledInputStream(InputStream originalInputStream, double bytesPerSecond) {
+ this.originalInputStream = originalInputStream;
+ this.rateLimiter = RateLimiter.create(bytesPerSecond);
+ }
+
+ @Override
+ public int read() throws IOException {
+ rateLimiter.acquire();
+ return originalInputStream.read();
+ }
+
+ @Override
+ public int read(@NotNull byte[] b) throws IOException {
+ return originalInputStream.read(b);
+ }
+
+ @Override
+ public int read(@NotNull byte[] b, int off, int len) throws IOException {
+ rateLimiter.acquire(len);
+ return originalInputStream.read(b, off, len);
+ }
+
+ @Override
+ public byte[] readAllBytes() throws IOException {
+ return originalInputStream.readAllBytes();
+ }
+
+ @Override
+ public byte[] readNBytes(int len) throws IOException {
+ return originalInputStream.readNBytes(len);
+ }
+
+ @Override
+ public int readNBytes(byte[] b, int off, int len) throws IOException {
+ return originalInputStream.readNBytes(b, off, len);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return originalInputStream.skip(n);
+ }
+
+ @Override
+ public void skipNBytes(long n) throws IOException {
+ originalInputStream.skipNBytes(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return originalInputStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ originalInputStream.close();
+ }
+
+ @Override
+ public void mark(int readlimit) {
+ originalInputStream.mark(readlimit);
+ }
+
+ @Override
+ public void reset() throws IOException {
+ originalInputStream.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return originalInputStream.markSupported();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/io/ThrottledOutputStream.java b/src/main/java/im/zhaojun/zfile/core/io/ThrottledOutputStream.java
new file mode 100644
index 0000000..5c04136
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/io/ThrottledOutputStream.java
@@ -0,0 +1,57 @@
+package im.zhaojun.zfile.core.io;
+
+import com.google.common.util.concurrent.RateLimiter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * 使用装饰器模式, 限速输出流, 单位为字节/秒.
+ *
+ * @author zhaojun
+ */
+@Slf4j
+public final class ThrottledOutputStream extends OutputStream {
+
+ private final OutputStream originalOutputStream;
+ private final RateLimiter rateLimiter;
+
+ public ThrottledOutputStream(OutputStream out, double bytesPerSecond) {
+ this.originalOutputStream = out;
+ this.rateLimiter = RateLimiter.create(bytesPerSecond);
+ }
+
+ public void setRate(double bytesPerSecond) {
+ rateLimiter.setRate(bytesPerSecond);
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ rateLimiter.acquire();
+ originalOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ rateLimiter.acquire(b.length);
+ originalOutputStream.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ rateLimiter.acquire(len);
+ originalOutputStream.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ originalOutputStream.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ originalOutputStream.close();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java b/src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java
index 877d99f..faf3cd3 100644
--- a/src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java
+++ b/src/main/java/im/zhaojun/zfile/core/model/request/PageQueryRequest.java
@@ -1,24 +1,26 @@
package im.zhaojun.zfile.core.model.request;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
+ * 通用分页请求对象,可继承该类增加业务字段.
+ *
* @author zhaojun
*/
@Data
public class PageQueryRequest {
- @ApiModelProperty(value="分页页数")
+ @Schema(name="分页页数")
private Integer page = 1;
- @ApiModelProperty(value="每页条数")
+ @Schema(name="每页条数")
private Integer limit = 10;
- @ApiModelProperty(value="排序字段")
+ @Schema(name="排序字段")
private String orderBy = "create_date";
- @ApiModelProperty(value="排序顺序")
+ @Schema(name="排序顺序")
private String orderDirection = "desc";
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/service/DynamicControllerManager.java b/src/main/java/im/zhaojun/zfile/core/service/DynamicControllerManager.java
deleted file mode 100644
index d2a3a2a..0000000
--- a/src/main/java/im/zhaojun/zfile/core/service/DynamicControllerManager.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package im.zhaojun.zfile.core.service;
-
-import org.springframework.stereotype.Service;
-import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
-import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
-
-import javax.annotation.Resource;
-import java.lang.reflect.Method;
-
-/**
- * @author zhaojun
- */
-@Service
-public class DynamicControllerManager {
-
- @Resource
- private RequestMappingHandlerMapping requestMappingHandlerMapping;
-
- private RequestMappingInfo shortLinkMappingInfo;
-
- private Object shortLinkHandler;
-
- private Method shortLinkMethod;
-
- public void initDirectLinkPrefixPath(String path, Object handler, Method method) {
- if (shortLinkMappingInfo != null) {
- throw new RuntimeException("请勿重复初始化 DirectLinkPrefixPath.");
- }
- shortLinkMappingInfo = RequestMappingInfo.paths(path + "/{storageKey}/**").build();
- shortLinkHandler = handler;
- shortLinkMethod = method;
- requestMappingHandlerMapping.registerMapping(shortLinkMappingInfo, handler, method);
- }
-
- public void changeDirectLinkPrefixPath(String path) {
- if (shortLinkMappingInfo != null) {
- requestMappingHandlerMapping.unregisterMapping(shortLinkMappingInfo);
- }
- shortLinkMappingInfo = RequestMappingInfo.paths(path + "/**").build();
- requestMappingHandlerMapping.registerMapping(shortLinkMappingInfo, shortLinkHandler, shortLinkMethod);
- }
-
-}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java b/src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
index c4f0a1f..39ae7f4 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/AjaxJson.java
@@ -1 +1,107 @@
-package im.zhaojun.zfile.core.util;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
/**
* ajax 请求返回 JSON 格式数据的封装
*
* @author zhaojun
*/
public class AjaxJson implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 0; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
public static final int REQUIRED_PASSWORD = 405; // 未输入密码
public static final int INVALID_PASSWORD = 406; // 无效的密码
@ApiModelProperty(value = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
private final int code;
@ApiModelProperty(value = "响应消息", example = "ok")
private String msg;
@ApiModelProperty(value = "响应数据")
private T data;
@ApiModelProperty(value = "数据总条数,分页情况有效")
private final Long dataCount;
@ApiModelProperty(value = "跟踪 ID")
private String traceId;
/**
* 返回code
*/
public int getCode() {
return this.code;
}
/**
* 给 msg 赋值,连缀风格
*/
public AjaxJson setMsg(String msg) {
this.msg = msg;
return this;
}
public String getMsg() {
return this.msg;
}
/**
* 给data赋值,连缀风格
*/
public AjaxJson setData(T data) {
this.data = data;
return this;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = null;
}
public AjaxJson(int 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", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson<>(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson> getSuccess(String msg, Object data) {
return new AjaxJson<>(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(T data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data, null);
}
public static AjaxJson> getSuccessArray(Object... data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data, null);
}
// 返回失败
public static AjaxJson> getError() {
return new AjaxJson<>(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson<>(CODE_ERROR, msg, null, null);
}
public static AjaxJson getBadRequestError(String msg) {
return new AjaxJson<>(CODE_INVALID_REQUEST, msg, null, null);
}
// 返回警告
public static AjaxJson> getWarning() {
return new AjaxJson<>(CODE_ERROR, "warning", null, null);
}
public static AjaxJson> getWarning(String msg) {
return new AjaxJson<>(CODE_WARNING, msg, null, null);
}
// 返回未登录
public static AjaxJson> getNotLogin() {
return new AjaxJson<>(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
// 返回没有权限的
public static AjaxJson> getNotJur(String msg) {
return new AjaxJson<>(CODE_NOT_JUR, msg, null, null);
}
// 返回一个自定义状态码的
public static AjaxJson> get(int code, String msg) {
return new AjaxJson<>(code, msg, null, null);
}
// 返回分页和数据的
public static AjaxJson> getPageData(Long dataCount, Object data) {
return new AjaxJson<>(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
public static AjaxJson> getByBoolean(boolean b) {
return b ? getSuccess("ok") : getError("error");
}
@Override
public String toString() {
return "AjaxJson{" +
"code=" + code +
", msg='" + msg + '\'' +
", data=" + data +
", dataCount=" + dataCount +
", traceId='" + traceId + '\'' +
'}';
}
public T getData() {
return data;
}
public Long getDataCount() {
return dataCount;
}
public void setTraceId(String traceId) {
this.traceId = traceId;
}
public String getTraceId() {
return traceId;
}
}
\ No newline at end of file
+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(name = "业务状态码,0 为正常,其他值均为异常,异常情况下见响应消息", example = "0")
+ private final String code;
+
+ @Schema(name = "响应消息", example = "ok")
+ private String msg;
+
+ @Schema(name = "响应数据")
+ private T data;
+
+ @Schema(name = "数据总条数,分页情况有效")
+ private final Long dataCount;
+
+ @Schema(name = "跟踪 ID")
+ private String traceId;
+
+ public AjaxJson(String code, String msg) {
+ if (code == null) {
+ code = ErrorCode.SYSTEM_ERROR.getCode();
+ }
+ this.code = code;
+ this.msg = msg;
+ this.dataCount = null;
+ }
+
+ public AjaxJson(String code, String msg, T data) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ this.dataCount = null;
+ }
+
+ public AjaxJson(String code, String msg, T data, Long dataCount) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ this.dataCount = dataCount;
+ }
+
+ // 返回成功
+ public static AjaxJson 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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java b/src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java
new file mode 100644
index 0000000..cac7772
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/ArrayUtils.java
@@ -0,0 +1,40 @@
+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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/CharPool.java b/src/main/java/im/zhaojun/zfile/core/util/CharPool.java
new file mode 100644
index 0000000..480188e
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/CharPool.java
@@ -0,0 +1,10 @@
+package im.zhaojun.zfile.core.util;
+
+public interface CharPool {
+
+ /**
+ * CHAR 常量:斜杠 {@code '/'} ASCII 47
+ */
+ char SLASH_CHAR = '/';
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java b/src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java
new file mode 100644
index 0000000..ae1728a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/CharSequenceUtil.java
@@ -0,0 +1,727 @@
+package im.zhaojun.zfile.core.util;
+
+import cn.hutool.core.text.StrSplitter;
+import jakarta.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 字符串工具类
+ *
+ * @author zhaojun
+ */
+public class CharSequenceUtil implements CharPool {
+
+ /**
+ * 找不到索引时的返回值
+ */
+ public static final int INDEX_NOT_FOUND = -1;
+
+ /**
+ * 字符串常量:{@code "null"}
+ * 注意:{@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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java b/src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
index 14a2543..057d55c 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/ClassUtils.java
@@ -1,5 +1,6 @@
package im.zhaojun.zfile.core.util;
+import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -10,6 +11,14 @@ import java.lang.reflect.Type;
*/
public class ClassUtils {
+ public static Class> forName(String className) {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
/**
* 获取指定类的泛型类型, 只获取第一个泛型类型
*
@@ -24,4 +33,9 @@ public class ClassUtils {
return (Class>) actualTypeArgument;
}
+ public static Class> getGenericType(Field field) {
+ ParameterizedType listType = (ParameterizedType) field.getGenericType();
+ return (Class>) listType.getActualTypeArguments()[0];
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/CodeMsg.java b/src/main/java/im/zhaojun/zfile/core/util/CodeMsg.java
deleted file mode 100644
index 59f23f0..0000000
--- a/src/main/java/im/zhaojun/zfile/core/util/CodeMsg.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package im.zhaojun.zfile.core.util;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
-
-/**
- * @author zhaojun
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-@Accessors(chain = true)
-public class CodeMsg {
-
- /**
- * 错误码
- *
- * 均为 5 位数, 如 00000, 10100, 20105 等.
- *
- * 第一位表示错误类型, 4 为用户请求输入错误, 5 为服务端处理错误, 6 为警告信息
- *
- * 第二位到第三位为二级类型
- *
- * 第四位到第五位为具体错误代码, 根据业务场景自行定义
- *
- * 以上三种类型均不允许重复, 且都需保持递增.
- */
- private String code;
-
- /**
- * 错误消息
- */
- private String msg;
-
- // 通用返回值
- public static CodeMsg SUCCESS = new CodeMsg("00000", "success");
- public static CodeMsg BAD_REQUEST = new CodeMsg("40000", "非法请求");
- public static CodeMsg ERROR = new CodeMsg("50000", "服务端异常");
-
-
- // -------------- 用户输入级错误 --------------
- public static CodeMsg REQUIRED_PASSWORD = new CodeMsg("40100", "请输入密码");
- public static CodeMsg PASSWORD_FAULT = new CodeMsg("40101", "密码输入错误");
-
- public static CodeMsg STORAGE_SOURCE_NOT_FOUND = new CodeMsg("40102", "无效的或初始化失败的存储源");
- public static CodeMsg STORAGE_SOURCE_FORBIDDEN = new CodeMsg("40103", "无权访问存储源");
- public static CodeMsg STORAGE_SOURCE_FILE_FORBIDDEN = new CodeMsg("40104", "无权访问该目录");
- public static CodeMsg STORAGE_SOURCE_ILLEGAL_OPERATION = new CodeMsg("40105", "非法操作");
-
-
-
- // -------------- 服务端处理错误 --------------
-
- // 初始化相关错误
- public static CodeMsg STORAGE_SOURCE_INIT_FAIL = new CodeMsg("50100", "初始化存储源失败");
- public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_CONFIG_FAIL = new CodeMsg("50101", "初始化存储源参数失败");
- public static CodeMsg STORAGE_SOURCE_INIT_STORAGE_PARAM_FIELD_FAIL = new CodeMsg("50102", "填充存储源字段失败");
-
-
- // 文件操作相关错误
- public static CodeMsg STORAGE_SOURCE_FILE_NEW_FOLDER_FAIL = new CodeMsg("50201", "新建文件夹失败");
- public static CodeMsg STORAGE_SOURCE_FILE_DELETE_FAIL = new CodeMsg("50202", "删除失败");
- public static CodeMsg STORAGE_SOURCE_FILE_RENAME_FAIL = new CodeMsg("50203", "重命名失败");
- public static CodeMsg STORAGE_SOURCE_FILE_GET_UPLOAD_FAIL = new CodeMsg("50204", "获取上传链接失败");
- public static CodeMsg STORAGE_SOURCE_FILE_PROXY_UPLOAD_FAIL = new CodeMsg("50205", "文件上传失败");
- public static CodeMsg STORAGE_SOURCE_FILE_PROXY_DOWNLOAD_FAIL = new CodeMsg("50206", "文件下载失败");
- public static CodeMsg STORAGE_SOURCE_FILE_GET_ITEM_FAIL = new CodeMsg("50207", "文件不存在或请求异常");
- public static CodeMsg STORAGE_SOURCE_FILE_DISABLE_PROXY_DOWNLOAD = new CodeMsg("50208", "非法操作, 当前文件不支持此类下载方式");
-
-
-
-
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java b/src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java
new file mode 100644
index 0000000..4749477
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/CollectionUtils.java
@@ -0,0 +1,131 @@
+package im.zhaojun.zfile.core.util;
+
+import cn.hutool.core.lang.func.Func1;
+
+import javax.annotation.Nullable;
+import java.util.*;
+
+public class CollectionUtils {
+
+
+ /**
+ * 判断集合是否为空
+ *
+ * @param collection
+ * 集合
+ *
+ * @return 是否为空
+ */
+ public static boolean isEmpty(@Nullable Collection> collection) {
+ return (collection == null || collection.isEmpty());
+ }
+
+
+ /**
+ * 判断集合是否不为空
+ *
+ * @param collection
+ * 集合
+ *
+ * @return 是否不为空
+ */
+ public static boolean isNotEmpty(@Nullable Collection> collection) {
+ return !isEmpty(collection);
+ }
+
+
+ /**
+ * 从集合中获取第一个元素, 如果集合为空则返回 {@code null}
+ *
+ * @param list
+ * 集合,可能为 {@code null}
+ *
+ * @return 第一个元素,如果集合为空则返回 {@code null}
+ */
+ @Nullable
+ public static T 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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java b/src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java
new file mode 100644
index 0000000..4c636ff
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/DnsUtil.java
@@ -0,0 +1,52 @@
+package im.zhaojun.zfile.core.util;
+
+import com.alibaba.dcm.DnsCacheManipulator;
+import com.alibaba.fastjson2.JSONArray;
+import org.springframework.lang.Nullable;
+
+public class DnsUtil {
+
+ /**
+ * 通过 HTTP DNS 获取域名对应的 IP 地址
+ *
+ * @param domain
+ * 域名
+ *
+ * @return IP 地址数组
+ */
+ public static @Nullable String[] getDomainIpByHttpDns(String domain) {
+ String jsonArrayStr = cn.hutool.http.HttpUtil.get("http://223.5.5.5/resolve?name=" + domain + "&short=1", 3000);
+ JSONArray jsonArray = JSONArray.parseArray(jsonArrayStr);
+ if (!jsonArray.isEmpty()) {
+ String[] result = new String[jsonArray.size()];
+ for (int i = 0; i < jsonArray.size(); i++) {
+ result[i] = jsonArray.getString(i);
+ }
+ return result;
+ } else {
+ return null;
+ }
+ }
+
+
+ /**
+ * 通过 HTTP DNS 获取域名对应的 IP 地址, 并设置 DNS 缓存.
+ *
+ * @param domain
+ * 域名
+ *
+ * @param cacheTime
+ * 缓存时间, 单位: 毫秒
+ *
+ * @return IP 地址数组
+ */
+ public static String[] getDomainIpByHttpDnsAndCache(String domain, int cacheTime) {
+ String[] domainIpByHttpDns = getDomainIpByHttpDns(domain);
+ if (domainIpByHttpDns != null) {
+ // 设置 DNS 缓存
+ DnsCacheManipulator.setDnsCache(cacheTime, domain, domainIpByHttpDns);
+ }
+ return domainIpByHttpDns;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/FileComparator.java b/src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
index 41cdc61..2acda55 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/FileComparator.java
@@ -8,12 +8,12 @@ import java.util.Comparator;
/**
* 文件比较器
- *
- * - 文件夹始终比文件排序高
- * - 默认按照名称排序
- * - 默认排序为升序
- * - 按名称排序不区分大小写
- *
+ *
+ * - 文件夹始终比文件排序高
+ * - 默认按照名称排序
+ * - 默认排序为升序
+ * - 按名称排序不区分大小写
+ *
* @author zhaojun
*/
public class FileComparator implements Comparator {
@@ -52,12 +52,11 @@ public class FileComparator implements Comparator {
FileTypeEnum o2Type = o2.getType();
NaturalOrderComparator naturalOrderComparator = new NaturalOrderComparator();
if (o1Type.equals(o2Type)) {
- int result;
- switch (sortBy) {
- case "time": result = CompareUtil.compare(o1.getTime(), o2.getTime()); break;
- case "size": result = CompareUtil.compare(o1.getSize(), o2.getSize()); break;
- default: result = naturalOrderComparator.compare(o1.getName(), o2.getName()); break;
- }
+ int result = switch (sortBy) {
+ case "time" -> CompareUtil.compare(o1.getTime(), o2.getTime());
+ case "size" -> CompareUtil.compare(o1.getSize(), o2.getSize());
+ default -> naturalOrderComparator.compare(o1.getName(), o2.getName());
+ };
return "asc".equals(order) ? result : -result;
}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java b/src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java
index 0b83d9b..618412e 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/FileResponseUtil.java
@@ -1,18 +1,17 @@
package im.zhaojun.zfile.core.util;
import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.util.StrUtil;
+import im.zhaojun.zfile.core.exception.ErrorCode;
+import im.zhaojun.zfile.core.exception.status.NotFoundAccessException;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.File;
-import java.nio.charset.StandardCharsets;
+import java.util.Collections;
/**
* 将文件输出对象
@@ -36,21 +35,18 @@ public class FileResponseUtil {
*/
public static ResponseEntity exportSingleThread(File file, String fileName) {
if (!file.exists()) {
- ByteArrayResource byteArrayResource = new ByteArrayResource("文件不存在或异常,请联系管理员.".getBytes(StandardCharsets.UTF_8));
- return ResponseEntity.status(HttpStatus.NOT_FOUND)
- .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
- .body(byteArrayResource);
+ throw new NotFoundAccessException(ErrorCode.BIZ_FILE_NOT_EXIST);
}
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
HttpHeaders headers = new HttpHeaders();
- if (StrUtil.isEmpty(fileName)) {
+ if (StringUtils.isEmpty(fileName)) {
fileName = file.getName();
}
- headers.setContentDispositionFormData("attachment", StringUtils.encodeAllIgnoreSlashes(fileName));
+ headers.put(HttpHeaders.CONTENT_DISPOSITION, Collections.singletonList("inline; filename=\"" + StringUtils.encodeAllIgnoreSlashes(fileName) + "\""));
return ResponseEntity
.ok()
diff --git a/src/main/java/im/zhaojun/zfile/core/util/FileUtils.java b/src/main/java/im/zhaojun/zfile/core/util/FileUtils.java
new file mode 100644
index 0000000..b3e617f
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/FileUtils.java
@@ -0,0 +1,46 @@
+package im.zhaojun.zfile.core.util;
+
+import org.apache.commons.io.FilenameUtils;
+
+/**
+ * 文件相关工具类
+ *
+ * @author zhaojun
+ */
+public class FileUtils {
+
+ public static String getName(final String fileName) {
+ if (fileName == null) {
+ return null;
+ }
+
+ int i = fileName.lastIndexOf(CharSequenceUtil.SLASH_CHAR);
+ if (i >= 0 && i <= fileName.length() - 1) {
+ return fileName.substring(i + 1);
+ }
+
+ return fileName;
+ }
+
+ public static String getParentPath(final String fileName) {
+ String fullPathNoEndSeparator = FilenameUtils.getFullPathNoEndSeparator(fileName);
+ if (fullPathNoEndSeparator == null || fullPathNoEndSeparator.isEmpty()) {
+ return StringUtils.SLASH;
+ }
+ return fullPathNoEndSeparator;
+ }
+
+ public static String getExtension(final String fileName) throws IllegalArgumentException {
+ if (fileName == null) {
+ return null;
+ }
+
+ int i = fileName.lastIndexOf('.');
+ if (i > 0 && i < fileName.length() - 1) {
+ return fileName.substring(i + 1);
+ }
+
+ return "";
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java b/src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java
index 88f19bb..496d35a 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/HttpUtil.java
@@ -1,10 +1,9 @@
package im.zhaojun.zfile.core.util;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.util.StrUtil;
import im.zhaojun.zfile.core.constant.ZFileConstant;
-import im.zhaojun.zfile.core.exception.PreviewException;
-import im.zhaojun.zfile.core.exception.TextParseException;
+import im.zhaojun.zfile.core.exception.ErrorCode;
+import im.zhaojun.zfile.core.exception.biz.GetPreviewTextContentBizException;
+import im.zhaojun.zfile.core.exception.core.BizException;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@@ -19,7 +18,6 @@ import java.net.URLConnection;
@Slf4j
public class HttpUtil {
-
/**
* 获取 URL 对应的文件内容
*
@@ -32,14 +30,14 @@ public class HttpUtil {
long maxFileSize = 1024 * ZFileConstant.TEXT_MAX_FILE_SIZE_KB;
if (getRemoteFileSize(url) > maxFileSize) {
- throw new PreviewException("预览文件超出大小, 最大支持 " + FileUtil.readableFileSize(maxFileSize));
+ throw new BizException(ErrorCode.BIZ_PREVIEW_FILE_SIZE_EXCEED);
}
String result;
try {
result = cn.hutool.http.HttpUtil.get(url);
} catch (Exception e) {
- throw new TextParseException(StrUtil.format("获取文件内容失败, URL: {}", url), e);
+ throw new GetPreviewTextContentBizException(url, e);
}
return result == null ? "" : result;
diff --git a/src/main/java/im/zhaojun/zfile/core/util/NumberUtils.java b/src/main/java/im/zhaojun/zfile/core/util/NumberUtils.java
new file mode 100644
index 0000000..fd7be9a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/NumberUtils.java
@@ -0,0 +1,18 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/OnlyOfficeKeyCacheUtils.java b/src/main/java/im/zhaojun/zfile/core/util/OnlyOfficeKeyCacheUtils.java
new file mode 100644
index 0000000..5ee513f
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/OnlyOfficeKeyCacheUtils.java
@@ -0,0 +1,98 @@
+package im.zhaojun.zfile.core.util;
+
+import cn.hutool.cache.Cache;
+import cn.hutool.cache.CacheUtil;
+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.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;
+ }
+
+ /**
+ * 获取文件锁, 防止并发操作文件缓存时出现问题.
+ *
+ * @param key
+ * 文件唯一标识
+ *
+ * @return 锁对象
+ */
+ public static ReentrantLock getLock(OnlyOfficeFile key) {
+ return locks.get(key, true, ReentrantLock::new);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java b/src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java
index c05606e..3d931e5 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/PatternMatcherUtils.java
@@ -1,8 +1,5 @@
package im.zhaojun.zfile.core.util;
-import cn.hutool.core.util.StrUtil;
-import im.zhaojun.zfile.core.constant.ZFileConstant;
-
import java.nio.file.FileSystems;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
@@ -26,7 +23,7 @@ public class PatternMatcherUtils {
* test1: /a
* test2: /a/
*
- * test1 和 test 2 均无法匹配这种情况, 此方法兼容了这种情况, 即对 test 内容后拼接 "/xx", 使其可以匹配上 pattern.
+ *
test1 和 test 2 均无法匹配这种情况, 此方法兼容了这种情况, 即对 test 内容后拼接 "/xx"(其实任意字符都可以), 使其可以匹配上 pattern.
*
注意:但此方法对包含文件名的情况无效, 仅支持 test 为 路径的情况.
*
* @param pattern
@@ -39,13 +36,13 @@ public class PatternMatcherUtils {
*/
public static boolean testCompatibilityGlobPattern(String pattern, String test) {
// 如果规则表达式最开始没有 /, 则兼容在最前方加上 /.
- if (!StrUtil.startWith(pattern, ZFileConstant.PATH_SEPARATOR)) {
- pattern = ZFileConstant.PATH_SEPARATOR + pattern;
+ if (!StringUtils.startWith(pattern, StringUtils.SLASH)) {
+ pattern = StringUtils.SLASH + pattern;
}
// 兼容性处理.
- test = StringUtils.concat(test, ZFileConstant.PATH_SEPARATOR);
- if (StrUtil.endWith(pattern, "/**")) {
+ test = StringUtils.concat(test, StringUtils.SLASH);
+ if (StringUtils.endWith(pattern, "/**") || StringUtils.endWith(pattern, "/*")) {
test += "xxx";
}
return testGlobPattern(pattern, test);
@@ -66,7 +63,7 @@ public class PatternMatcherUtils {
PathMatcher pathMatcher = PATH_MATCHER_MAP.getOrDefault(pattern, FileSystems.getDefault().getPathMatcher("glob:" + pattern));
PATH_MATCHER_MAP.put(pattern, pathMatcher);
- return pathMatcher.matches(Paths.get(test)) || StrUtil.equals(pattern, test);
+ return pathMatcher.matches(Paths.get(test)) || StringUtils.equals(pattern, test);
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/PlaceholderUtils.java b/src/main/java/im/zhaojun/zfile/core/util/PlaceholderUtils.java
index 043c13e..df1759d 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/PlaceholderUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/PlaceholderUtils.java
@@ -1,15 +1,9 @@
package im.zhaojun.zfile.core.util;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
/**
* 配置文件或模板中的占位符替换工具类
@@ -23,6 +17,7 @@ public class PlaceholderUtils {
* Prefix for system property placeholders: "${"
*/
public static final String PLACEHOLDER_PREFIX = "${";
+
/**
* Suffix for system property placeholders: "}"
*/
@@ -31,7 +26,7 @@ public class PlaceholderUtils {
/**
* 解析占位符, 将指定的占位符替换为指定的值. 变量值从 Spring 环境中获取, 如没取到, 则默认为空.
- *
+ *
* 必须在 Spring 环境下使用, 否则会抛出异常.
*
*
@@ -42,7 +37,7 @@ public class PlaceholderUtils {
*/
public static String resolvePlaceholdersBySpringProperties(String formatStr) {
String placeholderName = getFirstPlaceholderName(formatStr);
- if (StrUtil.isEmpty(placeholderName)) {
+ if (StringUtils.isEmpty(placeholderName)) {
return formatStr;
}
@@ -68,17 +63,17 @@ public class PlaceholderUtils {
if (parameter == null || parameter.isEmpty()) {
return formatStr;
}
- StringBuffer buf = new StringBuffer(formatStr);
- int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);
+ StringBuilder sb = new StringBuilder(formatStr);
+ int startIndex = sb.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
- int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
+ int endIndex = sb.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
- String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
+ String placeholder = sb.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
try {
String propVal = parameter.get(placeholder);
if (propVal != null) {
- buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
+ sb.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);
nextIndex = startIndex + propVal.length();
} else {
log.warn("Could not resolve placeholder '{}' in [{}] ", placeholder, formatStr);
@@ -86,12 +81,12 @@ public class PlaceholderUtils {
} catch (Exception ex) {
log.error("Could not resolve placeholder '{}' in [{}]: ", placeholder, formatStr, ex);
}
- startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);
+ startIndex = sb.indexOf(PLACEHOLDER_PREFIX, nextIndex);
} else {
startIndex = -1;
}
}
- return buf.toString();
+ return sb.toString();
}
@@ -105,8 +100,8 @@ public class PlaceholderUtils {
*/
public static String getFirstPlaceholderName(String formatStr) {
List list = getPlaceholderNames(formatStr);
- if (CollUtil.isNotEmpty(list)) {
- return list.get(0);
+ if (CollectionUtils.isNotEmpty(list)) {
+ return list.getFirst();
}
return null;
}
@@ -121,20 +116,20 @@ public class PlaceholderUtils {
* @return 占位符名称
*/
public static List getPlaceholderNames(String formatStr) {
- if (StrUtil.isEmpty(formatStr)) {
+ if (StringUtils.isEmpty(formatStr)) {
return Collections.emptyList();
}
List placeholderNameList = new ArrayList<>();
- StringBuffer buf = new StringBuffer(formatStr);
- int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);
+ StringBuilder sb = new StringBuilder(formatStr);
+ int startIndex = sb.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
- int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
+ int endIndex = sb.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());
if (endIndex != -1) {
- String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
+ String placeholder = sb.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();
- startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);
+ startIndex = sb.indexOf(PLACEHOLDER_PREFIX, nextIndex);
placeholderNameList.add(placeholder);
} else {
startIndex = -1;
diff --git a/src/main/java/im/zhaojun/zfile/core/util/ProxyDownloadUrlUtils.java b/src/main/java/im/zhaojun/zfile/core/util/ProxyDownloadUrlUtils.java
index b80e82d..0baf26e 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/ProxyDownloadUrlUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/ProxyDownloadUrlUtils.java
@@ -3,7 +3,6 @@ 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.core.util.StrUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import cn.hutool.extra.spring.SpringUtil;
@@ -22,14 +21,28 @@ public class ProxyDownloadUrlUtils {
private static SystemConfigService systemConfigService;
- private static final String PROXY_DOWNLOAD_LINK_DELIMITER= ":";
+ private static final String PROXY_DOWNLOAD_LINK_DELIMITER = ":";
/**
- * 服务器代理下载 URL 有效期 (分钟).
+ * 服务器代理下载 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);
@@ -44,8 +57,8 @@ public class ProxyDownloadUrlUtils {
long second = DateUtil.offsetSecond(DateUtil.date(), effectiveSecond).getTime();
String content = storageId + PROXY_DOWNLOAD_LINK_DELIMITER + pathAndName + PROXY_DOWNLOAD_LINK_DELIMITER + second;
- String rsaHexKey = systemConfigService.getRsaHexKeyOrGenerate();
- byte[] key = HexUtil.decodeHex(rsaHexKey);
+ String aesHexKey = systemConfigService.getAesHexKeyOrGenerate();
+ byte[] key = HexUtil.decodeHex(aesHexKey);
//构建
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
@@ -59,8 +72,8 @@ public class ProxyDownloadUrlUtils {
systemConfigService = SpringUtil.getBean(SystemConfigService.class);
}
- String rsaHexKey = systemConfigService.getRsaHexKeyOrGenerate();
- byte[] key = HexUtil.decodeHex(rsaHexKey);
+ String aesHexKey = systemConfigService.getAesHexKeyOrGenerate();
+ byte[] key = HexUtil.decodeHex(aesHexKey);
SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
long currentTimeMillis = System.currentTimeMillis();
@@ -72,14 +85,14 @@ public class ProxyDownloadUrlUtils {
try {
//解密
String decryptStr = aes.decryptStr(signature);
- List split = StrUtil.split(decryptStr, PROXY_DOWNLOAD_LINK_DELIMITER);
+ List split = StringUtils.split(decryptStr, PROXY_DOWNLOAD_LINK_DELIMITER);
storageId = split.get(0);
pathAndName = split.get(1);
expiredSecond = split.get(2);
// 校验存储源 ID 和文件路径及是否过期.
- if (StrUtil.equals(storageId, Convert.toStr(expectedStorageId))
- && StrUtil.equals(StringUtils.concat(pathAndName), StringUtils.concat(expectedPathAndName))
+ if (StringUtils.equals(storageId, Convert.toStr(expectedStorageId))
+ && StringUtils.equals(StringUtils.concat(pathAndName), StringUtils.concat(expectedPathAndName))
&& currentTimeMillis < Convert.toLong(expiredSecond)) {
return true;
}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/RequestHolder.java b/src/main/java/im/zhaojun/zfile/core/util/RequestHolder.java
index a2f27d8..9d160d0 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/RequestHolder.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/RequestHolder.java
@@ -1,25 +1,36 @@
package im.zhaojun.zfile.core.util;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.io.IoUtil;
+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 im.zhaojun.zfile.core.io.ThrottledOutputStream;
+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.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 javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
-import java.util.function.Function;
/**
* 获取 Request 工具类
*
* @author zhaojun
*/
+@Slf4j
public class RequestHolder {
/**
@@ -27,7 +38,7 @@ public class RequestHolder {
*
* @return HttpServletRequest
*/
- public static HttpServletRequest getRequest(){
+ public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
}
@@ -37,7 +48,7 @@ public class RequestHolder {
*
* @return HttpServletResponse
*/
- public static HttpServletResponse getResponse(){
+ public static HttpServletResponse getResponse() {
return ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
}
@@ -45,27 +56,135 @@ public class RequestHolder {
/**
* 向 response 写入文件流.
*
- * @param function
- * 文件输入流获取函数
+ * @param inputStream
+ * 文件输入流
*
- * @param path
- * 文件路径
+ * @param fileName
+ * 文件名称
+ *
+ * @param fileSize
+ * 文件大小
+ *
+ * @param isPartialContentFromInputStream
+ * 表示输入流是否为部分内容。
+ * 当该变量为 true 时,表示输入流已经根据 range 规则从存储源获取部分内容。
+ * 在这种情况下,不需要跳过 range start 部分,可以直接从输入流的全部内容复制到输出流。
+ *
+ * @param forceDownload
+ * 是否强制下载
*/
- public static void writeFile(Function function, String path){
- try (InputStream inputStream = function.apply(path)) {
- HttpServletResponse response = RequestHolder.getResponse();
- String fileName = FileUtil.getName(path);
-
- response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + StringUtils.encodeAllIgnoreSlashes(fileName));
- response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
-
- OutputStream outputStream = response.getOutputStream();
-
- IoUtil.copy(inputStream, outputStream);
- response.flushBuffer();
- } catch (IOException e) {
- throw new RuntimeException(e);
+ 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();
+
+ if (forceDownload) {
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + StringUtils.encodeAllIgnoreSlashes(fileName));
+ } else {
+ response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + StringUtils.encodeAllIgnoreSlashes(fileName));
+ 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);
+ }
+
+ /**
+ * 获取后端服务地址,如果经过了反向代理,需反向代理正确配置
+ *
+ * @return Axios-From 字段
+ */
+ public static String getRequestServerAddress() {
+ if (RequestContextHolder.getRequestAttributes() == null) {
+ return null;
+ }
+ HttpServletRequest request = RequestHolder.getRequest();
+ StringBuffer requestURL = request.getRequestURL();
+ String result = requestURL.substring(0, requestURL.indexOf(request.getRequestURI()));
+
+ String header = JakartaServletUtil.getHeaderIgnoreCase(request, "X-Forwarded-Proto");
+ if (StringUtils.isNotEmpty(header)) {
+ return StringUtils.setSchema(result, header);
+ }
+ return result;
+ }
+
+
+ /**
+ * 获取当前请求的 Origin 请求头
+ *
+ * @return Origin 请求头值
+ */
+ public static String getOriginAddress() {
+ if (RequestContextHolder.getRequestAttributes() == null) {
+ return null;
+ }
+ HttpServletRequest request = RequestHolder.getRequest();
+ return JakartaServletUtil.getHeaderIgnoreCase(request, HttpHeaders.ORIGIN);
}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/RequestUtils.java b/src/main/java/im/zhaojun/zfile/core/util/RequestUtils.java
new file mode 100644
index 0000000..8a70913
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/RequestUtils.java
@@ -0,0 +1,18 @@
+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));
+ }
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/config/utils/SpringMvcUtils.java b/src/main/java/im/zhaojun/zfile/core/util/SpringMvcUtils.java
similarity index 82%
rename from src/main/java/im/zhaojun/zfile/module/config/utils/SpringMvcUtils.java
rename to src/main/java/im/zhaojun/zfile/core/util/SpringMvcUtils.java
index 00934b2..b96cfda 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/utils/SpringMvcUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/SpringMvcUtils.java
@@ -1,10 +1,9 @@
-package im.zhaojun.zfile.module.config.utils;
+package im.zhaojun.zfile.core.util;
-import im.zhaojun.zfile.core.util.RequestHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerMapping;
-import javax.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletRequest;
/**
* @author zhaojun
@@ -19,4 +18,4 @@ public class SpringMvcUtils {
return apm.extractPathWithinPattern(bestMatchPattern, path);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/StrPool.java b/src/main/java/im/zhaojun/zfile/core/util/StrPool.java
new file mode 100644
index 0000000..94c989a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/StrPool.java
@@ -0,0 +1,98 @@
+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 = "{}";
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/StringUtils.java b/src/main/java/im/zhaojun/zfile/core/util/StringUtils.java
index 404bddf..6e53da9 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/StringUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/StringUtils.java
@@ -3,10 +3,6 @@ package im.zhaojun.zfile.core.util;
import cn.hutool.core.net.URLEncodeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
-import cn.hutool.extra.spring.SpringUtil;
-import im.zhaojun.zfile.module.config.service.SystemConfigService;
-import im.zhaojun.zfile.core.constant.ZFileConstant;
-import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import java.net.MalformedURLException;
import java.net.URL;
@@ -17,11 +13,7 @@ import java.nio.charset.StandardCharsets;
*
* @author zhaojun
*/
-public class StringUtils {
-
- public static final char DELIMITER = '/';
-
- public static final String DELIMITER_STR = "/";
+public class StringUtils extends CharSequenceUtil implements StrPool {
public static final String HTTP = "http";
@@ -47,21 +39,21 @@ public class StringUtils {
/**
- * 移除 URL 中的第一个 '/'
+ * 移除 URL 中的前面的所有 '/'
*
* @param path
* 路径
*
* @return 如 path = '/folder1/file1', 返回 'folder1/file1'
- * 如 path = '/folder1/file1', 返回 'folder1/file1'
+ * 如 path = '//folder1/file1', 返回 'folder1/file1'
*
*/
public static String trimStartSlashes(String path) {
- if (StrUtil.isEmpty(path)) {
+ if (isEmpty(path)) {
return path;
}
- while (path.startsWith(DELIMITER_STR)) {
+ while (path.startsWith(SLASH)) {
path = path.substring(1);
}
@@ -70,7 +62,7 @@ public class StringUtils {
/**
- * 移除 URL 中的最后一个 '/'
+ * 移除 URL 中结尾的所有 '/'
*
* @param path
* 路径
@@ -79,11 +71,11 @@ public class StringUtils {
* 如 path = '/folder1/file1///', 返回 '/folder1/file1'
*/
public static String trimEndSlashes(String path) {
- if (StrUtil.isEmpty(path)) {
+ if (isEmpty(path)) {
return path;
}
- while (path.endsWith(DELIMITER_STR)) {
+ while (path.endsWith(SLASH)) {
path = path.substring(0, path.length() - 1);
}
@@ -101,23 +93,23 @@ public class StringUtils {
* 如 path = '/folder1////file1///', 返回 '/folder1/file1/'
*/
public static String removeDuplicateSlashes(String path) {
- if (StrUtil.isEmpty(path)) {
+ if (isEmpty(path)) {
return path;
}
StringBuilder sb = new StringBuilder();
// 是否包含 http 或 https 协议信息
- boolean containProtocol = StrUtil.containsAnyIgnoreCase(path, HTTP_PROTOCOL, HTTPS_PROTOCOL);
+ boolean containProtocol = StringUtils.containsAnyIgnoreCase(path, HTTP_PROTOCOL, HTTPS_PROTOCOL);
if (containProtocol) {
path = trimStartSlashes(path);
}
// 是否包含 http 协议信息
- boolean startWithHttpProtocol = StrUtil.startWithIgnoreCase(path, HTTP_PROTOCOL);
+ boolean startWithHttpProtocol = StringUtils.startWithIgnoreCase(path, HTTP_PROTOCOL);
// 是否包含 https 协议信息
- boolean startWithHttpsProtocol = StrUtil.startWithIgnoreCase(path, HTTPS_PROTOCOL);
+ boolean startWithHttpsProtocol = StringUtils.startWithIgnoreCase(path, HTTPS_PROTOCOL);
if (startWithHttpProtocol) {
sb.append(HTTP_PROTOCOL);
@@ -128,7 +120,7 @@ public class StringUtils {
for (int i = sb.length(); i < path.length() - 1; i++) {
char current = path.charAt(i);
char next = path.charAt(i + 1);
- if (!(current == DELIMITER && next == DELIMITER)) {
+ if (!(current == SLASH_CHAR && next == SLASH_CHAR)) {
sb.append(current);
}
}
@@ -217,15 +209,15 @@ public class StringUtils {
* @return 拼接结果
*/
public static String concat(String... strs) {
- StringBuilder sb = new StringBuilder(DELIMITER_STR);
+ StringBuilder sb = new StringBuilder(SLASH);
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
- if (StrUtil.isEmpty(str)) {
+ if (isEmpty(str)) {
continue;
}
sb.append(str);
if (i != strs.length - 1) {
- sb.append(DELIMITER);
+ sb.append(SLASH_CHAR);
}
}
return removeDuplicateSlashes(sb.toString());
@@ -244,15 +236,15 @@ public class StringUtils {
* @return 拼接结果
*/
public static String concat(boolean encodeAllIgnoreSlashes, String... strs) {
- StringBuilder sb = new StringBuilder(DELIMITER_STR);
+ StringBuilder sb = new StringBuilder(SLASH);
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
- if (StrUtil.isEmpty(str)) {
+ if (isEmpty(str)) {
continue;
}
sb.append(str);
if (i != strs.length - 1) {
- sb.append(DELIMITER);
+ sb.append(SLASH_CHAR);
}
}
if (encodeAllIgnoreSlashes) {
@@ -263,26 +255,6 @@ public class StringUtils {
}
- /**
- * 拼接文件直链生成 URL
- *
- * @param storageKey
- * 存储源 ID
- *
- * @param fullPath
- * 文件全路径
- *
- * @return 生成结果
- */
- public static String generatorPathLink(String storageKey, String fullPath) {
- SystemConfigService systemConfigService = SpringUtil.getBean(SystemConfigService.class);
- SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
- String domain = systemConfig.getDomain();
- String directLinkPrefix = systemConfig.getDirectLinkPrefix();
- return concat(domain, directLinkPrefix, storageKey, encodeAllIgnoreSlashes(fullPath));
- }
-
-
/**
* 替换 URL 中的 Host 部分,如替换 http://a.com/1.txt 为 https://abc.com/1.txt
*
@@ -338,7 +310,7 @@ public class StringUtils {
* @return 编码后的字符
*/
public static String encodeAllIgnoreSlashes(String str) {
- if (StrUtil.isEmpty(str)) {
+ if (isEmpty(str)) {
return str;
}
@@ -347,7 +319,7 @@ public class StringUtils {
int prevIndex = -1;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
- if (c == ZFileConstant.PATH_SEPARATOR_CHAR) {
+ if (c == StringUtils.SLASH_CHAR) {
if (prevIndex < i) {
String substring = str.substring(prevIndex + 1, i);
sb.append(URLEncodeUtil.encodeAll(substring));
@@ -380,25 +352,140 @@ public class StringUtils {
/**
- * 获取路径的上级目录, 如最后为 /, 则也会认为是一级目录
+ * 移除字符串中所有换行符并去除前后空格
*
- * @param path
- * 文件路径
+ * @param str
+ * URL
*
- * @return 父级目录
+ * @return 移除协议后的 URL
*/
- public static String getParentPath(String path) {
- int toIndex = StrUtil.lastIndexOfIgnoreCase(path, ZFileConstant.PATH_SEPARATOR);
- if (toIndex <= 0) {
- return "/";
- } else {
- return StrUtil.sub(path, 0, toIndex);
- }
- }
-
public static String removeAllLineBreaksAndTrim(String str) {
String removeResult = StrUtil.removeAllLineBreaks(str);
- return StrUtil.trim(removeResult);
+ 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;
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/UrlUtils.java b/src/main/java/im/zhaojun/zfile/core/util/UrlUtils.java
index e88ad3b..f154fc0 100644
--- a/src/main/java/im/zhaojun/zfile/core/util/UrlUtils.java
+++ b/src/main/java/im/zhaojun/zfile/core/util/UrlUtils.java
@@ -8,9 +8,21 @@ import cn.hutool.core.util.StrUtil;
* @author zhaojun
*/
public class UrlUtils {
-
+
/**
- * 给 url 拼接参数
+ * 判断 URL 是否包含协议部分
+ *
+ * @param url
+ * URL 地址
+ *
+ * @return 是否包含协议部分
+ */
+ public static boolean hasScheme(String url) {
+ return url.startsWith("http://") || url.startsWith("https://");
+ }
+
+ /**
+ * 为 URL 拼接参数
*
* @param url
* 原始 URL
@@ -24,11 +36,77 @@ public class UrlUtils {
* @return 拼接后的 URL
*/
public static String concatQueryParam(String url, String name, String value) {
- if (StrUtil.contains(url, "?")) {
+ 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;
+ }
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/ZFileAuthUtil.java b/src/main/java/im/zhaojun/zfile/core/util/ZFileAuthUtil.java
new file mode 100644
index 0000000..5e889da
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/ZFileAuthUtil.java
@@ -0,0 +1,38 @@
+package im.zhaojun.zfile.core.util;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.extra.spring.SpringUtil;
+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);
+ }
+
+ return userService.getById(StpUtil.getLoginId(UserConstant.ANONYMOUS_ID));
+ }
+
+ public static Integer getCurrentUserId() {
+ if (userService == null) {
+ userService = SpringUtil.getBean(UserService.class);
+ }
+
+ try {
+ return StpUtil.getLoginId(UserConstant.ANONYMOUS_ID);
+ } catch (Exception e) {
+ return UserConstant.ANONYMOUS_ID;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/AbstractRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/AbstractRuleMatcher.java
new file mode 100644
index 0000000..cb0f625
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/AbstractRuleMatcher.java
@@ -0,0 +1,43 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/IRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/IRuleMatcher.java
new file mode 100644
index 0000000..a7b0b9c
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/IRuleMatcher.java
@@ -0,0 +1,75 @@
+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();
+
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/RuleMatcherFactory.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/RuleMatcherFactory.java
new file mode 100644
index 0000000..1b654d9
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/RuleMatcherFactory.java
@@ -0,0 +1,42 @@
+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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/AntPathRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/AntPathRuleMatcher.java
new file mode 100644
index 0000000..20186f0
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/AntPathRuleMatcher.java
@@ -0,0 +1,32 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/IpRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/IpRuleMatcher.java
new file mode 100644
index 0000000..6cf0c21
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/IpRuleMatcher.java
@@ -0,0 +1,272 @@
+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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/RegexRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/RegexRuleMatcher.java
new file mode 100644
index 0000000..ee82e04
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/RegexRuleMatcher.java
@@ -0,0 +1,39 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/SpringSimpleRuleMatcher.java b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/SpringSimpleRuleMatcher.java
new file mode 100644
index 0000000..c72de6c
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/core/util/matcher/impl/SpringSimpleRuleMatcher.java
@@ -0,0 +1,35 @@
+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);
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/core/validation/StringListValue.java b/src/main/java/im/zhaojun/zfile/core/validation/StringListValue.java
index be5868d..3999eb4 100644
--- a/src/main/java/im/zhaojun/zfile/core/validation/StringListValue.java
+++ b/src/main/java/im/zhaojun/zfile/core/validation/StringListValue.java
@@ -1,7 +1,7 @@
package im.zhaojun.zfile.core.validation;
-import javax.validation.Constraint;
-import javax.validation.Payload;
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
diff --git a/src/main/java/im/zhaojun/zfile/core/validation/StringListValueConstraintValidator.java b/src/main/java/im/zhaojun/zfile/core/validation/StringListValueConstraintValidator.java
index b7a3f57..92a4431 100644
--- a/src/main/java/im/zhaojun/zfile/core/validation/StringListValueConstraintValidator.java
+++ b/src/main/java/im/zhaojun/zfile/core/validation/StringListValueConstraintValidator.java
@@ -1,9 +1,9 @@
package im.zhaojun.zfile.core.validation;
-import cn.hutool.core.util.StrUtil;
+import im.zhaojun.zfile.core.util.StringUtils;
+import jakarta.validation.ConstraintValidator;
+import jakarta.validation.ConstraintValidatorContext;
-import javax.validation.ConstraintValidator;
-import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -44,7 +44,7 @@ public class StringListValueConstraintValidator implements ConstraintValidator clientIp() {
- String clientIp = ServletUtil.getClientIP(httpServletRequest);
+ String clientIp = JakartaServletUtil.getClientIP(httpServletRequest);
return AjaxJson.getSuccessData(clientIp);
}
diff --git a/src/main/java/im/zhaojun/zfile/module/admin/controller/RuleMatcherTestController.java b/src/main/java/im/zhaojun/zfile/module/admin/controller/RuleMatcherTestController.java
new file mode 100644
index 0000000..e73d267
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/admin/controller/RuleMatcherTestController.java
@@ -0,0 +1,50 @@
+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));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/admin/model/request/TestRuleMatcherRequest.java b/src/main/java/im/zhaojun/zfile/module/admin/model/request/TestRuleMatcherRequest.java
new file mode 100644
index 0000000..f8c7d80
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/admin/model/request/TestRuleMatcherRequest.java
@@ -0,0 +1,19 @@
+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;
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/annotation/JSONStringParse.java b/src/main/java/im/zhaojun/zfile/module/config/annotation/JSONStringParse.java
new file mode 100644
index 0000000..1c8706c
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/config/annotation/JSONStringParse.java
@@ -0,0 +1,17 @@
+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 {
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/constant/SystemConfigConstant.java b/src/main/java/im/zhaojun/zfile/module/config/constant/SystemConfigConstant.java
index d0ec097..7fc8e55 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/constant/SystemConfigConstant.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/constant/SystemConfigConstant.java
@@ -13,6 +13,7 @@ public class SystemConfigConstant {
public static final String LOGIN_VERIFY_MODE = "loginVerifyMode";
- public static final String RSA_HEX_KEY = "rsaHexKey";
+ // 这里名称和值不一样是历史遗留问题,最开始设计时弄混了名称,实际使用的是 aes
+ public static final String AES_HEX_KEY = "rsaHexKey";
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/controller/AntPathMatcherHelperController.java b/src/main/java/im/zhaojun/zfile/module/config/controller/AntPathMatcherHelperController.java
deleted file mode 100644
index c8a2e6f..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/controller/AntPathMatcherHelperController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package im.zhaojun.zfile.module.config.controller;
-
-import cn.hutool.core.util.StrUtil;
-import im.zhaojun.zfile.core.util.AjaxJson;
-import im.zhaojun.zfile.module.config.model.request.TestAntPathMatcherRequest;
-import im.zhaojun.zfile.module.link.aspect.RefererCheckAspect;
-import io.swagger.annotations.Api;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import java.util.List;
-
-/**
- * @author zhaojun
- */
-@Api(tags = "Ant Path Matcher 辅助 Controller")
-@Slf4j
-@RequestMapping("/admin")
-@RestController
-public class AntPathMatcherHelperController {
-
- @Resource
- private RefererCheckAspect refererCheckAspect;
-
- @PostMapping("/ant-path-test")
- public AjaxJson clientIp(@RequestBody TestAntPathMatcherRequest testAntPathMatcherRequest) {
- if (testAntPathMatcherRequest == null) {
- return AjaxJson.getSuccessData(false);
- }
- String testPath = testAntPathMatcherRequest.getTestPath();
- String antPath = testAntPathMatcherRequest.getAntPath();
- if (StrUtil.isBlank(testPath) || StrUtil.isBlank(antPath)) {
- return AjaxJson.getSuccessData(false);
- }
- List refererValueList = StrUtil.split(antPath, '\n');
- return AjaxJson.getSuccessData(refererCheckAspect.containsPathMatcher(refererValueList, testPath));
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/controller/InstallController.java b/src/main/java/im/zhaojun/zfile/module/config/controller/InstallController.java
deleted file mode 100644
index 1f19453..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/controller/InstallController.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package im.zhaojun.zfile.module.config.controller;
-
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.crypto.SecureUtil;
-import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
-import im.zhaojun.zfile.module.config.service.SystemConfigService;
-import im.zhaojun.zfile.core.exception.InstallSystemException;
-import im.zhaojun.zfile.core.util.AjaxJson;
-import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
-import im.zhaojun.zfile.module.config.model.request.InstallSystemRequest;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-
-/**
- * 系统初始化接口
- *
- * @author zhaojun
- */
-@Api(tags = "初始化模块")
-@RestController
-@RequestMapping("/api")
-public class InstallController {
-
- @Resource
- private SystemConfigService systemConfigService;
-
-
- @GetMapping("/install/status")
- @ApiOperationSupport(order = 1)
- @ApiOperation(value = "获取系统初始化状态", notes = "根据管理员用户名是否存在判断系统已初始化, 已初始化返回 true, 未初始化返回 false")
- public AjaxJson isInstall() {
- return AjaxJson.getSuccessData(systemConfigService.getSystemIsInstalled());
- }
-
-
- @ApiOperationSupport(order = 2)
- @ApiOperation(value = "初始化系统", notes = "根据管理员用户名是否存在判断系统已初始化, 已初始化返回 true, 未初始化返回 false")
- @PostMapping("/install")
- public AjaxJson install(@RequestBody InstallSystemRequest installSystemRequest) {
- if (systemConfigService.getSystemIsInstalled()) {
- throw new InstallSystemException("请勿重复初始化");
- }
-
- installSystemRequest.setPassword(SecureUtil.md5(installSystemRequest.getPassword()));
-
- SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
- BeanUtil.copyProperties(installSystemRequest, systemConfigDTO);
- systemConfigDTO.setInstalled(true);
- systemConfigService.updateSystemConfig(systemConfigDTO);
-
- return AjaxJson.getSuccess();
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/controller/SettingController.java b/src/main/java/im/zhaojun/zfile/module/config/controller/SettingController.java
index cfd0866..c710590 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/controller/SettingController.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/controller/SettingController.java
@@ -1,35 +1,30 @@
package im.zhaojun.zfile.module.config.controller;
import cn.hutool.core.util.StrUtil;
-import cn.hutool.crypto.SecureUtil;
+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.UpdateLinkSettingRequest;
-import im.zhaojun.zfile.module.config.model.request.UpdateSecuritySettingRequest;
-import im.zhaojun.zfile.module.config.model.request.UpdateSiteSettingRequest;
-import im.zhaojun.zfile.module.config.model.request.UpdateUserNameAndPasswordRequest;
-import im.zhaojun.zfile.module.config.model.request.UpdateViewSettingRequest;
+import im.zhaojun.zfile.module.config.model.request.*;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.BeanUtils;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import org.springframework.web.bind.annotation.*;
/**
* 站点设定值接口
*
* @author zhaojun
*/
-@Api(tags = "站点设置模块")
+@Tag(name = "站点设置模块")
@ApiSort(2)
@RestController
@RequestMapping("/admin")
@@ -38,35 +33,31 @@ public class SettingController {
@Resource
private SystemConfigService systemConfigService;
+ @Resource
+ private ZFileProperties zFileProperties;
+
@ApiOperationSupport(order = 1)
- @ApiOperation(value = "获取站点信息",
- notes = "获取站点相关信息,如站点名称,风格样式,是否显示公告,是否显示文档区,自定义 CSS,JS 等参数")
+ @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 = 2)
- @ApiOperation(value = "修改管理员账号密码")
- @PutMapping("/config/password")
- public AjaxJson> updatePwd(@Valid @RequestBody UpdateUserNameAndPasswordRequest settingRequest) {
- SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
- BeanUtils.copyProperties(settingRequest, systemConfigDTO);
- String password = systemConfigDTO.getPassword();
- if (StrUtil.isNotEmpty(password)) {
- systemConfigDTO.setPassword(SecureUtil.md5(password));
- }
- systemConfigService.updateSystemConfig(systemConfigDTO);
- return AjaxJson.getSuccess();
- }
-
-
@ApiOperationSupport(order = 3)
- @ApiOperation(value = "修改站点设置")
+ @Operation(summary = "修改站点设置")
@PutMapping("/config/site")
- public AjaxJson> updateSiteSetting(@Valid @RequestBody UpdateSiteSettingRequest settingRequest) {
+ @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);
@@ -75,9 +66,10 @@ public class SettingController {
@ApiOperationSupport(order = 4)
- @ApiOperation(value = "修改显示设置")
+ @Operation(summary = "修改显示设置")
@PutMapping("/config/view")
- public AjaxJson> updateViewSetting(@Valid @RequestBody UpdateViewSettingRequest settingRequest) {
+ @DemoDisable
+ public AjaxJson updateViewSetting(@Valid @RequestBody UpdateViewSettingRequest settingRequest) {
SystemConfigDTO systemConfigDTO = new SystemConfigDTO();
BeanUtils.copyProperties(settingRequest, systemConfigDTO);
systemConfigService.updateSystemConfig(systemConfigDTO);
@@ -86,24 +78,40 @@ public class SettingController {
@ApiOperationSupport(order = 5)
- @ApiOperation(value = "修改登陆安全设置")
+ @Operation(summary = "修改登陆安全设置")
@PutMapping("/config/security")
- public AjaxJson> updateSecuritySetting(@Valid @RequestBody UpdateSecuritySettingRequest settingRequest) {
+ @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)
- @ApiOperation(value = "修改直链设置")
+ @Operation(summary = "修改直链设置")
@PutMapping("/config/link")
- public AjaxJson> updateLinkSetting(@Valid @RequestBody UpdateLinkSettingRequest settingRequest) {
+ @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();
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/controller/SiteController.java b/src/main/java/im/zhaojun/zfile/module/config/controller/SiteController.java
index c227905..5593a65 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/controller/SiteController.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/controller/SiteController.java
@@ -2,34 +2,34 @@ 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.module.storage.service.StorageSourceService;
-import im.zhaojun.zfile.module.config.service.SystemConfigService;
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.model.request.base.FileListConfigRequest;
-import im.zhaojun.zfile.module.config.model.result.SiteConfigResult;
import im.zhaojun.zfile.module.storage.model.result.StorageSourceConfigResult;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
+import im.zhaojun.zfile.module.storage.service.StorageSourceService;
+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.GetMapping;
-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.ResponseBody;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import javax.validation.Valid;
+import java.util.Objects;
/**
- * 站点基础模块接口
+ * 面向前台的站点基础模块接口
*
* @author zhaojun
*/
-@Api(tags = "站点基础模块")
+@Tag(name = "站点基础模块")
@ApiSort(1)
@Slf4j
@RequestMapping("/api/site")
@@ -46,21 +46,25 @@ public class SiteController {
private SystemConfigService systemConfigService;
@ApiOperationSupport(order = 1)
- @ApiOperation(value = "获取站点全局设置", notes = "获取站点全局设置, 包括是否页面布局、列表尺寸、公告、配置信息")
+ @Operation(summary = "获取站点全局设置", description = "获取站点全局设置, 包括是否页面布局、列表尺寸、公告、配置信息")
@GetMapping("/config/global")
- public AjaxJson globalConfig() {
+ @ProCheck
+ public AjaxJson globalConfig() {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
- SiteConfigResult siteConfigResult = new SiteConfigResult();
- BeanUtils.copyProperties(systemConfig, siteConfigResult);
+ FrontSiteConfigResult frontSiteConfigResult = new FrontSiteConfigResult();
+ BeanUtils.copyProperties(systemConfig, frontSiteConfigResult);
- siteConfigResult.setDebugMode(zFileProperties.isDebug());
- return AjaxJson.getSuccessData(siteConfigResult);
+ 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)
- @ApiOperation(value = "获取存储源设置", notes = "获取某个存储源的设置信息, 包括是否启用, 名称, 存储源类型, 存储源配置信息")
+ @Operation(summary = "获取存储源设置", description = "获取某个存储源的设置信息, 包括是否启用, 名称, 存储源类型, 存储源配置信息")
@PostMapping("/config/storage")
public AjaxJson storageList(@Valid @RequestBody FileListConfigRequest fileListConfigRequest) {
StorageSourceConfigResult storageSourceConfigResult = storageSourceService.getStorageConfigSource(fileListConfigRequest);
@@ -68,13 +72,5 @@ public class SiteController {
}
- @ResponseBody
- @ApiOperationSupport(order = 3)
- @ApiOperation(value = "重置管理员密码", notes = "开启 debug 模式时,访问此接口会强制将管理员账户密码修改为 admin 123456, 并修改登录验证方式为图片验证码, 详见:https://docs.zfile.vip/#/question?id=reset-pwd")
- @GetMapping("/reset-password")
- public AjaxJson> resetPwd() {
- systemConfigService.resetAdminLoginInfo();
- return AjaxJson.getSuccess();
- }
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/event/DirectLinkPrefixModifyHandler.java b/src/main/java/im/zhaojun/zfile/module/config/event/DirectLinkPrefixModifyHandler.java
index 85c565d..a21b163 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/event/DirectLinkPrefixModifyHandler.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/event/DirectLinkPrefixModifyHandler.java
@@ -1,13 +1,17 @@
package im.zhaojun.zfile.module.config.event;
-import im.zhaojun.zfile.core.service.DynamicControllerManager;
+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 javax.annotation.Resource;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
/**
+ * 接收系统设置修改事件, 修改直链前缀时, 动态更新直链前缀.
+ *
* @author zhaojun
*/
@Slf4j
@@ -15,12 +19,20 @@ import javax.annotation.Resource;
public class DirectLinkPrefixModifyHandler implements ISystemConfigModifyHandler {
@Resource
- private DynamicControllerManager dynamicControllerManager;
+ private DynamicDirectLinkPrefixService dynamicDirectLinkPrefixService;
@Override
public void modify(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
- dynamicControllerManager.changeDirectLinkPrefixPath(newSystemConfig.getValue());
- log.info("检测到修改了直链前缀, [{}] -> [{}], 已自动更新直链前缀.", originalSystemConfig.getValue(), newSystemConfig.getValue());
+ 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
diff --git a/src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandler.java b/src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandlerChain.java
similarity index 63%
rename from src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandler.java
rename to src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandlerChain.java
index 9c3d162..319a328 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandler.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/event/SystemConfigModifyHandlerChain.java
@@ -3,27 +3,22 @@ package im.zhaojun.zfile.module.config.event;
import im.zhaojun.zfile.module.config.model.entity.SystemConfig;
import org.springframework.stereotype.Component;
-import javax.annotation.Resource;
+import jakarta.annotation.Resource;
import java.util.List;
/**
* @author zhaojun
*/
@Component
-public class SystemConfigModifyHandler implements ISystemConfigModifyHandler {
+public class SystemConfigModifyHandlerChain {
@Resource
private List handlers;
- public void modify(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
+ public void execute(SystemConfig originalSystemConfig, SystemConfig newSystemConfig) {
handlers.stream()
.filter(handler -> handler.matches(originalSystemConfig.getName()))
.forEach(handler -> handler.modify(originalSystemConfig, newSystemConfig));
}
- @Override
- public boolean matches(String name) {
- return true;
- }
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/dto/LinkExpireDTO.java b/src/main/java/im/zhaojun/zfile/module/config/model/dto/LinkExpireDTO.java
new file mode 100644
index 0000000..fcfbea7
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/dto/LinkExpireDTO.java
@@ -0,0 +1,18 @@
+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;
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/dto/SystemConfigDTO.java b/src/main/java/im/zhaojun/zfile/module/config/model/dto/SystemConfigDTO.java
index 181104c..10ff597 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/dto/SystemConfigDTO.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/dto/SystemConfigDTO.java
@@ -1,170 +1,259 @@
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.login.model.enums.LoginVerifyModeEnum;
import im.zhaojun.zfile.module.link.model.enums.RefererTypeEnum;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+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
-@ApiModel(description = "系统设置类")
-public class SystemConfigDTO {
+@Schema(description = "系统设置类")
+public class SystemConfigDTO implements Serializable {
- @JsonIgnore
- @ApiModelProperty(value = "ID", required = true, example = "1")
- private Integer id;
+ private static final long serialVersionUID = 1L;
- @ApiModelProperty(value = "站点名称", example = "ZFile Site Name")
+ @Schema(name = "站点名称", example = "ZFile Site Name")
private String siteName;
- @ApiModelProperty(value = "用户名", example = "admin")
+ @Schema(name = "用户名", example = "admin")
+ @Deprecated
private String username;
- @ApiModelProperty(value = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
+ @Schema(name = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
private String avatar;
- @ApiModelProperty(value = "备案号", example = "冀ICP备12345678号-1")
+ @Schema(name = "备案号", example = "冀ICP备12345678号-1")
private String icp;
@JsonIgnore
+ @Deprecated
private String password;
- @ApiModelProperty(value = "站点域名", example = "https://zfile.vip")
- private String domain;
-
- @ApiModelProperty(value = "自定义 JS")
+ @Schema(name = "自定义 JS")
private String customJs;
- @ApiModelProperty(value = "自定义 CSS")
+ @Schema(name = "自定义 CSS")
private String customCss;
- @ApiModelProperty(value = "列表尺寸", notes = "large:大,default:中,small:小", example = "default")
+ @Schema(name = "列表尺寸", description ="large:大,default:中,small:小", example = "default")
private String tableSize;
- @ApiModelProperty(value = "是否显示文档区", example = "true")
+ @Schema(name = "是否显示文档区", example = "true")
private Boolean showDocument;
- @ApiModelProperty(value = "网站公告", example = "ZFile 网站公告")
+ @Schema(name = "网站公告", example = "ZFile 网站公告")
private String announcement;
- @ApiModelProperty(value = "是否显示网站公告", example = "true")
+ @Schema(name = "是否显示网站公告", example = "true")
private Boolean showAnnouncement;
- @ApiModelProperty(value = "页面布局", notes = "full:全屏,center:居中", example = "full")
+ @Schema(name = "页面布局", description ="full:全屏,center:居中", example = "full")
private String layout;
- @ApiModelProperty(value = "是否显示生成直链功能(含直链和路径短链)", example = "true")
+ @Schema(name = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
+ private String mobileLayout;
+
+ @Schema(name = "是否显示生成直链功能(含直链和路径短链)", example = "true")
private Boolean showLinkBtn;
- @ApiModelProperty(value = "是否显示生成短链功能", example = "true")
+ @Schema(name = "是否显示生成短链功能", example = "true")
private Boolean showShortLink;
- @ApiModelProperty(value = "是否显示生成路径链接功能", example = "true")
+ @Schema(name = "是否显示生成路径链接功能", example = "true")
private Boolean showPathLink;
- @ApiModelProperty(value = "是否已初始化", example = "true")
+ @Schema(name = "是否已初始化", example = "true")
private Boolean installed;
- @ApiModelProperty(value = "自定义视频文件后缀格式")
+ @Schema(name = "自定义视频文件后缀格式")
private String customVideoSuffix;
- @ApiModelProperty(value = "自定义图像文件后缀格式")
+ @Schema(name = "自定义图像文件后缀格式")
private String customImageSuffix;
- @ApiModelProperty(value = "自定义音频文件后缀格式")
+ @Schema(name = "自定义音频文件后缀格式")
private String customAudioSuffix;
- @ApiModelProperty(value = "自定义文本文件后缀格式")
+ @Schema(name = "自定义文本文件后缀格式")
private String customTextSuffix;
- @ApiModelProperty(value = "直链地址前缀")
+ @Schema(name = "自定义Office后缀格式")
+ private String customOfficeSuffix;
+
+ @Schema(name = "直链地址前缀")
private String directLinkPrefix;
- @ApiModelProperty(value = "直链 Referer 防盗链类型")
+ @Schema(name = "直链 Referer 防盗链类型")
private RefererTypeEnum refererType;
- @ApiModelProperty(value = "是否记录下载日志", example = "true")
+ @Schema(name = "是否记录下载日志", example = "true")
private Boolean recordDownloadLog;
- @ApiModelProperty(value = "直链 Referer 是否允许为空")
+ @Schema(name = "直链 Referer 是否允许为空")
private Boolean refererAllowEmpty;
- @ApiModelProperty(value = "直链 Referer 值")
+ @Schema(name = "直链 Referer 值")
private String refererValue;
- @ApiModelProperty(value = "登陆验证方式,支持验证码和 2FA 认证")
+ /**
+ * 废弃的字段,改为使用 {@link #adminTwoFactorVerify} 和 {@link #loginVerifySecret} 代替
+ */
+ @Schema(name = "管理员登陆验证方式,目前仅支持 2FA 认证或关闭")
+ @Deprecated
private LoginVerifyModeEnum loginVerifyMode;
- @ApiModelProperty(value = "登陆验证 Secret")
+ @Schema(name = "登陆验证 Secret")
private String loginVerifySecret;
- @ApiModelProperty(value = "根目录是否显示所有存储源", notes = "根目录是否显示所有存储源, 如果为 true, 则根目录显示所有存储源列表, 如果为 false, 则会自动跳转到第一个存储源.", example = "true", required = true)
+ @Schema(name = "是否启用登陆验证码", example = "true")
+ private Boolean loginImgVerify;
+
+ @Schema(name = "是否为管理员启用双因素认证", example = "true")
+ private Boolean adminTwoFactorVerify;
+
+ @Schema(name = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean rootShowStorage;
- @ApiModelProperty(value = "前端域名", notes = "前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
+ @Schema(name = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
private String frontDomain;
- @ApiModelProperty(value = "是否在前台显示登陆按钮", example = "true")
+ @Schema(name = "是否在前台显示登陆按钮", example = "true")
private Boolean showLogin;
- @ApiModelProperty(value = "RAS Hex Key", example = "r2HKbzc1DfvOs5uHhLn7pA==")
+ @Schema(name = "登录日志模式", example = "all")
+ private LoginLogModeEnum loginLogMode;
+
+ @Schema(name = "RAS Hex Key", example = "r2HKbzc1DfvOs5uHhLn7pA==")
private String rsaHexKey;
- @ApiModelProperty(value = "默认文件点击习惯", example = "click")
+ @Schema(name = "默认文件点击习惯", example = "click")
private FileClickModeEnum fileClickMode;
- @ApiModelProperty(value = "最大同时上传文件数", example = "5")
+ @Schema(name = "授权码", example = "e619510f-cdcd-f657-6c5e-2d12e9a28ae5")
+ private String authCode;
+
+ @Schema(name = "最大同时上传文件数", example = "5")
private Integer maxFileUploads;
- @ApiModelProperty(value = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
+ @Schema(name = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
private String onlyOfficeUrl;
- @ApiModelProperty(value = "是否允许路径直链可直接访问", example = "true", required = true)
+ @Schema(name = "onlyOffice Secret", example = "X9rBGypwWE86Lca8e4Mo55iHFoiyh9ed")
+ private String onlyOfficeSecret;
+
+ @Schema(name = "启用 WebDAV", example = "true")
+ private Boolean webdavEnable;
+
+ @Schema(name = "WebDAV 服务器中转下载", example = "true")
+ private Boolean webdavProxy;
+
+ @Schema(name = "WebDAV 匿名用户访问", example = "true")
+ private Boolean webdavAllowAnonymous;
+
+ @Schema(name = "WebDAV 账号", example = "admin")
+ private String webdavUsername;
+
+ @Schema(name = "WebDAV 密码", example = "123456")
+ private String webdavPassword;
+
+ @Schema(name = "是否允许路径直链可直接访问", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean allowPathLinkAnonAccess;
- @ApiModelProperty(value = "默认最大显示文件数", example = "1000")
+ @Schema(name = "默认最大显示文件数", example = "1000")
private Integer maxShowSize;
- @ApiModelProperty(value = "每次加载更多文件数", example = "50")
+ @Schema(name = "每次加载更多文件数", example = "50")
private Integer loadMoreSize;
- @ApiModelProperty(value = "默认排序字段", example = "name")
+ @Schema(name = "默认排序字段", example = "name")
private String defaultSortField;
- @ApiModelProperty(value = "默认排序方向", example = "asc")
+ @Schema(name = "默认排序方向", example = "asc")
private String defaultSortOrder;
- @ApiModelProperty(value = "站点 Home 名称", example = "xxx 的小站")
+ @Schema(name = "站点 Home 名称", example = "xxx 的小站")
private String siteHomeName;
- @ApiModelProperty(value = "站点 Home Logo", example = "true")
+ @Schema(name = "站点 Home Logo", example = "true")
private String siteHomeLogo;
- @ApiModelProperty(value = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
+ @Schema(name = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
private String siteHomeLogoLink;
- @ApiModelProperty(value = "站点 Logo 链接打开方式", example = "_blank")
+ @Schema(name = "站点 Logo 链接打开方式", example = "_blank")
private String siteHomeLogoTargetMode;
- @ApiModelProperty(value = "限制直链下载秒数", example = "_blank")
+ @Schema(name = "管理员页面点击 Logo 回到首页打开方式", example = "_blank")
+ private String siteAdminLogoTargetMode;
+
+ @Schema(name = "管理员页面点击版本号打开更新日志", example = "true")
+ private Boolean siteAdminVersionOpenChangeLog;
+
+ @Schema(name = "限制直链下载秒数", example = "_blank")
private Integer linkLimitSecond;
- @ApiModelProperty(value = "限制直链下载次数", example = "_blank")
+ @Schema(name = "限制直链下载次数", example = "_blank")
private Integer linkDownloadLimit;
- @ApiModelProperty(value = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
+ @Schema(name = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
private String faviconUrl;
- @ApiModelProperty(value = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
- private String linkExpireTimes;
+ @Schema(name = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
+ @JSONStringParse
+ private List linkExpireTimes;
- @ApiModelProperty(value = "是否默认记住密码", example = "true")
+ @Schema(name = "是否默认记住密码", example = "true")
private Boolean defaultSavePwd;
+
+ /**
+ * 废弃的字段,不再使用悬浮菜单
+ */
+ @Deprecated
+ @Schema(name = "是否启用 hover 菜单", example = "true")
+ private Boolean enableHoverMenu;
+
+ @Schema(name = "访问 ip 黑名单", example = "162.13.1.0/24\n192.168.1.1")
+ private String accessIpBlocklist;
+
+ @Schema(name = "访问 ua 黑名单", example = "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36*")
+ private String accessUaBlocklist;
+
+ @Schema(name = "匿名用户首页显示内容")
+ 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;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/entity/SystemConfig.java b/src/main/java/im/zhaojun/zfile/module/config/model/entity/SystemConfig.java
index 933d185..406cbd5 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/entity/SystemConfig.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/entity/SystemConfig.java
@@ -4,8 +4,7 @@ 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.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@@ -16,7 +15,7 @@ import java.io.Serializable;
* @author zhaojun
*/
@Data
-@ApiModel(description = "系统设置")
+@Schema(description = "系统设置")
@TableName(value = "system_config")
public class SystemConfig implements Serializable {
@@ -25,22 +24,22 @@ public class SystemConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
- @ApiModelProperty(value = "ID, 新增无需填写", example = "1")
+ @Schema(name = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "name")
- @ApiModelProperty(value = "系统设置名称", example = "siteName")
+ @Schema(name = "系统设置名称", example = "siteName")
private String name;
@TableField(value = "`value`")
- @ApiModelProperty(value = "系统设置值", example = "ZFile 演示站")
+ @Schema(name = "系统设置值", example = "ZFile 演示站")
private String value;
@TableField(value = "title")
- @ApiModelProperty(value = "系统设置描述", example = "站点名称")
+ @Schema(name = "系统设置描述", example = "站点名称")
private String title;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/InstallSystemRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/InstallSystemRequest.java
deleted file mode 100644
index 0257215..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/InstallSystemRequest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package im.zhaojun.zfile.module.config.model.request;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 系统初始化请求参数
- *
- * @author zhaojun
- */
-@Data
-@ApiModel(description = "系统初始化请求类")
-public class InstallSystemRequest {
-
- @ApiModelProperty(value = "站点名称", example = "ZFile Site Name")
- private String siteName;
-
- @ApiModelProperty(value = "用户名", example = "admin")
- private String username;
-
- @ApiModelProperty(value = "密码", example = "123456")
- private String password;
-
- @ApiModelProperty(value = "站点域名", example = "https://zfile.vip")
- private String domain;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/TestAntPathMatcherRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/TestAntPathMatcherRequest.java
deleted file mode 100644
index 0715123..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/TestAntPathMatcherRequest.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package im.zhaojun.zfile.module.config.model.request;
-
-import lombok.Data;
-
-@Data
-public class TestAntPathMatcherRequest {
-
- private String antPath;
-
- private String testPath;
-
-}
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateAccessSettingRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateAccessSettingRequest.java
new file mode 100644
index 0000000..d554906
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateAccessSettingRequest.java
@@ -0,0 +1,21 @@
+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(name = "访问 ip 黑名单", example = "162.13.1.0/24\n192.168.1.1")
+ private String accessIpBlocklist;
+
+ @Schema(name = "访问 ua 黑名单", example = "Mozilla/5.0 (Linux; Android) AppleWebKit/537.36*")
+ private String accessUaBlocklist;
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateLinkSettingRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateLinkSettingRequest.java
index 823b665..c23f60c 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateLinkSettingRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateLinkSettingRequest.java
@@ -1,53 +1,57 @@
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.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
import lombok.Data;
+import java.util.List;
+
/**
* 直链设置请求参数类
*
* @author zhaojun
*/
@Data
-@ApiModel(description = "直链设置请求参数类")
+@Schema(description = "直链设置请求参数类")
public class UpdateLinkSettingRequest {
- @ApiModelProperty(value = "是否记录下载日志", example = "true")
+ @Schema(name = "是否记录下载日志", example = "true")
private Boolean recordDownloadLog;
- @ApiModelProperty(value = "直链 Referer 防盗链类型")
+ @Schema(name = "直链 Referer 防盗链类型")
private RefererTypeEnum refererType;
- @ApiModelProperty(value = "直链 Referer 是否允许为空")
+ @Schema(name = "直链 Referer 是否允许为空")
private Boolean refererAllowEmpty;
- @ApiModelProperty(value = "直链 Referer 值")
+ @Schema(name = "直链 Referer 值")
private String refererValue;
- @ApiModelProperty(value = "直链地址前缀")
+ @Schema(name = "直链地址前缀")
+ @NotBlank(message = "直链地址前缀不能为空")
private String directLinkPrefix;
- @ApiModelProperty(value = "是否显示生成直链功能(含直链和路径短链)", example = "true", required = true)
+ @Schema(name = "是否显示生成直链功能(含直链和路径短链)", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showLinkBtn;
- @ApiModelProperty(value = "是否显示生成短链功能", example = "true", required = true)
+ @Schema(name = "是否显示生成短链功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showShortLink;
- @ApiModelProperty(value = "是否显示生成路径链接功能", example = "true", required = true)
+ @Schema(name = "是否显示生成路径链接功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showPathLink;
- @ApiModelProperty(value = "是否允许路径直链可直接访问", example = "true", required = true)
+ @Schema(name = "是否允许路径直链可直接访问", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean allowPathLinkAnonAccess;
- @ApiModelProperty(value = "限制直链下载秒数", example = "_blank")
+ @Schema(name = "限制直链下载秒数", example = "_blank")
private Integer linkLimitSecond;
- @ApiModelProperty(value = "限制直链下载次数", example = "_blank")
+ @Schema(name = "限制直链下载次数", example = "_blank")
private Integer linkDownloadLimit;
- @ApiModelProperty(value = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
- private String linkExpireTimes;
+ @Schema(name = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
+ private List linkExpireTimes;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSecuritySettingRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSecuritySettingRequest.java
index 9caf9c5..f8d0690 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSecuritySettingRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSecuritySettingRequest.java
@@ -1,8 +1,7 @@
package im.zhaojun.zfile.module.config.model.request;
-import im.zhaojun.zfile.module.login.model.enums.LoginVerifyModeEnum;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import im.zhaojun.zfile.module.user.model.enums.LoginLogModeEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
@@ -11,16 +10,25 @@ import lombok.Data;
* @author zhaojun
*/
@Data
-@ApiModel(description = "登陆安全设置请求参数类")
+@Schema(description = "登陆安全设置请求参数类")
public class UpdateSecuritySettingRequest {
- @ApiModelProperty(value = "是否在前台显示登陆按钮", example = "true")
+ @Schema(name = "是否在前台显示登陆按钮", example = "true")
private Boolean showLogin;
- @ApiModelProperty(value = "登陆验证方式,支持验证码和 2FA 认证")
- private LoginVerifyModeEnum loginVerifyMode;
+ @Schema(name = "登录日志模式", example = "all")
+ private LoginLogModeEnum loginLogMode;
- @ApiModelProperty(value = "登陆验证 Secret")
+ @Schema(name = "是否启用登陆验证码", example = "true")
+ private Boolean loginImgVerify;
+
+ @Schema(name = "是否为管理员启用双因素认证", example = "true")
+ private Boolean adminTwoFactorVerify;
+
+ @Schema(name = "2FA登陆验证 Secret")
private String loginVerifySecret;
+ @Schema(name = "匿名用户首页显示内容")
+ private String guestIndexHtml;
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSiteSettingRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSiteSettingRequest.java
index edf8c42..b85829c 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSiteSettingRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateSiteSettingRequest.java
@@ -1,10 +1,9 @@
package im.zhaojun.zfile.module.config.model.request;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
/**
* 站点设置请求参数类
@@ -12,42 +11,47 @@ import javax.validation.constraints.NotBlank;
* @author zhaojun
*/
@Data
-@ApiModel(description = "站点设置请求参数类")
+@Schema(description = "站点设置请求参数类")
public class UpdateSiteSettingRequest {
- @ApiModelProperty(value = "站点名称", required = true, example = "ZFile Site Name")
+ @Schema(name = "站点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ZFile Site Name")
@NotBlank(message = "站点名称不能为空")
private String siteName;
- @ApiModelProperty(value = "站点域名", required = true, example = "https://zfile.vip")
- @NotBlank(message = "站点域名不能为空")
- private String domain;
-
- @ApiModelProperty(value = "前端域名", notes = "前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
+ @Schema(name = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
private String frontDomain;
- @ApiModelProperty(value = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
+ @Schema(name = "头像地址", example = "https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png")
private String avatar;
- @ApiModelProperty(value = "备案号", example = "冀ICP备12345678号-1")
+ @Schema(name = "备案号", example = "冀ICP备12345678号-1")
private String icp;
- @ApiModelProperty(value = "最大同时上传文件数", example = "5")
+ @Schema(name = "授权码", example = "e619510f-cdcd-f657-6c5e-2d12e9a28ae5")
+ private String authCode;
+
+ @Schema(name = "最大同时上传文件数", example = "5")
private Integer maxFileUploads;
- @ApiModelProperty(value = "站点 Home 名称", example = "xxx 的小站")
+ @Schema(name = "站点 Home 名称", example = "xxx 的小站")
private String siteHomeName;
- @ApiModelProperty(value = "站点 Home Logo", example = "true")
+ @Schema(name = "站点 Home Logo", example = "true")
private String siteHomeLogo;
- @ApiModelProperty(value = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
+ @Schema(name = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
private String siteHomeLogoLink;
- @ApiModelProperty(value = "站点 Logo 链接打开方式", example = "_blank")
+ @Schema(name = "站点 Logo 链接打开方式", example = "_blank")
private String siteHomeLogoTargetMode;
- @ApiModelProperty(value = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
+ @Schema(name = "网站 favicon 图标地址", example = "https://www.example.com/favicon.ico")
private String faviconUrl;
+ @Schema(name = "管理员页面点击 Logo 回到首页打开方式", example = "_blank")
+ private String siteAdminLogoTargetMode;
+
+ @Schema(name = "管理员页面点击版本号打开更新日志", example = "true")
+ private Boolean siteAdminVersionOpenChangeLog;
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateUserNameAndPasswordRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateUserNameAndPasswordRequest.java
index 78bcf9c..47a7091 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateUserNameAndPasswordRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateUserNameAndPasswordRequest.java
@@ -1 +1,25 @@
-package im.zhaojun.zfile.module.config.model.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 用户修改密码请求参数类
*
* @author zhaojun
*/
@Data
@ApiModel(description = "用户修改密码请求参数类")
public class UpdateUserNameAndPasswordRequest {
@ApiModelProperty(value = "用户名", required = true, example = "admin")
@NotBlank(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true, example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
}
\ No newline at end of file
+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(name = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
+ @NotBlank(message = "用户名不能为空")
+ private String username;
+
+ @Schema(name = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
+ @NotBlank(message = "密码不能为空")
+ private String password;
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateViewSettingRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateViewSettingRequest.java
index 537461a..3a9f3b3 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateViewSettingRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateViewSettingRequest.java
@@ -1,8 +1,7 @@
package im.zhaojun.zfile.module.config.model.request;
import im.zhaojun.zfile.module.config.model.enums.FileClickModeEnum;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
@@ -11,64 +10,80 @@ import lombok.Data;
* @author zhaojun
*/
@Data
-@ApiModel(description = "显示设置请求参数类")
+@Schema(description = "显示设置请求参数类")
public class UpdateViewSettingRequest {
- @ApiModelProperty(value = "根目录是否显示所有存储源", notes = "根目录是否显示所有存储源, 如果为 true, 则根目录显示所有存储源列表, 如果为 false, 则会自动跳转到第一个存储源.", example = "true", required = true)
+ @Schema(name = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean rootShowStorage;
- @ApiModelProperty(value = "页面布局", notes = "full:全屏,center:居中", example = "full", required = true)
+ @Schema(name = "页面布局", description ="full:全屏,center:居中", example = "full", requiredMode = Schema.RequiredMode.REQUIRED)
private String layout;
- @ApiModelProperty(value = "列表尺寸", notes = "large:大,default:中,small:小", example = "default", required = true)
+ @Schema(name = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
+ private String mobileLayout;
+
+ @Schema(name = "列表尺寸", description ="large:大,default:中,small:小", example = "default", requiredMode = Schema.RequiredMode.REQUIRED)
private String tableSize;
- @ApiModelProperty(value = "自定义视频文件后缀格式")
+ @Schema(name = "自定义视频文件后缀格式")
private String customVideoSuffix;
- @ApiModelProperty(value = "自定义图像文件后缀格式")
+ @Schema(name = "自定义图像文件后缀格式")
private String customImageSuffix;
- @ApiModelProperty(value = "自定义音频文件后缀格式")
+ @Schema(name = "自定义音频文件后缀格式")
private String customAudioSuffix;
- @ApiModelProperty(value = "自定义文本文件后缀格式")
+ @Schema(name = "自定义文本文件后缀格式")
private String customTextSuffix;
- @ApiModelProperty(value = "是否显示文档区", example = "true", required = true)
+ @Schema(name = "自定义Office后缀格式")
+ private String customOfficeSuffix;
+
+ @Schema(name = "是否显示文档区", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showDocument;
- @ApiModelProperty(value = "是否显示网站公告", example = "true", required = true)
+ @Schema(name = "是否显示网站公告", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean showAnnouncement;
- @ApiModelProperty(value = "网站公告", example = "ZFile 网站公告")
+ @Schema(name = "网站公告", example = "ZFile 网站公告")
private String announcement;
- @ApiModelProperty(value = "自定义 CSS")
+ @Schema(name = "自定义 CSS")
private String customCss;
- @ApiModelProperty(value = "自定义 JS")
+ @Schema(name = "自定义 JS")
private String customJs;
- @ApiModelProperty(value = "默认文件点击习惯", example = "click")
+ @Schema(name = "默认文件点击习惯", example = "click")
private FileClickModeEnum fileClickMode;
- @ApiModelProperty(value = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
+ @Schema(name = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
private String onlyOfficeUrl;
- @ApiModelProperty(value = "默认最大显示文件数", example = "1000")
+ @Schema(name = "onlyOffice Secret", example = "X9rBGypwWE86Lca8e4Mo55iHFoiyh9ed")
+ private String onlyOfficeSecret;
+
+ @Schema(name = "默认最大显示文件数", example = "1000")
private Integer maxShowSize;
- @ApiModelProperty(value = "每次加载更多文件数", example = "50")
+ @Schema(name = "每次加载更多文件数", example = "50")
private Integer loadMoreSize;
- @ApiModelProperty(value = "默认排序字段", example = "name")
+ @Schema(name = "默认排序字段", example = "name")
private String defaultSortField;
- @ApiModelProperty(value = "默认排序方向", example = "asc")
+ @Schema(name = "默认排序方向", example = "asc")
private String defaultSortOrder;
- @ApiModelProperty(value = "是否默认记住密码", example = "true")
+ @Schema(name = "是否默认记住密码", example = "true")
private Boolean defaultSavePwd;
+ /**
+ * 废弃的字段,不再使用悬浮菜单
+ */
+ @Deprecated
+ @Schema(name = "是否启用 hover 菜单", example = "true")
+ private Boolean enableHoverMenu;
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateWebDAVRequest.java b/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateWebDAVRequest.java
deleted file mode 100644
index fef9526..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/model/request/UpdateWebDAVRequest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package im.zhaojun.zfile.module.config.model.request;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * @author zhaojun
- */
-@Data
-@ApiModel(description = "WebDAV 设置请求参数类")
-public class UpdateWebDAVRequest {
-
- @ApiModelProperty(value = "启用 WebDAV", example = "true")
- private Boolean webdavEnable;
-
- @ApiModelProperty(value = "WebDAV 服务器中转下载", example = "true")
- private Boolean webdavProxy;
-
- @ApiModelProperty(value = "WebDAV 账号", example = "admin")
- private String webdavUsername;
-
- @ApiModelProperty(value = "WebDAV 密码", example = "123456")
- private String webdavPassword;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/result/FrontSiteConfigResult.java b/src/main/java/im/zhaojun/zfile/module/config/model/result/FrontSiteConfigResult.java
new file mode 100644
index 0000000..3dfac33
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/config/model/result/FrontSiteConfigResult.java
@@ -0,0 +1,144 @@
+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(name="全局站点设置响应类")
+public class FrontSiteConfigResult {
+
+ @Schema(name = "是否已初始化", example = "true")
+ private Boolean installed;
+
+ @Schema(name = "Debug 模式", example = "true", description ="开启 debug 模式后,可重置管理员密码")
+ private Boolean debugMode;
+
+ @Schema(name = "直链地址前缀", example = "true", description ="直链地址前缀, 如 http(s)://ip:port/${直链前缀}/path/filename")
+ private String directLinkPrefix;
+
+ @Schema(name = "站点名称", example = "ZFile Site Name")
+ private String siteName;
+
+ @Schema(name = "备案号", example = "冀ICP备12345678号-1")
+ private String icp;
+
+ @Schema(name = "页面布局", description ="full:全屏,center:居中", example = "full", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String layout;
+
+ @Schema(name = "移动端页面布局", description ="full:全屏,center:居中", example = "full")
+ private String mobileLayout;
+
+ @Schema(name = "列表尺寸", description ="large:大,default:中,small:小", example = "default", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String tableSize;
+
+ @Schema(name = "是否显示生成直链功能(含直链和路径短链)", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean showLinkBtn;
+
+ @Schema(name = "是否显示生成短链功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean showShortLink;
+
+ @Schema(name = "是否显示生成路径链接功能", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean showPathLink;
+
+ @Schema(name = "是否显示文档区", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean showDocument;
+
+ @Schema(name = "是否显示网站公告", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean showAnnouncement;
+
+ @Schema(name = "网站公告", example = "ZFile 网站公告")
+ private String announcement;
+
+ @Schema(name = "自定义 JS")
+ private String customJs;
+
+ @Schema(name = "自定义 CSS")
+ private String customCss;
+
+ @Schema(name = "自定义视频文件后缀格式")
+ private String customVideoSuffix;
+
+ @Schema(name = "自定义图像文件后缀格式")
+ private String customImageSuffix;
+
+ @Schema(name = "自定义音频文件后缀格式")
+ private String customAudioSuffix;
+
+ @Schema(name = "自定义文本文件后缀格式")
+ private String customTextSuffix;
+
+ @Schema(name = "自定义Office后缀格式")
+ private String customOfficeSuffix;
+
+ @Schema(name = "根目录是否显示所有存储源", description ="勾选则根目录显示所有存储源列表, 反之会自动显示第一个存储源的内容.", example = "true", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Boolean rootShowStorage;
+
+ @Schema(name = "前端域名", description ="前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
+ private String frontDomain;
+
+ @Schema(name = "是否在前台显示登陆按钮", example = "true")
+ private Boolean showLogin;
+
+ @Schema(name = "登录日志模式", example = "all")
+ private LoginLogModeEnum loginLogMode;
+
+ @Schema(name = "默认文件点击习惯", example = "click")
+ private FileClickModeEnum fileClickMode;
+
+ @Schema(name = "最大同时上传文件数", example = "5")
+ private Integer maxFileUploads;
+
+ @Schema(name = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
+ private String onlyOfficeUrl;
+
+ @Schema(name = "默认最大显示文件数", example = "1000")
+ private Integer maxShowSize;
+
+ @Schema(name = "每次加载更多文件数", example = "50")
+ private Integer loadMoreSize;
+
+ @Schema(name = "默认排序字段", example = "name")
+ private String defaultSortField;
+
+ @Schema(name = "默认排序方向", example = "asc")
+ private String defaultSortOrder;
+
+ @Schema(name = "站点 Home 名称", example = "xxx 的小站")
+ private String siteHomeName;
+
+ @Schema(name = "站点 Home Logo", example = "true")
+ private String siteHomeLogo;
+
+ @Schema(name = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
+ private String siteHomeLogoLink;
+
+ @Schema(name = "站点 Logo 链接打开方式", example = "_blank")
+ private String siteHomeLogoTargetMode;
+
+ @Schema(name = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
+ private List linkExpireTimes;;
+
+ @Schema(name = "是否默认记住密码", example = "true")
+ private Boolean defaultSavePwd;
+
+ /**
+ * 废弃的字段,不再使用悬浮菜单
+ */
+ @Deprecated
+ @Schema(name = "是否启用 hover 菜单", example = "true")
+ private Boolean enableHoverMenu;
+
+ @Schema(name = "是否是游客", example = "true")
+ private boolean guest;
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/model/result/SiteConfigResult.java b/src/main/java/im/zhaojun/zfile/module/config/model/result/SiteConfigResult.java
deleted file mode 100644
index 3804df5..0000000
--- a/src/main/java/im/zhaojun/zfile/module/config/model/result/SiteConfigResult.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package im.zhaojun.zfile.module.config.model.result;
-
-import im.zhaojun.zfile.module.config.model.enums.FileClickModeEnum;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 全局站点设置响应类
- *
- * @author zhaojun
- */
-@Data
-@ApiModel(value="全局站点设置响应类")
-public class SiteConfigResult {
-
- @ApiModelProperty(value = "是否已初始化", example = "true")
- private Boolean installed;
-
- @ApiModelProperty(value = "Debug 模式", example = "true", notes = "开启 debug 模式后,可重置管理员密码")
- private Boolean debugMode;
-
- @ApiModelProperty(value = "直链地址前缀", example = "true", notes = "直链地址前缀, 如 http(s)://ip:port/${直链前缀}/path/filename")
- private String directLinkPrefix;
-
- @ApiModelProperty(value = "站点名称", example = "ZFile Site Name")
- private String siteName;
-
- @ApiModelProperty(value = "备案号", example = "冀ICP备12345678号-1")
- private String icp;
-
- @ApiModelProperty(value = "站点域名(后端)", example = "https://zfile.vip", notes = "该值需配置为后端的站点域名,生成直链等操作需要此参数.")
- private String domain;
-
- @ApiModelProperty(value = "页面布局", notes = "full:全屏,center:居中", example = "full", required = true)
- private String layout;
-
- @ApiModelProperty(value = "列表尺寸", notes = "large:大,default:中,small:小", example = "default", required = true)
- private String tableSize;
-
- @ApiModelProperty(value = "是否显示生成直链功能(含直链和路径短链)", example = "true", required = true)
- private Boolean showLinkBtn;
-
- @ApiModelProperty(value = "是否显示生成短链功能", example = "true", required = true)
- private Boolean showShortLink;
-
- @ApiModelProperty(value = "是否显示生成路径链接功能", example = "true", required = true)
- private Boolean showPathLink;
-
- @ApiModelProperty(value = "是否显示文档区", example = "true", required = true)
- private Boolean showDocument;
-
- @ApiModelProperty(value = "是否显示网站公告", example = "true", required = true)
- private Boolean showAnnouncement;
-
- @ApiModelProperty(value = "网站公告", example = "ZFile 网站公告")
- private String announcement;
-
- @ApiModelProperty(value = "自定义 JS")
- private String customJs;
-
- @ApiModelProperty(value = "自定义 CSS")
- private String customCss;
-
- @ApiModelProperty(value = "自定义视频文件后缀格式")
- private String customVideoSuffix;
-
- @ApiModelProperty(value = "自定义图像文件后缀格式")
- private String customImageSuffix;
-
- @ApiModelProperty(value = "自定义音频文件后缀格式")
- private String customAudioSuffix;
-
- @ApiModelProperty(value = "自定义文本文件后缀格式")
- private String customTextSuffix;
-
- @ApiModelProperty(value = "根目录是否显示所有存储源", notes = "根目录是否显示所有存储源, 如果为 true, 则根目录显示所有存储源列表, 如果为 false, 则会自动跳转到第一个存储源.", example = "true", required = true)
- private Boolean rootShowStorage;
-
- @ApiModelProperty(value = "前端域名", notes = "前端域名,前后端分离情况下需要配置.", example = "http://xxx.example.com")
- private String frontDomain;
-
- @ApiModelProperty(value = "是否在前台显示登陆按钮", example = "true")
- private Boolean showLogin;
-
- @ApiModelProperty(value = "默认文件点击习惯", example = "click")
- private FileClickModeEnum fileClickMode;
-
- @ApiModelProperty(value = "最大同时上传文件数", example = "5")
- private Integer maxFileUploads;
-
- @ApiModelProperty(value = "onlyOffice 在线预览地址", example = "http://office.zfile.vip")
- private String onlyOfficeUrl;
-
- @ApiModelProperty(value = "默认最大显示文件数", example = "1000")
- private Integer maxShowSize;
-
- @ApiModelProperty(value = "每次加载更多文件数", example = "50")
- private Integer loadMoreSize;
-
- @ApiModelProperty(value = "默认排序字段", example = "name")
- private String defaultSortField;
-
- @ApiModelProperty(value = "默认排序方向", example = "asc")
- private String defaultSortOrder;
-
- @ApiModelProperty(value = "站点 Home 名称", example = "xxx 的小站")
- private String siteHomeName;
-
- @ApiModelProperty(value = "站点 Home Logo", example = "true")
- private String siteHomeLogo;
-
- @ApiModelProperty(value = "站点 Logo 点击后链接", example = "https://www.zfile.vip")
- private String siteHomeLogoLink;
-
- @ApiModelProperty(value = "站点 Logo 链接打开方式", example = "_blank")
- private String siteHomeLogoTargetMode;
-
- @ApiModelProperty(value = "短链过期时间设置", example = "[{value: 1, unit: \"day\"}, {value: 1, unit: \"week\"}, {value: 1, unit: \"month\"}, {value: 1, unit: \"year\"}]")
- private String linkExpireTimes;
-
- @ApiModelProperty(value = "是否默认记住密码", example = "true")
- private Boolean defaultSavePwd;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/config/service/SystemConfigService.java b/src/main/java/im/zhaojun/zfile/module/config/service/SystemConfigService.java
index 088072b..3c90063 100644
--- a/src/main/java/im/zhaojun/zfile/module/config/service/SystemConfigService.java
+++ b/src/main/java/im/zhaojun/zfile/module/config/service/SystemConfigService.java
@@ -1,25 +1,26 @@
package im.zhaojun.zfile.module.config.service;
-import cn.hutool.core.collection.CollUtil;
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.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
-import im.zhaojun.zfile.core.config.ZFileProperties;
-import im.zhaojun.zfile.core.exception.ServiceException;
-import im.zhaojun.zfile.core.util.CodeMsg;
-import im.zhaojun.zfile.core.util.EnumConvertUtils;
+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.ISystemConfigModifyHandler;
+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.login.model.enums.LoginVerifyModeEnum;
+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;
@@ -28,15 +29,12 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import javax.annotation.Resource;
import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
import static im.zhaojun.zfile.module.config.service.SystemConfigService.CACHE_NAME;
+
/**
* 系统设置 Service
*
@@ -46,33 +44,24 @@ import static im.zhaojun.zfile.module.config.service.SystemConfigService.CACHE_N
@Service
@CacheConfig(cacheNames = CACHE_NAME)
public class SystemConfigService {
-
+
public static final String CACHE_NAME = "systemConfig";
-
- private static final String DEFAULT_USERNAME = "admin";
- private static final String DEFAULT_PASSWORD = "123456";
-
- private static final LoginVerifyModeEnum DEFAULT_LOGIN_VERIFY_MODE = LoginVerifyModeEnum.IMG_VERIFY_MODE;
+ private static final String SERIAL_VERSION_UID_FIELD_NAME = "serialVersionUID";
@Resource
private SystemConfigMapper systemConfigMapper;
-
- @Resource
- private SystemConfigService systemConfigService;
-
- @Resource
- private ZFileProperties zFileProperties;
-
+
@Resource
private CacheManager cacheManager;
@Resource
- private ISystemConfigModifyHandler systemConfigModifyHandler;
-
+ private SystemConfigModifyHandlerChain systemConfigModifyHandlerChain;
+
private final Class systemConfigClazz = SystemConfigDTO.class;
-
+ public static final List ignoreFieldList = Arrays.asList("domain");
+
/**
* 获取系统设置, 如果缓存中有, 则去缓存取, 没有则查询数据库并写入到缓存中.
*
@@ -85,7 +74,10 @@ public class SystemConfigService {
for (SystemConfig systemConfig : systemConfigList) {
String key = systemConfig.getName();
-
+ if (ignoreFieldList.contains(key)) {
+ log.debug("从数据库加载字段填充到 DTO 时,忽略字段: {}", key);
+ continue;
+ }
try {
Field field = systemConfigClazz.getDeclaredField(key);
field.setAccessible(true);
@@ -95,6 +87,15 @@ public class SystemConfigService {
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);
}
@@ -116,10 +117,10 @@ public class SystemConfigService {
*/
@Transactional(rollbackFor = Exception.class)
@CacheEvict(allEntries = true)
- public void updateSystemConfig(SystemConfigDTO systemConfigDTO) {
+ public synchronized void updateSystemConfig(SystemConfigDTO systemConfigDTO) {
// 获取更新前的值
List systemConfigListInDb = systemConfigMapper.findAll();
- Map systemConfigMapInDb = CollUtil.toMap(systemConfigListInDb, null, SystemConfig::getName);
+ Map systemConfigMapInDb = CollectionUtils.toMap(systemConfigListInDb, null, SystemConfig::getName);
// 存储更新后的值
List updateSystemConfigList = new ArrayList<>();
@@ -128,6 +129,9 @@ public class SystemConfigService {
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);
@@ -143,6 +147,8 @@ public class SystemConfigService {
// 如果是枚举类型, 则取 value 值.
if (EnumUtil.isEnum(val)) {
val = EnumConvertUtils.convertEnumToStr(val);
+ } else if (field.isAnnotationPresent(JSONStringParse.class)) {
+ val = JSONObject.toJSONString(val);
}
// 如果和原来的值一样, 则跳过
String originVal = systemConfig.getValue();
@@ -157,111 +163,111 @@ public class SystemConfigService {
updateSystemConfig.setTitle(systemConfig.getTitle());
updateSystemConfigList.add(updateSystemConfig);
}
+ } else {
+ log.warn("尝试保存系统配置表中不存在字段: {}", key);
}
}
updateSystemConfigList.forEach(systemConfigInForm -> {
SystemConfig systemConfigInDb = systemConfigMapInDb.get(systemConfigInForm.getName());
- systemConfigModifyHandler.modify(systemConfigInDb, systemConfigInForm);
+ systemConfigModifyHandlerChain.execute(systemConfigInDb, systemConfigInForm);
systemConfigMapper.updateById(systemConfigInForm);
});
}
+
/**
- * 重置管理员登录信息, 重置登录账号为 admin, 密码为 123456, 登录校验方式为 图形验证码.
- */
- @Transactional(rollbackFor = Exception.class)
- @CacheEvict(allEntries = true)
- public void resetAdminLoginInfo() {
- if (!zFileProperties.isDebug()) {
- log.warn("当前为非调试模式, 无法重置管理员登录信息");
- throw new ServiceException(CodeMsg.BAD_REQUEST);
- }
-
- SystemConfig usernameConfig = systemConfigMapper.findByName(SystemConfigConstant.USERNAME);
- usernameConfig.setValue(DEFAULT_USERNAME);
- systemConfigMapper.updateById(usernameConfig);
-
- String encryptionPassword = SecureUtil.md5(DEFAULT_PASSWORD);
- SystemConfig passwordConfig = systemConfigMapper.findByName(SystemConfigConstant.PASSWORD);
- passwordConfig.setValue(encryptionPassword);
- systemConfigMapper.updateById(passwordConfig);
-
- SystemConfig loginVerifyModeConfig = systemConfigMapper.findByName(SystemConfigConstant.LOGIN_VERIFY_MODE);
- loginVerifyModeConfig.setValue(DEFAULT_LOGIN_VERIFY_MODE.getValue());
- systemConfigMapper.updateById(loginVerifyModeConfig);
-
- log.info("重置管理员登录信息成功, 账号: {}, 密码: {}, 登录校验方式: {}", DEFAULT_USERNAME, DEFAULT_PASSWORD, DEFAULT_LOGIN_VERIFY_MODE);
- }
-
-
- /**
- * 获取 RSA Hex 格式密钥
+ * 获取 AES Hex 格式密钥
*
- * @return RSA Hex 格式密钥
+ * @return AES Hex 格式密钥
*/
- public synchronized String getRsaHexKeyOrGenerate() {
- SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
- String rsaHexKey = systemConfigDTO.getRsaHexKey();
- if (StrUtil.isEmpty(rsaHexKey)) {
+ 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();
- rsaHexKey = HexUtil.encodeHexStr(key);
-
- SystemConfig loginVerifyModeConfig = systemConfigMapper.findByName(SystemConfigConstant.RSA_HEX_KEY);
- loginVerifyModeConfig.setValue(rsaHexKey);
+ aesHexKey = HexUtil.encodeHexStr(key);
+
+ SystemConfig loginVerifyModeConfig = systemConfigMapper.findByName(SystemConfigConstant.AES_HEX_KEY);
+ loginVerifyModeConfig.setValue(aesHexKey);
systemConfigMapper.updateById(loginVerifyModeConfig);
- systemConfigDTO.setRsaHexKey(rsaHexKey);
-
+ systemConfigDTO.setRsaHexKey(aesHexKey);
+
Cache cache = cacheManager.getCache(CACHE_NAME);
Optional.ofNullable(cache).ifPresent(cache1 -> cache1.put("dto", systemConfigDTO));
}
- return rsaHexKey;
+ return aesHexKey;
}
-
-
- /**
- * 获取系统是否已初始化
- *
- * @return 管理员名称
- */
- public Boolean getSystemIsInstalled() {
- return systemConfigService.getSystemConfig().getInstalled();
- }
-
-
- /**
- * 获取后端站点域名
- *
- * @return 后端站点域名
- */
- public String getDomain() {
- SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
- return systemConfigDTO.getDomain();
- }
-
-
+
+
/**
* 获取前端站点域名
*
* @return 前端站点域名
*/
public String getFrontDomain() {
- SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
+ SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
return systemConfigDTO.getFrontDomain();
}
-
-
+
+
/**
* 获取实际的前端站点域名
*
* @return 实际的前端站点域名
*/
public String getRealFrontDomain() {
- SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
- return StrUtil.firstNonNull(systemConfigDTO.getFrontDomain(), systemConfigDTO.getDomain());
+ SystemConfigDTO systemConfigDTO = ((SystemConfigService)AopContext.currentProxy()).getSystemConfig();
+ return StringUtils.firstNonNull(systemConfigDTO.getFrontDomain(), getAxiosFromDomainOrSetting(), RequestHolder.getOriginAddress());
}
-
-
+
+
+ /**
+ * 优先使用请求中的 axios-from 参数, 如果没有则使用系统设置中的域名.(用来避免网络环境变化但是系统设置中未及时修改导致的问题)
+ *
+ * @return axios-from 参数或者系统设置中的域名
+ */
+ public String getAxiosFromDomainOrSetting() {
+ 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 页面地址.
*
@@ -269,7 +275,99 @@ public class SystemConfigService {
*
*/
public String getForbiddenUrl() {
- return getRealFrontDomain() + "/403";
+ 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);
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/filter/controller/StorageSourceFilterController.java b/src/main/java/im/zhaojun/zfile/module/filter/controller/StorageSourceFilterController.java
index ef067ba..e922fcb 100644
--- a/src/main/java/im/zhaojun/zfile/module/filter/controller/StorageSourceFilterController.java
+++ b/src/main/java/im/zhaojun/zfile/module/filter/controller/StorageSourceFilterController.java
@@ -2,20 +2,18 @@ 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 im.zhaojun.zfile.core.util.AjaxJson;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import 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 javax.annotation.Resource;
import java.util.List;
/**
@@ -23,7 +21,7 @@ import java.util.List;
*
* @author zhaojun
*/
-@Api(tags = "存储源模块-过滤文件")
+@Tag(name = "存储源模块-过滤文件")
@ApiSort(6)
@RestController
@RequestMapping("/admin")
@@ -33,8 +31,8 @@ public class StorageSourceFilterController {
private FilterConfigService filterConfigService;
@ApiOperationSupport(order = 1)
- @ApiOperation(value = "获取存储源过滤文件列表", notes = "根据存储源 ID 获取存储源设置的过滤文件列表")
- @ApiImplicitParam(paramType = "path", name = "storageId", value = "存储源 id", required = true, dataTypeClass = Integer.class)
+ @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));
@@ -42,9 +40,10 @@ public class StorageSourceFilterController {
@ApiOperationSupport(order = 2)
- @ApiOperation(value = "保存存储源过滤文件列表", notes = "保存指定存储源 ID 设置的过滤文件列表")
- @ApiImplicitParam(paramType = "path", name = "storageId", value = "存储源 id", required = true, dataTypeClass = Integer.class)
+ @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();
diff --git a/src/main/java/im/zhaojun/zfile/module/filter/model/entity/FilterConfig.java b/src/main/java/im/zhaojun/zfile/module/filter/model/entity/FilterConfig.java
index d54d9b0..c6465c9 100644
--- a/src/main/java/im/zhaojun/zfile/module/filter/model/entity/FilterConfig.java
+++ b/src/main/java/im/zhaojun/zfile/module/filter/model/entity/FilterConfig.java
@@ -6,8 +6,7 @@ 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.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@@ -18,35 +17,34 @@ import java.io.Serializable;
* @author zhaojun
*/
@Data
-@ApiModel(value="存储源过滤配置")
+@Schema(name="存储源过滤配置")
@TableName(value = "filter_config")
public class FilterConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
- @ApiModelProperty(value = "ID, 新增无需填写", example = "1")
- @JsonIgnore
+ @Schema(name = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "storage_id")
- @ApiModelProperty(value = "存储源 ID", required = true, example = "1")
+ @Schema(name = "存储源 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer storageId;
@TableField(value = "expression")
- @ApiModelProperty(value = "过滤表达式", required = true, example = "/*.png")
+ @Schema(name = "过滤表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "/*.png")
private String expression;
@TableField(value = "description")
- @ApiModelProperty(value = "表达式描述", required = true, example = "用来辅助记忆表达式")
+ @Schema(name = "表达式描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "用来辅助记忆表达式")
private String description;
@TableField(value = "mode")
- @ApiModelProperty(value = "模式", required = true, example = "隐藏模式,仅隐藏: hidden, 隐藏后不可访问: inaccessible, 隐藏后不可下载: disable_download")
+ @Schema(name = "模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "隐藏模式,仅隐藏: hidden, 隐藏后不可访问: inaccessible, 隐藏后不可下载: disable_download")
private FilterConfigHiddenModeEnum mode;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/filter/service/FilterConfigService.java b/src/main/java/im/zhaojun/zfile/module/filter/service/FilterConfigService.java
index da9fb38..7a668d1 100644
--- a/src/main/java/im/zhaojun/zfile/module/filter/service/FilterConfigService.java
+++ b/src/main/java/im/zhaojun/zfile/module/filter/service/FilterConfigService.java
@@ -1,20 +1,27 @@
package im.zhaojun.zfile.module.filter.service;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.io.FileUtil;
-import cn.hutool.core.util.StrUtil;
+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 javax.annotation.Resource;
import java.util.List;
/**
@@ -29,10 +36,10 @@ public class FilterConfigService {
@Resource
private FilterConfigMapper filterConfigMapper;
-
+
@Resource
- private FilterConfigService filterConfigService;
-
+ private UserStorageSourceService userStorageSourceService;
+
/**
* 根据存储源 ID 获取存储源配置列表
*
@@ -41,12 +48,13 @@ public class FilterConfigService {
*
* @return 存储源过滤规则配置列表
*/
- @Cacheable(key = "'filter-base-' + #storageId")
+ @Cacheable(key = "'filter-base-' + #storageId",
+ condition = "#storageId != null")
public List findByStorageId(Integer storageId) {
return filterConfigMapper.findByStorageId(storageId);
}
-
-
+
+
/**
* 获取所有类型为禁止访问的过滤规则
*
@@ -55,12 +63,13 @@ public class FilterConfigService {
*
* @return 禁止访问的过滤规则列表
*/
- @Cacheable(key = "'filter-inaccessible-' + #storageId")
+ @Cacheable(key = "'filter-inaccessible-' + #storageId",
+ condition = "#storageId != null")
public List findByStorageIdAndInaccessible(Integer storageId) {
return filterConfigMapper.findByStorageIdAndInaccessible(storageId);
}
-
-
+
+
/**
* 获取所有类型为禁止下载的过滤规则
*
@@ -69,12 +78,13 @@ public class FilterConfigService {
*
* @return 禁止下载的过滤规则列表
*/
- @Cacheable(key = "'filter-disable-download-' + #storageId")
+ @Cacheable(key = "'filter-disable-download-' + #storageId",
+ condition = "#storageId != null")
public List findByStorageIdAndDisableDownload(Integer storageId) {
return filterConfigMapper.findByStorageIdAndDisableDownload(storageId);
}
-
-
+
+
/**
* 批量保存存储源过滤规则配置, 会先删除之前的所有配置(在事务中运行)
*
@@ -86,13 +96,14 @@ public class FilterConfigService {
*/
@Transactional(rollbackFor = Exception.class)
public void batchSave(Integer storageId, List filterConfigList) {
- filterConfigService.deleteByStorageId(storageId);
+ ((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(),
@@ -100,11 +111,11 @@ public class FilterConfigService {
}
}
}
-
-
+
+
/**
* 根据存储源 ID 删除所有过滤规则配置
- *
+ *
* @param storageId
* 存储源 ID
*/
@@ -119,7 +130,26 @@ public class FilterConfigService {
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);
+ }
+
+ }
+
/**
* 判断访问的路径是否是不允许访问的
*
@@ -131,10 +161,10 @@ public class FilterConfigService {
*
*/
public boolean checkFileIsInaccessible(Integer storageId, String path) {
- List filterConfigList = filterConfigService.findByStorageIdAndInaccessible(storageId);
+ List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageIdAndInaccessible(storageId);
return testPattern(storageId, filterConfigList, path);
}
-
+
/**
* 指定存储源下的文件名称, 根据过滤表达式判断是否会显示, 如果符合任意一条表达式, 表示隐藏则返回 true, 反之表示不隐藏则返回 false.
@@ -148,7 +178,7 @@ public class FilterConfigService {
* @return 是否是隐藏文件夹
*/
public boolean checkFileIsHidden(Integer storageId, String fileName) {
- List filterConfigList = filterConfigService.findByStorageId(storageId);
+ List filterConfigList = ((FilterConfigService) AopContext.currentProxy()).findByStorageId(storageId);
return testPattern(storageId, filterConfigList, fileName);
}
@@ -165,15 +195,15 @@ public class FilterConfigService {
* @return 是否显示
*/
public boolean checkFileIsDisableDownload(Integer storageId, String fileName) {
- List filterConfigList = filterConfigService.findByStorageIdAndDisableDownload(storageId);
- String filePath = FileUtil.getParent(fileName, 1);
- if (StrUtil.isEmpty(filePath)) {
+ 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。
@@ -189,18 +219,27 @@ public class FilterConfigService {
*/
private boolean testPattern(Integer storageId, List patternList, String test) {
// 如果规则列表为空, 则表示不需要过滤, 直接返回 false
- if (CollUtil.isEmpty(patternList)) {
+ 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 (StrUtil.isEmpty(expression)) {
+ if (StringUtils.isEmpty(expression)) {
if (log.isDebugEnabled()) {
log.debug("存储源 {} 过滤文件测试表达式: {}, 测试字符串: {}, 表达式为空,跳过该规则校验", storageId, expression, test);
}
@@ -209,11 +248,11 @@ public class FilterConfigService {
try {
boolean match = PatternMatcherUtils.testCompatibilityGlobPattern(expression, test);
-
+
if (log.isDebugEnabled()) {
log.debug("存储源 {} 过滤文件测试表达式: {}, 测试字符串: {}, 匹配结果: {}", storageId, expression, test, match);
}
-
+
if (match) {
return true;
}
@@ -224,6 +263,29 @@ public class FilterConfigService {
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());
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/install/controller/InstallController.java b/src/main/java/im/zhaojun/zfile/module/install/controller/InstallController.java
new file mode 100644
index 0000000..8291ecc
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/install/controller/InstallController.java
@@ -0,0 +1,42 @@
+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();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/install/model/request/InstallSystemRequest.java b/src/main/java/im/zhaojun/zfile/module/install/model/request/InstallSystemRequest.java
new file mode 100644
index 0000000..d01e119
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/install/model/request/InstallSystemRequest.java
@@ -0,0 +1,24 @@
+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(name = "站点名称", example = "ZFile Site Name")
+ private String siteName;
+
+ @Schema(name = "用户名", example = "admin")
+ private String username;
+
+ @Schema(name = "密码", example = "123456")
+ private String password;
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/install/service/InstallService.java b/src/main/java/im/zhaojun/zfile/module/install/service/InstallService.java
new file mode 100644
index 0000000..d89de95
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/install/service/InstallService.java
@@ -0,0 +1,52 @@
+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();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/aspect/LinkRateLimiterAspect.java b/src/main/java/im/zhaojun/zfile/module/link/aspect/LinkRateLimiterAspect.java
index c76b72e..7883dfe 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/aspect/LinkRateLimiterAspect.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/aspect/LinkRateLimiterAspect.java
@@ -1,18 +1,20 @@
package im.zhaojun.zfile.module.link.aspect;
-import cn.hutool.cache.impl.TimedCache;
-import cn.hutool.extra.servlet.ServletUtil;
+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 javax.annotation.Resource;
-import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -33,7 +35,8 @@ public class LinkRateLimiterAspect {
@Resource
private SystemConfigService systemConfigService;
- private TimedCache timedCache;
+ @Resource
+ private LinkRateLimiterCache linkRateLimiterCache;
/**
* 校验直链访问频率.
@@ -48,17 +51,21 @@ public class LinkRateLimiterAspect {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
Integer linkLimitSecond = systemConfig.getLinkLimitSecond();
Integer linkDownloadLimit = systemConfig.getLinkDownloadLimit();
- if (timedCache == null) {
- timedCache = new TimedCache<>(linkLimitSecond * 1000);
+ // 如果未设置直链限制, 则不进行校验
+ if (linkLimitSecond == null || linkDownloadLimit == null || linkLimitSecond == 0 || linkDownloadLimit == 0) {
+ return point.proceed();
}
- String clientIp = ServletUtil.getClientIP(httpServletRequest);
- AtomicInteger ipDownloadCount = timedCache.get(clientIp, false, () -> new AtomicInteger(1));
- if (ipDownloadCount.get() > linkDownloadLimit) {
- throw new RuntimeException("当前系统限制每 " + systemConfig.getLinkLimitSecond() + " 秒内只能访问 " + linkDownloadLimit + " 次直链, 已超出请稍后访问.");
+ 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);
}
- ipDownloadCount.incrementAndGet();
- log.info("ip {}, download count: {}", clientIp, ipDownloadCount.get());
+
return point.proceed();
}
diff --git a/src/main/java/im/zhaojun/zfile/module/link/aspect/RefererCheckAspect.java b/src/main/java/im/zhaojun/zfile/module/link/aspect/RefererCheckAspect.java
index 67f7d8e..e7e8938 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/aspect/RefererCheckAspect.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/aspect/RefererCheckAspect.java
@@ -1,11 +1,14 @@
package im.zhaojun.zfile.module.link.aspect;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.StrUtil;
-import im.zhaojun.zfile.module.storage.annotation.RefererCheck;
-import im.zhaojun.zfile.module.link.model.enums.RefererTypeEnum;
-import im.zhaojun.zfile.module.config.service.SystemConfigService;
+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;
@@ -14,9 +17,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
-import javax.annotation.Resource;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.List;
@@ -71,27 +71,27 @@ public class RefererCheckAspect {
// 如果 referer 不允许为空,且当前 referer 为空,则校验
Boolean refererAllowEmpty = systemConfig.getRefererAllowEmpty();
- if (!refererAllowEmpty && StrUtil.isEmpty(referer)) {
+ if (!refererAllowEmpty && StringUtils.isEmpty(referer)) {
log.warn("请求路径 {}, referer 不允许为空,当前请求 referer 为空,禁止访问.", requestUrl);
httpServletResponse.sendRedirect(forbiddenUrl);
return null;
- } else if (refererAllowEmpty && StrUtil.isEmpty(referer)) { // 如果 referer 允许为空,且当前 referer 为空,则跳过校验
+ } else if (refererAllowEmpty && StringUtils.isEmpty(referer)) { // 如果 referer 允许为空,且当前 referer 为空,则跳过校验
return point.proceed();
}
// 获取允许的 referer 地址
String refererValue = systemConfig.getRefererValue();
- List refererValueList = StrUtil.split(refererValue, '\n');
+ List refererValueList = StringUtils.split(refererValue, StringUtils.LF);
// 如果是白名单模式,则校验当前 referer, 如果未在允许的列表中,则禁止访问.
- if (refererType == RefererTypeEnum.WHITE_LIST && !containsPathMatcher(refererValueList, 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)) {
+ if (refererType == RefererTypeEnum.BLACK_LIST && containsPathMatcher(refererValueList, referer) != null) {
log.warn("请求路径 {}, referer 为黑名单模式,当前请求 referer {} 在黑名单中,禁止访问.", requestUrl, referer);
httpServletResponse.sendRedirect(forbiddenUrl);
@@ -110,20 +110,20 @@ public class RefererCheckAspect {
* @param value
* 要校验的值
*
- * @return 如果集合为空 (null 或者空), 返回 false, 否则在表达式列表中找到匹配的返回 true, 找不到返回 false.
+ * @return 返回匹配到的规则项,如果没有匹配到,则返回 null.
*/
- public boolean containsPathMatcher(Collection patternList, String value) {
- if (CollUtil.isEmpty(patternList)) {
- return false;
+ public String containsPathMatcher(Collection patternList, String value) {
+ if (CollectionUtils.isEmpty(patternList)) {
+ return null;
}
for (String pattern : patternList) {
if (pathMatcher.match(pattern, value)) {
- return true;
+ return pattern;
}
}
- return false;
+ return null;
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/cache/LinkRateLimiterCache.java b/src/main/java/im/zhaojun/zfile/module/link/cache/LinkRateLimiterCache.java
new file mode 100644
index 0000000..8adc2cc
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/cache/LinkRateLimiterCache.java
@@ -0,0 +1,55 @@
+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;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/controller/DirectLinkController.java b/src/main/java/im/zhaojun/zfile/module/link/controller/DirectLinkController.java
index 0c9afa3..ca989ee 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/controller/DirectLinkController.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/controller/DirectLinkController.java
@@ -1,19 +1,22 @@
package im.zhaojun.zfile.module.link.controller;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
-import im.zhaojun.zfile.core.constant.ZFileConstant;
-import im.zhaojun.zfile.core.service.DynamicControllerManager;
+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.config.utils.SpringMvcUtils;
+import im.zhaojun.zfile.core.util.SpringMvcUtils;
+import im.zhaojun.zfile.module.link.service.DynamicDirectLinkPrefixService;
import im.zhaojun.zfile.module.link.service.LinkDownloadService;
-import io.swagger.annotations.Api;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
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.ResponseBody;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
-import javax.annotation.PostConstruct;
-import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.Method;
@@ -22,26 +25,29 @@ import java.lang.reflect.Method;
*
* @author zhaojun
*/
-@Api(tags = "短链")
+@Tag(name = "短链")
@ApiSort(5)
@Controller
@Slf4j
public class DirectLinkController {
+ @Resource
+ private LinkDownloadService linkDownloadService;
+
@Resource
private SystemConfigService systemConfigService;
@Resource
- private DynamicControllerManager dynamicControllerManager;
+ private DynamicDirectLinkPrefixService dynamicDirectLinkPrefixService;
- @Resource
- private LinkDownloadService linkDownloadService;
+ public static final String DIRECT_LINK_SUFFIX_PATH = "/{storageKey}/**";
- @PostConstruct
+ @EventListener(ApplicationReadyEvent.class)
public void init() throws NoSuchMethodException {
String directLinkPrefix = systemConfigService.getSystemConfig().getDirectLinkPrefix();
Method directLinkMethod = DirectLinkController.class.getMethod("directLink", String.class);
- dynamicControllerManager.initDirectLinkPrefixPath(directLinkPrefix, this, directLinkMethod);
+ RequestMappingInfo requestMappingInfo = RequestMappingInfo.paths(directLinkPrefix + DIRECT_LINK_SUFFIX_PATH).build();
+ dynamicDirectLinkPrefixService.registerMappingHandlerMapping(SystemConfig.DIRECT_LINK_PREFIX_NAME, requestMappingInfo, this, directLinkMethod);
}
/**
@@ -50,17 +56,16 @@ public class DirectLinkController {
* @param storageKey
* 存储源 key
*/
- @ResponseBody
- public void directLink(@PathVariable("storageKey") String storageKey) throws IOException {
+ public ResponseEntity> directLink(@PathVariable("storageKey") String storageKey) throws IOException {
// 获取直链全路径
String filePath = SpringMvcUtils.getExtractPathWithinPattern();
// 如果路径不是以 / 开头, 则补充上
- if (filePath.length() > 0 && filePath.charAt(0) != ZFileConstant.PATH_SEPARATOR_CHAR) {
- filePath = "/" + filePath;
+ if (StringUtils.isNotEmpty(filePath) && filePath.charAt(0) != StringUtils.SLASH_CHAR) {
+ filePath = StringUtils.SLASH + filePath;
}
- linkDownloadService.handlerDirectLink(storageKey, filePath);
+ return linkDownloadService.handlerDirectLink(storageKey, filePath);
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkController.java b/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkController.java
index ece8f08..49cea62 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkController.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkController.java
@@ -1,29 +1,31 @@
package im.zhaojun.zfile.module.link.controller;
-import cn.hutool.core.util.BooleanUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
-import im.zhaojun.zfile.core.exception.IllegalDownloadLinkException;
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.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.annotations.Api;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiOperation;
+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.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import javax.validation.Valid;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -33,7 +35,7 @@ import java.util.List;
*
* @author zhaojun
*/
-@Api(tags = "直短链模块")
+@Tag(name = "直短链模块")
@ApiSort(5)
@Controller
@Slf4j
@@ -54,44 +56,35 @@ public class ShortLinkController {
@PostMapping("/api/short-link/batch/generate")
@ResponseBody
@ApiOperationSupport(order = 1)
- @ApiOperation(value = "生成短链", notes = "对指定存储源的某文件路径生成短链")
+ @Operation(summary = "生成短链", description ="对指定存储源的某文件路径生成短链")
+ @StoragePermissionCheck(action = FileOperatorTypeEnum.SHORT_LINK)
public AjaxJson> generatorShortLink(@RequestBody @Valid BatchGenerateLinkRequest batchGenerateLinkRequest) {
List result = new ArrayList<>();
// 获取站点域名
- SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
-
- // 是否允许使用短链和短链,如果都不允许,则提示禁止生成.
- Boolean showShortLink = systemConfig.getShowShortLink();
- Boolean showPathLink = systemConfig.getShowPathLink();
- if ( BooleanUtil.isFalse(showShortLink) && BooleanUtil.isFalse(showPathLink)) {
- throw new IllegalDownloadLinkException("当前系统不允许使用直链和短链.");
- }
-
- String domain = systemConfig.getDomain();
+ String domain = systemConfigService.getAxiosFromDomainOrSetting();
Long expireTime = batchGenerateLinkRequest.getExpireTime();
String storageKey = batchGenerateLinkRequest.getStorageKey();
Integer storageId = storageSourceService.findIdByKey(storageKey);
for (String path : batchGenerateLinkRequest.getPaths()) {
// 拼接全路径地址.
- String fullPath = StringUtils.concat(path);
+ 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());
- String pathLink = StringUtils.generatorPathLink(storageKey, fullPath);
- result.add(new BatchGenerateLinkResponse(shortUrl, pathLink));
+ result.add(new BatchGenerateLinkResponse(shortUrl));
}
return AjaxJson.getSuccessData(result);
}
@GetMapping("/s/{key}")
- @ResponseStatus(HttpStatus.FOUND)
@ApiOperationSupport(order = 2)
- @ApiOperation(value = "跳转短链", notes = "根据短链 key 跳转(302 重定向)到对应的直链.")
- @ApiImplicitParam(paramType = "path", name = "key", value = "短链 key", required = true, dataTypeClass = String.class)
- public void parseShortKey(@PathVariable String key) throws IOException {
- linkDownloadService.handlerShortLink(key);
+ @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);
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkManagerController.java b/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkManagerController.java
index 89ed12f..0e7e373 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkManagerController.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/controller/ShortLinkManagerController.java
@@ -1,15 +1,20 @@
package im.zhaojun.zfile.module.link.controller;
-import cn.hutool.core.convert.Convert;
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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.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;
@@ -17,23 +22,26 @@ 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.annotations.Api;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Value;
+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.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
+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;
/**
@@ -41,7 +49,7 @@ import java.util.stream.Stream;
*
* @author zhaojun
*/
-@Api(tags = "直链管理")
+@Tag(name = "直链管理")
@ApiSort(7)
@Controller
@RequestMapping("/admin")
@@ -55,65 +63,26 @@ public class ShortLinkManagerController {
@Resource
private ShortLinkConvert shortLinkConvert;
-
- @Value("${spring.datasource.driver-class-name}")
- private String datasourceDriveClassName;
+
+ @Resource
+ private LinkRateLimiterCache linkRateLimiterCache;
+
@ApiOperationSupport(order = 1)
@GetMapping("/link/list")
- @ApiOperation(value = "搜索短链")
+ @Operation(summary = "搜索短链")
@ResponseBody
- public AjaxJson> list(QueryShortLinkLogRequest queryObj) {
- // 分页和排序
- boolean asc = Objects.equals(queryObj.getOrderDirection(), "asc");
- Page pages = new Page(queryObj.getPage(), queryObj.getLimit())
- .addOrder(new OrderItem(queryObj.getOrderBy(), asc));
-
-
- String dateFrom = queryObj.getDateFrom();
- String dateTo = queryObj.getDateTo();
-
- boolean isSqlite = StrUtil.equals(datasourceDriveClassName, "org.sqlite.JDBC");
- if (isSqlite) {
- if (StrUtil.isNotEmpty(queryObj.getDateFrom())) {
- dateFrom = Convert.toStr(DateUtil.parseDateTime(dateFrom).getTime());
- }
- if (StrUtil.isNotEmpty(queryObj.getDateTo())) {
- dateTo = Convert.toStr(DateUtil.parseDateTime(dateTo).getTime());
- }
- }
-
- // 搜索条件
- QueryWrapper queryWrapper =
- new QueryWrapper<>(new ShortLink())
- .eq(StrUtil.isNotEmpty(queryObj.getStorageId()), "storage_id", queryObj.getStorageId())
- .like(StrUtil.isNotEmpty(queryObj.getKey()), "short_key", queryObj.getKey())
- .like(StrUtil.isNotEmpty(queryObj.getUrl()), "url", queryObj.getUrl())
- .ge(StrUtil.isNotEmpty(queryObj.getDateFrom()), "create_date", dateFrom)
- .le(StrUtil.isNotEmpty(queryObj.getDateTo()), "create_date", dateTo);
-
- // 执行查询
- 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);
- });
- return AjaxJson.getPageData(selectResult.getTotal(), shortLinkResultList);
+ public AjaxJson> list(QueryShortLinkLogRequest queryObj) {
+ Page resultPage = getShortLinkResultPage(queryObj);
+ return AjaxJson.getPageData(resultPage.getTotal(), resultPage.getRecords());
}
-
@ApiOperationSupport(order = 2)
@DeleteMapping("/link/delete/{id}")
- @ApiOperation(value = "删除短链")
- @ApiImplicitParam(paramType = "path", name = "id", value = "短链 id", required = true, dataTypeClass = Integer.class)
+ @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();
@@ -123,10 +92,100 @@ public class ShortLinkManagerController {
@ApiOperationSupport(order = 3)
@PostMapping("/link/delete/batch")
@ResponseBody
- @ApiOperation(value = "批量删除短链")
+ @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);
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/convert/ShortLinkConvert.java b/src/main/java/im/zhaojun/zfile/module/link/convert/ShortLinkConvert.java
index d0b8ec2..d900b20 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/convert/ShortLinkConvert.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/convert/ShortLinkConvert.java
@@ -5,6 +5,7 @@ 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;
/**
@@ -13,7 +14,7 @@ import org.springframework.stereotype.Component;
* @author zhaojun
*/
@Component
-@Mapper(componentModel = "spring")
+@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface ShortLinkConvert {
@Mapping(source = "shortLink.id", target = "id")
diff --git a/src/main/java/im/zhaojun/zfile/module/link/dto/DynamicRegisterMappingHandlerDTO.java b/src/main/java/im/zhaojun/zfile/module/link/dto/DynamicRegisterMappingHandlerDTO.java
new file mode 100644
index 0000000..1dbcf5a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/dto/DynamicRegisterMappingHandlerDTO.java
@@ -0,0 +1,19 @@
+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;
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/link/event/DeleteExpireLinkEvent.java b/src/main/java/im/zhaojun/zfile/module/link/event/DeleteExpireLinkEvent.java
new file mode 100644
index 0000000..1793fde
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/event/DeleteExpireLinkEvent.java
@@ -0,0 +1,7 @@
+package im.zhaojun.zfile.module.link.event;
+
+import lombok.Data;
+
+@Data
+public class DeleteExpireLinkEvent {
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/link/mapper/ShortLinkMapper.java b/src/main/java/im/zhaojun/zfile/module/link/mapper/ShortLinkMapper.java
index cd01847..44b4a91 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/mapper/ShortLinkMapper.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/mapper/ShortLinkMapper.java
@@ -2,9 +2,12 @@ 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 接口
*
@@ -24,20 +27,6 @@ public interface ShortLinkMapper extends BaseMapper {
ShortLink findByKey(@Param("key")String key);
- /**
- * 根据存储源 ID 和文件路径查询短链接
- *
- * @param storageId
- * 存储源 ID
- *
- * @param url
- * 短链接 url
- *
- * @return 短链接信息
- */
- ShortLink findByStorageIdAndUrl(@Param("storageId") Integer storageId, @Param("url") String url);
-
-
/**
* 根据存储源 ID 删除所有数据
*
@@ -45,5 +34,19 @@ public interface ShortLinkMapper extends BaseMapper {
* 存储源 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();
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/dto/CacheInfo.java b/src/main/java/im/zhaojun/zfile/module/link/model/dto/CacheInfo.java
new file mode 100644
index 0000000..d8cc911
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/dto/CacheInfo.java
@@ -0,0 +1,18 @@
+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;
+
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/entity/ShortLink.java b/src/main/java/im/zhaojun/zfile/module/link/model/entity/ShortLink.java
index 595c320..06d67cf 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/entity/ShortLink.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/entity/ShortLink.java
@@ -1,11 +1,11 @@
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.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
@@ -17,39 +17,49 @@ import java.util.Date;
* @author zhaojun
*/
@Data
-@ApiModel(description = "短链信息")
+@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)
- @ApiModelProperty(value = "ID, 新增无需填写", example = "1")
+ @Schema(name = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "storage_id")
- @ApiModelProperty(value = "存储源 ID", example = "1")
+ @Schema(name = "存储源 ID", example = "1")
private Integer storageId;
@TableField(value = "short_key")
- @ApiModelProperty(value = "短链 key", example = "voldd3")
+ @Schema(name = "短链 key", example = "voldd3")
private String shortKey;
@TableField(value = "url")
- @ApiModelProperty(value = "短链 url", example = "/directlink/1/test02.png")
+ @Schema(name = "短链 url", example = "/directlink/1/test02.png")
private String url;
@TableField(value = "create_date")
- @ApiModelProperty(value = "创建时间", example = "2021-11-22 10:05")
+ @Schema(name = "创建时间", example = "2021-11-22 10:05")
private Date createDate;
@TableField(value = "expire_date")
- @ApiModelProperty(value = "过期时间", example = "2021-11-23 10:05")
+ @Schema(name = "过期时间", example = "2021-11-23 10:05")
private Date expireDate;
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/BatchGenerateLinkRequest.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/BatchGenerateLinkRequest.java
index cad6af9..4c27af5 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/request/BatchGenerateLinkRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/BatchGenerateLinkRequest.java
@@ -1,11 +1,11 @@
package im.zhaojun.zfile.module.link.model.request;
-import io.swagger.annotations.ApiModel;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
-import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.NotEmpty;
-import javax.validation.constraints.NotNull;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
import java.util.List;
/**
@@ -13,7 +13,7 @@ import java.util.List;
* @author zhaojun
*/
@Data
-@ApiModel(description = "批量生成直链请求类")
+@Schema(description = "批量生成直链请求类")
public class BatchGenerateLinkRequest {
@NotBlank(message = "存储源 key 不能为空")
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryDownloadLogRequest.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryDownloadLogRequest.java
index 95330df..979831f 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryDownloadLogRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryDownloadLogRequest.java
@@ -1,9 +1,14 @@
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.annotations.ApiModelProperty;
+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;
/**
* 查询下载日志请求参数
@@ -14,31 +19,46 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class QueryDownloadLogRequest extends PageQueryRequest {
- @ApiModelProperty(value="文件路径")
+ @Schema(name="文件路径")
private String path;
- @ApiModelProperty(value="存储源 key")
+ @Schema(name="存储源 key")
private String storageKey;
- @ApiModelProperty(value="短链 key")
+ @Schema(name="链接类型")
+ private String linkType;
+
+ @Schema(name="短链 key")
private String shortKey;
- @ApiModelProperty(value="访问时间从")
- private String dateFrom;
+ @Schema(name="访问时间")
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private List searchDate;
- @ApiModelProperty(value="访问时间至")
- private String dateTo;
-
- @ApiModelProperty(value="访问 ip")
+ @Schema(name="访问 ip")
private String ip;
- @ApiModelProperty(value="访问 user_agent")
+ @Schema(name="访问 user_agent")
private String userAgent;
- @ApiModelProperty(value="访问 referer")
+ @Schema(name="访问 referer")
private String referer;
- @ApiModelProperty(value="排序字段")
+ @Schema(name="排序字段")
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());
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryLoginLogRequest.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryLoginLogRequest.java
new file mode 100644
index 0000000..1dc1ced
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryLoginLogRequest.java
@@ -0,0 +1,61 @@
+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(name="用户名")
+ private String username;
+
+ @Schema(name="密码")
+ private String password;
+
+ @Schema(name="IP")
+ private String ip;
+
+ @Schema(name="User-Agent")
+ private String userAgent;
+
+ @Schema(name="来源")
+ private String referer;
+
+ @Schema(name="登录结果")
+ private String result;
+
+ @Schema(name="访问时间")
+ @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private List searchDate;
+
+ @Schema(name="排序字段")
+ 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());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryShortLinkLogRequest.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryShortLinkLogRequest.java
index 21462bf..e525304 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryShortLinkLogRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/QueryShortLinkLogRequest.java
@@ -1,9 +1,14 @@
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.annotations.ApiModelProperty;
+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
@@ -12,19 +17,30 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class QueryShortLinkLogRequest extends PageQueryRequest {
- @ApiModelProperty(value="短链 key")
+ @Schema(name="短链 key")
private String key;
- @ApiModelProperty(value="存储源 id")
+ @Schema(name="存储源 id")
private String storageId;
- @ApiModelProperty(value="短链文件路径")
+ @Schema(name="短链文件路径")
private String url;
-
- @ApiModelProperty(value="访问时间从")
- private String dateFrom;
-
- @ApiModelProperty(value="访问时间至")
- private String dateTo;
-
+
+ @Schema(name="访问时间")
+ @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());
+ }
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkResult.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkResult.java
index c9a3267..34d96e7 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkResult.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkResult.java
@@ -1,7 +1,8 @@
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.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@@ -14,25 +15,32 @@ import java.util.Date;
@Data
public class ShortLinkResult {
- @ApiModelProperty(value = "短链 id", example = "1")
+ @Schema(name = "短链 id", example = "1")
private Integer id;
- @ApiModelProperty(value = "存储源名称", example = "我的本地存储")
+ @Schema(name = "存储源名称", example = "我的本地存储")
private String storageName;
- @ApiModelProperty(value = "存储源类型")
+ @Schema(name = "存储源类型")
private StorageTypeEnum storageType;
- @ApiModelProperty(value = "短链 key", example = "voldd3")
+ @JsonIgnore
+ private String storageTypeStr;
+
+ public String getStorageTypeStr() {
+ return storageType.getDescription();
+ }
+
+ @Schema(name = "短链 key", example = "voldd3")
private String shortKey;
- @ApiModelProperty(value = "文件 url", example = "/directlink/1/test02.png")
+ @Schema(name = "文件 url", example = "/directlink/1/test02.png")
private String url;
- @ApiModelProperty(value = "创建时间", example = "2021-11-22 10:05")
+ @Schema(name = "创建时间", example = "2021-11-22 10:05")
private Date createDate;
- @ApiModelProperty(value = "过期时间", example = "2021-11-23 10:05")
+ @Schema(name = "过期时间", example = "2021-11-23 10:05")
private Date expireDate;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkSearchRequest.java b/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkSearchRequest.java
index ab88612..b90ee96 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkSearchRequest.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/request/ShortLinkSearchRequest.java
@@ -1,10 +1,9 @@
package im.zhaojun.zfile.module.link.model.request;
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
-import javax.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotBlank;
/**
* 短链接搜索请求参数
@@ -12,35 +11,35 @@ import javax.validation.constraints.NotBlank;
* @author zhaojun
*/
@Data
-@ApiModel(description = "搜索存储源中文件请求类")
+@Schema(description = "搜索存储源中文件请求类")
public class ShortLinkSearchRequest {
- @ApiModelProperty(value = "存储源 id", required = true, example = "1")
+ @Schema(name = "存储源 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotBlank(message = "存储源 id 不能为空")
private Integer storageId;
- @ApiModelProperty(value = "存储源 key", example = "local")
+ @Schema(name = "存储源 key", example = "local")
private String key;
- @ApiModelProperty(value = "文件 url/路径", example = "/a")
+ @Schema(name = "文件 url/路径", example = "/a")
private String url;
- @ApiModelProperty(value = "开始时间", example = "2022-01-01 00:00:00")
+ @Schema(name = "开始时间", example = "2022-01-01 00:00:00")
private String dateFrom;
- @ApiModelProperty(value = "结束时间", example = "2022-12-31 23:59:59")
+ @Schema(name = "结束时间", example = "2022-12-31 23:59:59")
private String dateTo;
- @ApiModelProperty(value = "页码", example = "1")
+ @Schema(name = "页码", example = "1")
private Integer page;
- @ApiModelProperty(value = "每页数量", example = "10")
+ @Schema(name = "每页数量", example = "10")
private Integer limit;
- @ApiModelProperty(value = "排序字段", example = "id")
+ @Schema(name = "排序字段", example = "id")
private String orderBy;
- @ApiModelProperty(value = "排序方式", example = "desc")
+ @Schema(name = "排序方式", example = "desc")
private String orderDirection;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/model/result/BatchGenerateLinkResponse.java b/src/main/java/im/zhaojun/zfile/module/link/model/result/BatchGenerateLinkResponse.java
index 0f1baa2..5e271f7 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/model/result/BatchGenerateLinkResponse.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/model/result/BatchGenerateLinkResponse.java
@@ -1,6 +1,6 @@
package im.zhaojun.zfile.module.link.model.result;
-import io.swagger.annotations.ApiModel;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -8,12 +8,10 @@ import lombok.Data;
* @author zhaojun
*/
@Data
-@ApiModel(description = "批量生成直链结果类")
+@Schema(description = "批量生成直链结果类")
@AllArgsConstructor
public class BatchGenerateLinkResponse {
private String shortLink;
-
- private String pathLink;
-
+
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/service/DynamicDirectLinkPrefixService.java b/src/main/java/im/zhaojun/zfile/module/link/service/DynamicDirectLinkPrefixService.java
new file mode 100644
index 0000000..69d619a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/link/service/DynamicDirectLinkPrefixService.java
@@ -0,0 +1,45 @@
+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()));
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/service/LinkDownloadService.java b/src/main/java/im/zhaojun/zfile/module/link/service/LinkDownloadService.java
index f1efe17..c9a009c 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/service/LinkDownloadService.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/service/LinkDownloadService.java
@@ -3,17 +3,14 @@ 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 cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.BooleanUtil;
-import cn.hutool.core.util.CharsetUtil;
-import cn.hutool.core.util.StrUtil;
-import im.zhaojun.zfile.core.exception.IllegalDownloadLinkException;
-import im.zhaojun.zfile.core.exception.InvalidShortLinkException;
-import im.zhaojun.zfile.core.exception.file.InvalidStorageSourceException;
-import im.zhaojun.zfile.core.exception.file.operator.StorageSourceFileOperatorException;
+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.RequestHolder;
-import im.zhaojun.zfile.core.util.UrlUtils;
+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;
@@ -26,16 +23,14 @@ 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.http.util.EncodingUtils;
+import org.apache.commons.lang3.BooleanUtils;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
-import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
@@ -46,9 +41,6 @@ import java.util.Set;
@Service
public class LinkDownloadService {
- @Resource
- private StorageSourceContext storageSourceContext;
-
@Resource
private StorageSourceService storageSourceService;
@@ -68,33 +60,33 @@ public class LinkDownloadService {
@RefererCheck
@LinkRateLimiter
- public void handlerDirectLink(String storageKey, String filePath) throws IOException {
+ public ResponseEntity> handlerDirectLink(String storageKey, String filePath) {
// 检查系统是否允许直链
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
- if (BooleanUtil.isFalse(systemConfigDTO.getShowPathLink())) {
- throw new InvalidShortLinkException("当前系统不允许使用直链.");
+ if (BooleanUtils.isNotTrue(systemConfigDTO.getShowPathLink())) {
+ throw new ForbiddenAccessException(ErrorCode.BIZ_DIRECT_LINK_NOT_ALLOWED);
}
- handlerDownload(storageKey, filePath, null, DownloadLog.DOWNLOAD_TYPE_DIRECT_LINK);
+ return handlerDownloadGetUrl(storageKey, filePath, null, DownloadLog.DOWNLOAD_TYPE_DIRECT_LINK);
}
@RefererCheck
@LinkRateLimiter
- public void handlerShortLink(String shortKey) throws IOException {
+ public ResponseEntity> handlerShortLink(String shortKey) throws IOException {
// 从缓存中判断是否短链是否过期
if (expireKeySet.contains(shortKey)) {
- throw new InvalidShortLinkException("此链接已过期.");
+ throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_EXPIRED);
}
// 判断是否允许生成短链.
SystemConfigDTO systemConfigDTO = systemConfigService.getSystemConfig();
- if ( BooleanUtil.isFalse(systemConfigDTO.getShowShortLink())) {
- throw new IllegalDownloadLinkException("当前系统不允许使用短链.");
+ if (BooleanUtils.isNotTrue(systemConfigDTO.getShowShortLink())) {
+ throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_NOT_ALLOWED);
}
// 判断短链是否存在
ShortLink shortLink = shortLinkService.findByKey(shortKey);
if (shortLink == null) {
- throw new InvalidShortLinkException("此直链不存在或已失效.");
+ throw new NotFoundAccessException(ErrorCode.BIZ_SHORT_LINK_NOT_FOUNT);
}
// 判断短链是否过期
@@ -103,7 +95,7 @@ public class LinkDownloadService {
boolean isExpire = now.isAfter(shortLink.getExpireDate());
if (isExpire) {
expireKeySet.add(shortKey);
- throw new InvalidShortLinkException("此链接已过期.");
+ throw new ForbiddenAccessException(ErrorCode.BIZ_SHORT_LINK_EXPIRED);
}
}
@@ -111,7 +103,7 @@ public class LinkDownloadService {
Integer storageId = shortLink.getStorageId();
String storageKey = storageSourceService.findStorageKeyById(storageId);
String filePath = shortLink.getUrl();
- handlerDownload(storageKey, filePath, shortKey, DownloadLog.DOWNLOAD_TYPE_SHORT_LINK);
+ return handlerDownloadGetUrl(storageKey, filePath, shortKey, DownloadLog.DOWNLOAD_TYPE_SHORT_LINK);
}
/**
@@ -128,78 +120,77 @@ public class LinkDownloadService {
*
* @param downloadType
* 下载类型, 直链下载(directLink)或短链下载(shortLink)
- *
- * @throws IOException 可能抛出的 IO 异常
*/
- private void handlerDownload(String storageKey, String filePath, String shortKey, String downloadType) throws IOException {
- HttpServletRequest request = RequestHolder.getRequest();
- HttpServletResponse response = RequestHolder.getResponse();
+ 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 (InvalidStorageSourceException e) {
- throw new RuntimeException("无效的或初始化失败的存储源 [" + storageKey + "] 文件 [" + filePath + "] 下载链接异常, 无法下载.", e);
+ 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 RuntimeException("未启用的存储源 [" + storageKey + "] 文件 [" + filePath + "] 下载链接异常, 无法下载.");
+ throw new ErrorPageBizException("未启用的存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.");
}
// 检查是否访问了禁止下载的目录
if (filterConfigService.checkFileIsDisableDownload(storageSource.getId(), filePath)) {
// 获取 Forbidden 页面地址
- String forbiddenUrl = systemConfigService.getForbiddenUrl();
- RequestHolder.getResponse().sendRedirect(forbiddenUrl);
- return;
+ 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 (StorageSourceFileOperatorException e) {
- throw new RuntimeException("获取存储源 [" + storageKey + "] 文件 [" + filePath + "] 下载链接异常, 无法下载.", e);
+ } catch (NotFoundAccessException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ErrorPageBizException("获取存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接异常, 无法下载.", e);
}
// 判断下载链接是否为空
- if (StrUtil.isEmpty(downloadUrl)) {
- throw new RuntimeException("获取存储源 [" + storageKey + "] 文件 [" + filePath + "] 下载链接为空, 无法下载.");
+ if (StringUtils.isEmpty(downloadUrl)) {
+ throw new ErrorPageBizException("获取存储源 [" + storageKey + "] 文件 [" + fileAlias + "] 下载链接为空, 无法下载.");
}
// 记录下载日志.
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
Boolean recordDownloadLog = systemConfig.getRecordDownloadLog();
- if (BooleanUtil.isTrue(recordDownloadLog)) {
+ if (BooleanUtils.isTrue(recordDownloadLog)) {
DownloadLog downloadLog = new DownloadLog(downloadType, filePath, storageKey, shortKey);
downloadLogService.save(downloadLog);
}
// 判断下载链接是否为 m3u8 格式, 如果是则返回 m3u8 内容.
- if (StrUtil.equalsIgnoreCase(FileUtil.extName(filePath), "m3u8")) {
+ if (StringUtils.equalsIgnoreCase(FileUtil.extName(filePath), "m3u8")) {
String textContent = HttpUtil.getTextContent(downloadUrl);
- response.setContentType("application/vnd.apple.mpegurl;charset=utf-8");
- OutputStream outputStream = response.getOutputStream();
- byte[] textContentBytes = EncodingUtils.getBytes(textContent, CharsetUtil.CHARSET_UTF_8.displayName());
- IoUtil.write(outputStream, true, textContentBytes);
- return;
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_TYPE, "application/vnd.apple.mpegurl;charset=utf-8")
+ .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + StringUtils.encodeAllIgnoreSlashes(FileUtils.getName(filePath)))
+ .body(textContent);
}
- // 禁止直链被浏览器 302 缓存.
- response.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate, private");
- response.setHeader(HttpHeaders.PRAGMA, "no-cache");
- response.setHeader(HttpHeaders.EXPIRES, "0");
-
- // 重定向到下载链接.
- String parameterType = request.getParameter("type");
- if (StrUtil.equals(parameterType, "preview")) {
- downloadUrl = UrlUtils.concatQueryParam(downloadUrl, "type", "preview");
- }
-
- response.sendRedirect(downloadUrl);
+ 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();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/link/service/ShortLinkService.java b/src/main/java/im/zhaojun/zfile/module/link/service/ShortLinkService.java
index 1afbd15..70442a1 100644
--- a/src/main/java/im/zhaojun/zfile/module/link/service/ShortLinkService.java
+++ b/src/main/java/im/zhaojun/zfile/module/link/service/ShortLinkService.java
@@ -1,26 +1,32 @@
package im.zhaojun.zfile.module.link.service;
-import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil;
-import com.alibaba.fastjson2.JSONArray;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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 javax.annotation.Resource;
import java.util.Date;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
+import java.util.Objects;
/**
* 短链 Service
@@ -36,10 +42,10 @@ public class ShortLinkService {
private ShortLinkMapper shortLinkMapper;
@Resource
- private ShortLinkService shortLinkService;
+ private SystemConfigService systemConfigService;
@Resource
- private SystemConfigService systemConfigService;
+ private ApplicationEventPublisher applicationEventPublisher;
/**
* 根据短链接 key 查询短链接
@@ -49,31 +55,29 @@ public class ShortLinkService {
*
* @return 短链接信息
*/
- @Cacheable(key = "#key", unless = "#result == null")
+ @Cacheable(key = "#key", unless = "#result == null", condition = "#key != null")
public ShortLink findByKey(String key) {
return shortLinkMapper.findByKey(key);
}
-
/**
- * 根据存储源 ID 和文件路径查询短链接
+ * 根据存储源 ID 和 URL 查询短链接
*
* @param storageId
* 存储源 ID
*
- * @param fileFullPath
- * 文件全路径
+ * @param url
+ * 文件路径
*
* @return 短链接信息
*/
- @Cacheable(key = "#storageId + #fileFullPath", unless = "#result == null")
- public ShortLink findByStorageIdAndUrl(Integer storageId, String fileFullPath) {
- return shortLinkMapper.findByStorageIdAndUrl(storageId, fileFullPath);
+ public @Nullable ShortLink findByStorageIdAndUrl(Integer storageId, String url, @Nullable Date expireDate) {
+ return shortLinkMapper.findByStorageIdAndUrl(storageId, url, expireDate);
}
/**
- * 为存储源指定路径生成短链接, 保证生成的短连接 key 是不同的
+ * 为存储源指定路径生成短链接, 保证生成的短连接 key 是不同的 (如果是永久链接, 则不会重新生成)
*
* @param storageId
* 存储源 id
@@ -86,7 +90,15 @@ public class ShortLinkService {
public ShortLink generatorShortLink(Integer storageId, String fullPath, Long expireTime) {
boolean validate = checkExpireDateIsValidate(expireTime);
if (!validate) {
- throw new IllegalArgumentException("过期时间不合法");
+ 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;
@@ -95,7 +107,7 @@ public class ShortLinkService {
do {
// 获取短链
randomKey = RandomUtil.randomString(6);
- shortLink = shortLinkService.findByKey(randomKey);
+ shortLink = ((ShortLinkService) AopContext.currentProxy()).findByKey(randomKey);
generateCount++;
} while (shortLink != null);
@@ -106,7 +118,7 @@ public class ShortLinkService {
shortLink.setShortKey(randomKey);
if (expireTime == -1) {
- shortLink.setExpireDate(DateUtil.parseDate("9999-12-31"));
+ shortLink.setExpireDate(ShortLink.PERMANENT_EXPIRE_DATE);
} else {
shortLink.setExpireDate(new Date(System.currentTimeMillis() + expireTime * 1000L));
}
@@ -121,8 +133,13 @@ public class ShortLinkService {
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) {
@@ -140,24 +157,44 @@ public class ShortLinkService {
@CacheEvict(allEntries = true)
public int deleteByStorageId(Integer storageId) {
int deleteSize = shortLinkMapper.deleteByStorageId(storageId);
- log.info("删除存储源 ID 为 {} 的直/短链 {} 条", storageId, deleteSize);
+ log.info("删除存储源 ID 为 {} 的短链 {} 条", storageId, deleteSize);
return deleteSize;
}
- public Page selectPage(Page pages, QueryWrapper queryWrapper) {
+ /**
+ * 监听存储源删除事件,根据存储源 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();
- String linkExpireTimes = systemConfig.getLinkExpireTimes();
- JSONArray jsonArray = JSONArray.parse(linkExpireTimes);
- Set expireSet = new HashSet<>();
- for (int i = 0; i < jsonArray.size(); i++) {
- expireSet.add(jsonArray.getJSONObject(i).getLong("seconds"));
+
+ List linkExpireTimeList = systemConfig.getLinkExpireTimes();
+
+ for (LinkExpireDTO linkExpireDTO : linkExpireTimeList) {
+ if (linkExpireDTO.getSeconds().equals(expires)) {
+ return true;
+ }
}
- return expireSet.contains(expires);
+ return false;
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/controller/DownloadLogManagerController.java b/src/main/java/im/zhaojun/zfile/module/log/controller/DownloadLogManagerController.java
index a83360e..8c7e93c 100644
--- a/src/main/java/im/zhaojun/zfile/module/log/controller/DownloadLogManagerController.java
+++ b/src/main/java/im/zhaojun/zfile/module/log/controller/DownloadLogManagerController.java
@@ -1,12 +1,14 @@
package im.zhaojun.zfile.module.log.controller;
-import cn.hutool.core.util.StrUtil;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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.link.model.request.BatchDeleteRequest;
import im.zhaojun.zfile.module.link.model.request.QueryDownloadLogRequest;
import im.zhaojun.zfile.module.log.convert.DownloadLogConvert;
@@ -15,19 +17,15 @@ 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.annotations.Api;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiOperation;
+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.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,7 +37,7 @@ import java.util.stream.Stream;
*
* @author zhaojun
*/
-@Api(tags = "直链日志管理")
+@Tag(name = "直链日志管理")
@ApiSort(7)
@Controller
@RequestMapping("/admin/download/log")
@@ -56,25 +54,26 @@ public class DownloadLogManagerController {
@ApiOperationSupport(order = 1)
@GetMapping("/list")
- @ApiOperation(value = "直链下载日志")
+ @Operation(summary = "直链下载日志")
@ResponseBody
- public AjaxJson> list(QueryDownloadLogRequest queryDownloadLogRequest) {
+ 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(new OrderItem(queryDownloadLogRequest.getOrderBy(), asc));
+ .addOrder(orderItem);
- DownloadLog downloadLog = new DownloadLog();
- QueryWrapper queryWrapper =
- new QueryWrapper<>(downloadLog)
- .eq(StrUtil.isNotEmpty(queryDownloadLogRequest.getStorageKey()), "storage_key", queryDownloadLogRequest.getStorageKey())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getPath()), "path", queryDownloadLogRequest.getPath())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getShortKey()), "short_key", queryDownloadLogRequest.getShortKey())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getIp()), "ip", queryDownloadLogRequest.getIp())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getReferer()), "referer", queryDownloadLogRequest.getReferer())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getUserAgent()), "user_agent", queryDownloadLogRequest.getUserAgent())
- .ge(StrUtil.isNotEmpty(queryDownloadLogRequest.getDateFrom()), "create_time", queryDownloadLogRequest.getDateFrom())
- .le(StrUtil.isNotEmpty(queryDownloadLogRequest.getDateTo()), "create_time", queryDownloadLogRequest.getDateTo());
+ 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);
@@ -94,9 +93,10 @@ public class DownloadLogManagerController {
@ApiOperationSupport(order = 2)
@DeleteMapping("/delete/{id}")
- @ApiOperation(value = "删除直链")
- @ApiImplicitParam(paramType = "path", name = "id", value = "直链 id", required = true, dataTypeClass = Integer.class)
+ @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();
@@ -106,7 +106,8 @@ public class DownloadLogManagerController {
@ApiOperationSupport(order = 3)
@PostMapping("/delete/batch")
@ResponseBody
- @ApiOperation(value = "批量删除直链")
+ @Operation(summary = "批量删除直链")
+ @DemoDisable
public AjaxJson batchDelete(@RequestBody BatchDeleteRequest batchDeleteRequest) {
List ids = batchDeleteRequest.getIds();
downloadLogService.removeBatchByIds(ids);
@@ -116,19 +117,19 @@ public class DownloadLogManagerController {
@ApiOperationSupport(order = 4)
@PostMapping("/delete/batch/query")
@ResponseBody
- @ApiOperation(value = "根据查询条件批量删除直链")
+ @Operation(summary = "根据查询条件批量删除直链")
+ @DemoDisable
public AjaxJson batchDeleteBySearchParams(@RequestBody QueryDownloadLogRequest queryDownloadLogRequest) {
- DownloadLog downloadLog = new DownloadLog();
- QueryWrapper queryWrapper =
- new QueryWrapper<>(downloadLog)
- .eq(StrUtil.isNotEmpty(queryDownloadLogRequest.getStorageKey()), "storage_key", queryDownloadLogRequest.getStorageKey())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getPath()), "path", queryDownloadLogRequest.getPath())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getShortKey()), "short_key", queryDownloadLogRequest.getShortKey())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getIp()), "ip", queryDownloadLogRequest.getIp())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getReferer()), "referer", queryDownloadLogRequest.getReferer())
- .like(StrUtil.isNotEmpty(queryDownloadLogRequest.getUserAgent()), "user_agent", queryDownloadLogRequest.getUserAgent())
- .ge(StrUtil.isNotEmpty(queryDownloadLogRequest.getDateFrom()), "create_time", queryDownloadLogRequest.getDateFrom())
- .le(StrUtil.isNotEmpty(queryDownloadLogRequest.getDateTo()), "create_time", queryDownloadLogRequest.getDateTo());
+
+ 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();
diff --git a/src/main/java/im/zhaojun/zfile/module/log/controller/LoginLogController.java b/src/main/java/im/zhaojun/zfile/module/log/controller/LoginLogController.java
new file mode 100644
index 0000000..6010019
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/log/controller/LoginLogController.java
@@ -0,0 +1,64 @@
+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());
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/mapper/DownloadLogMapper.java b/src/main/java/im/zhaojun/zfile/module/log/mapper/DownloadLogMapper.java
index a28e23e..54c61b0 100644
--- a/src/main/java/im/zhaojun/zfile/module/log/mapper/DownloadLogMapper.java
+++ b/src/main/java/im/zhaojun/zfile/module/log/mapper/DownloadLogMapper.java
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Mapper;
*/
@Mapper
public interface DownloadLogMapper extends BaseMapper {
-
+
/**
* 根据存储源 KEY 删除所有数据
*
@@ -19,5 +19,13 @@ public interface DownloadLogMapper extends BaseMapper {
* 存储源 KEY
*/
int deleteByStorageKey(String storageKey);
+
+
+ /**
+ * 删除过期的短链下载日志
+ *
+ * @return 删除的行数
+ */
+ int deleteExpireShortLinkLog();
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/mapper/LoginLogMapper.java b/src/main/java/im/zhaojun/zfile/module/log/mapper/LoginLogMapper.java
new file mode 100644
index 0000000..fbfcba0
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/log/mapper/LoginLogMapper.java
@@ -0,0 +1,9 @@
+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 {
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopFileDTO.java b/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopFileDTO.java
deleted file mode 100644
index fd8ee90..0000000
--- a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopFileDTO.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package im.zhaojun.zfile.module.log.model.dto;
-
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 下载文件 排行 dto 类
- *
- * @author zhaojun
- */
-@Data
-public class DownloadTopFileDTO {
-
- @ApiModelProperty(value = "短链 key")
- private String shortKey;
-
- @ApiModelProperty(value = "存储源 key")
- private String storageKey;
-
- @ApiModelProperty(value = "文件路径")
- private String path;
-
- @ApiModelProperty(value = "下载次数")
- private Integer count;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopIpDTO.java b/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopIpDTO.java
deleted file mode 100644
index 2f996b9..0000000
--- a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopIpDTO.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package im.zhaojun.zfile.module.log.model.dto;
-
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 下载 ip 排行 dto 类
- * @author zhaojun
- */
-@Data
-public class DownloadTopIpDTO {
-
- @ApiModelProperty(value = "ip 地址")
- private String ip;
-
- @ApiModelProperty(value = "下载次数")
- private Integer count;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopRefererDTO.java b/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopRefererDTO.java
deleted file mode 100644
index f3622eb..0000000
--- a/src/main/java/im/zhaojun/zfile/module/log/model/dto/DownloadTopRefererDTO.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package im.zhaojun.zfile.module.log.model.dto;
-
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 下载 referer 排行 dto 类
- * @author zhaojun
- */
-@Data
-public class DownloadTopRefererDTO {
-
- @ApiModelProperty(value = "referer", notes = "来源网站")
- private String referer;
-
- @ApiModelProperty(value = "下载次数")
- private Integer count;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/entity/DownloadLog.java b/src/main/java/im/zhaojun/zfile/module/log/model/entity/DownloadLog.java
index 3540913..2625bcf 100644
--- a/src/main/java/im/zhaojun/zfile/module/log/model/entity/DownloadLog.java
+++ b/src/main/java/im/zhaojun/zfile/module/log/model/entity/DownloadLog.java
@@ -1,18 +1,18 @@
package im.zhaojun.zfile.module.log.model.entity;
-import cn.hutool.extra.servlet.ServletUtil;
+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.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
+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 javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
@@ -22,7 +22,7 @@ import java.util.Date;
* @author zhaojun
*/
@Data
-@ApiModel(value="文件下载日志")
+@Tag(name ="文件下载日志")
@TableName(value = "`download_log`")
@NoArgsConstructor
public class DownloadLog implements Serializable {
@@ -34,47 +34,47 @@ public class DownloadLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
- @ApiModelProperty(value = "ID, 新增无需填写", example = "1")
+ @Schema(name = "ID, 新增无需填写", example = "1")
private Integer id;
@TableField(value = "`download_type`")
- @ApiModelProperty(value="下载类型", example = "directLink", allowableValues = "directLink, shortLink")
+ @Schema(name="下载类型", example = "directLink", allowableValues = "directLink, shortLink")
private String downloadType;
@TableField(value = "`path`")
- @ApiModelProperty(value="文件路径")
+ @Schema(name="文件路径")
private String path;
@TableField(value = "`storage_key`")
- @ApiModelProperty(value="存储源 key")
+ @Schema(name="存储源 key")
private String storageKey;
@TableField(value = "`create_time`")
- @ApiModelProperty(value="访问时间")
+ @Schema(name="访问时间")
private Date createTime;
@TableField(value = "`ip`")
- @ApiModelProperty(value="访问 ip")
+ @Schema(name="访问 ip")
private String ip;
@TableField(value = "short_key")
- @ApiModelProperty(value = "短链 key", example = "voldd3")
+ @Schema(name = "短链 key", example = "voldd3")
private String shortKey;
@TableField(value = "`user_agent`")
- @ApiModelProperty(value="访问 user_agent")
+ @Schema(name="访问 user_agent")
private String userAgent;
@TableField(value = "`referer`")
- @ApiModelProperty(value="访问 referer")
+ @Schema(name="访问 referer")
private String referer;
public DownloadLog(String downloadType, String path, String storageKey, String shortKey) {
@@ -84,7 +84,7 @@ public class DownloadLog implements Serializable {
this.shortKey = shortKey;
this.createTime = new Date();
HttpServletRequest request = RequestHolder.getRequest();
- this.ip = ServletUtil.getClientIP(request);
+ this.ip = JakartaServletUtil.getClientIP(request);
this.referer = request.getHeader(HttpHeaders.REFERER);
this.userAgent = request.getHeader(HttpHeaders.USER_AGENT);
}
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/entity/LoginLog.java b/src/main/java/im/zhaojun/zfile/module/log/model/entity/LoginLog.java
new file mode 100644
index 0000000..a25aeb8
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/log/model/entity/LoginLog.java
@@ -0,0 +1,39 @@
+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;
+
+}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/request/DownloadTopInfoRequest.java b/src/main/java/im/zhaojun/zfile/module/log/model/request/DownloadTopInfoRequest.java
deleted file mode 100644
index d9f4a67..0000000
--- a/src/main/java/im/zhaojun/zfile/module/log/model/request/DownloadTopInfoRequest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package im.zhaojun.zfile.module.log.model.request;
-
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 下载排行请求类
- *
- * @author zhaojun
- */
-@Data
-public class DownloadTopInfoRequest {
-
- @ApiModelProperty(value = "排行数量", required = true)
- private Integer top;
-
- @ApiModelProperty(value = "开始时间")
- private String startTime;
-
- @ApiModelProperty(value = "结束时间")
- private String endTime;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/model/result/DownloadLogResult.java b/src/main/java/im/zhaojun/zfile/module/log/model/result/DownloadLogResult.java
index b6648e6..36741c6 100644
--- a/src/main/java/im/zhaojun/zfile/module/log/model/result/DownloadLogResult.java
+++ b/src/main/java/im/zhaojun/zfile/module/log/model/result/DownloadLogResult.java
@@ -1,7 +1,7 @@
package im.zhaojun.zfile.module.log.model.result;
import im.zhaojun.zfile.module.storage.model.enums.StorageTypeEnum;
-import io.swagger.annotations.ApiModelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@@ -14,34 +14,34 @@ import java.util.Date;
@Data
public class DownloadLogResult {
- @ApiModelProperty(value="")
+ @Schema(name="")
private Integer id;
- @ApiModelProperty(value="文件路径")
+ @Schema(name="文件路径")
private String path;
- @ApiModelProperty(value = "存储源类型")
+ @Schema(name = "存储源类型")
private StorageTypeEnum storageType;
- @ApiModelProperty(value = "存储源名称", example = "我的本地存储")
+ @Schema(name = "存储源名称", example = "我的本地存储")
private String storageName;
- @ApiModelProperty(value = "存储源Key", example = "local")
+ @Schema(name = "存储源Key", example = "local")
private String storageKey;
- @ApiModelProperty(value="访问时间")
+ @Schema(name="访问时间")
private Date createTime;
- @ApiModelProperty(value="访问 ip")
+ @Schema(name="访问 ip")
private String ip;
- @ApiModelProperty(value = "短链 Key")
+ @Schema(name = "短链 Key")
private String shortKey;
- @ApiModelProperty(value="访问 user_agent")
+ @Schema(name="访问 user_agent")
private String userAgent;
- @ApiModelProperty(value="访问 referer")
+ @Schema(name="访问 referer")
private String referer;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/service/DownloadLogService.java b/src/main/java/im/zhaojun/zfile/module/log/service/DownloadLogService.java
index 39c3381..581d9cb 100644
--- a/src/main/java/im/zhaojun/zfile/module/log/service/DownloadLogService.java
+++ b/src/main/java/im/zhaojun/zfile/module/log/service/DownloadLogService.java
@@ -1,13 +1,18 @@
package im.zhaojun.zfile.module.log.service;
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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 javax.annotation.Resource;
import java.util.List;
/**
@@ -15,6 +20,7 @@ import java.util.List;
*
* @author zhaojun
*/
+@Slf4j
@Service
public class DownloadLogService {
@@ -25,7 +31,7 @@ public class DownloadLogService {
downloadLogMapper.insert(downloadLog);
}
- public Page selectPage(Page pages, QueryWrapper queryWrapper) {
+ public Page selectPage(Page pages, Wrapper queryWrapper) {
return downloadLogMapper.selectPage(pages, queryWrapper);
}
@@ -38,12 +44,49 @@ public class DownloadLogService {
downloadLogMapper.deleteBatchIds(ids);
}
- public void deleteByQueryWrapper(QueryWrapper queryWrapper) {
+ public void deleteByQueryWrapper(Wrapper queryWrapper) {
downloadLogMapper.delete(queryWrapper);
}
public int deleteByStorageKey(String storageKey) {
- return downloadLogMapper.deleteByStorageKey(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);
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/log/service/LoginLogService.java b/src/main/java/im/zhaojun/zfile/module/log/service/LoginLogService.java
new file mode 100644
index 0000000..e9db67e
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/log/service/LoginLogService.java
@@ -0,0 +1,24 @@
+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);
+ }
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/login/controller/LoginController.java b/src/main/java/im/zhaojun/zfile/module/login/controller/LoginController.java
deleted file mode 100644
index 0574ee2..0000000
--- a/src/main/java/im/zhaojun/zfile/module/login/controller/LoginController.java
+++ /dev/null
@@ -1 +0,0 @@
-package im.zhaojun.zfile.module.login.controller;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.crypto.SecureUtil;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import com.github.xiaoymin.knife4j.annotations.ApiSort;
import dev.samstevens.totp.exceptions.QrGenerationException;
import im.zhaojun.zfile.core.util.AjaxJson;
import im.zhaojun.zfile.module.config.model.dto.SystemConfigDTO;
import im.zhaojun.zfile.module.config.service.SystemConfigService;
import im.zhaojun.zfile.module.login.model.enums.LoginVerifyModeEnum;
import im.zhaojun.zfile.module.login.model.request.VerifyLoginTwoFactorAuthenticatorRequest;
import im.zhaojun.zfile.module.login.model.result.LoginTwoFactorAuthenticatorResult;
import im.zhaojun.zfile.module.login.model.result.LoginVerifyImgResult;
import im.zhaojun.zfile.module.login.request.UserLoginRequest;
import im.zhaojun.zfile.module.login.service.ImgVerifyCodeService;
import im.zhaojun.zfile.module.login.service.TwoFactorAuthenticatorVerifyService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Objects;
/**
* 登陆注销相关接口
*
* @author zhaojun
*/
@Api(tags = "登录模块")
@ApiSort(1)
@RestController
@RequestMapping("/admin")
public class LoginController {
@Resource
private SystemConfigService systemConfigService;
@Resource
private ImgVerifyCodeService imgVerifyCodeService;
@Resource
private TwoFactorAuthenticatorVerifyService twoFactorAuthenticatorVerifyService;
@ApiOperationSupport(order = 1, ignoreParameters = {"zfile-token"})
@ApiOperation(value = "登录")
@PostMapping("/login")
public AjaxJson> doLogin(@Valid @RequestBody UserLoginRequest userLoginRequest) {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
String verifyCode = userLoginRequest.getVerifyCode();
String verifyCodeUuid = userLoginRequest.getVerifyCodeUUID();
LoginVerifyModeEnum loginVerifyMode = systemConfig.getLoginVerifyMode();
String loginVerifySecret = systemConfig.getLoginVerifySecret();
if (Objects.equals(loginVerifyMode, LoginVerifyModeEnum.TWO_FACTOR_AUTHENTICATION_MODE)) {
twoFactorAuthenticatorVerifyService.checkCode(loginVerifySecret, verifyCode);
} else if (Objects.equals(loginVerifyMode, LoginVerifyModeEnum.IMG_VERIFY_MODE)) {
imgVerifyCodeService.checkCaptcha(verifyCodeUuid, verifyCode);
}
if (Objects.equals(systemConfig.getUsername(), userLoginRequest.getUsername()) &&
Objects.equals(systemConfig.getPassword(), SecureUtil.md5(userLoginRequest.getPassword()))) {
StpUtil.login("admin");
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return AjaxJson.getSuccess("登录成功", tokenInfo.getTokenValue());
}
return AjaxJson.getError("登录失败, 账号或密码错误");
}
@ApiOperationSupport(order = 2)
@ApiOperation(value = "注销")
@PostMapping("/logout")
public AjaxJson> logout() {
StpUtil.logout();
return AjaxJson.getSuccess("注销成功");
}
@ApiOperationSupport(order = 3)
@ApiOperation(value = "生成 2FA")
@GetMapping("/2fa/setup")
public AjaxJson setupDevice() throws QrGenerationException {
LoginTwoFactorAuthenticatorResult loginTwoFactorAuthenticatorResult = twoFactorAuthenticatorVerifyService.setupDevice();
return AjaxJson.getSuccessData(loginTwoFactorAuthenticatorResult);
}
@ApiOperationSupport(order = 4)
@ApiOperation(value = "2FA 验证并绑定")
@PostMapping("/2fa/verify")
public AjaxJson> deviceVerify(@Valid @RequestBody VerifyLoginTwoFactorAuthenticatorRequest verifyLoginTwoFactorAuthenticatorRequest) {
twoFactorAuthenticatorVerifyService.deviceVerify(verifyLoginTwoFactorAuthenticatorRequest);
return AjaxJson.getSuccess();
}
@ApiOperationSupport(order = 5)
@ApiOperation(value = "获取登陆验证方式")
@GetMapping("/login/verify-mode")
public AjaxJson loginVerifyMode() {
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
return AjaxJson.getSuccessData(systemConfig.getLoginVerifyMode());
}
@ApiOperationSupport(order = 6)
@ApiOperation(value = "获取图形验证码")
@GetMapping("/login/captcha")
public AjaxJson captcha() {
LoginVerifyImgResult loginVerifyImgResult = imgVerifyCodeService.generatorCaptcha();
return AjaxJson.getSuccessData(loginVerifyImgResult);
}
@ApiOperationSupport(order = 7)
@ApiOperation(value = "检测是否已登录")
@GetMapping("/login/check")
public AjaxJson checkLogin() {
return AjaxJson.getSuccessData(StpUtil.isLogin());
}
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/login/model/request/VerifyLoginTwoFactorAuthenticatorRequest.java b/src/main/java/im/zhaojun/zfile/module/login/model/request/VerifyLoginTwoFactorAuthenticatorRequest.java
deleted file mode 100644
index 20b53fb..0000000
--- a/src/main/java/im/zhaojun/zfile/module/login/model/request/VerifyLoginTwoFactorAuthenticatorRequest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package im.zhaojun.zfile.module.login.model.request;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-
-import javax.validation.constraints.NotBlank;
-
-/**
- * 验证 2FA 认证返回结果
- *
- * @author zhaojun
- */
-@Data
-@AllArgsConstructor
-@ApiModel(description = "验证二步验证结果")
-public class VerifyLoginTwoFactorAuthenticatorRequest {
-
- @ApiModelProperty(value = "二步验证二维码", required = true, example = "EwBoxxxxxxxxxxxxxxxbAI=")
- @NotBlank(message = "二步验证密钥不能为空")
- private String secret;
-
- @ApiModelProperty(value = "APP 生成的二步验证验证码", required = true, example = "125612")
- @NotBlank(message = "二步验证验证码不能为空")
- private String code;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginTwoFactorAuthenticatorResult.java b/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginTwoFactorAuthenticatorResult.java
deleted file mode 100644
index 04afd1b..0000000
--- a/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginTwoFactorAuthenticatorResult.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package im.zhaojun.zfile.module.login.model.result;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-
-/**
- * 登陆 2FA 认证生成返回结果
- *
- * @author zhaojun
- */
-@Data
-@AllArgsConstructor
-@ApiModel(description = "生成二步验证结果")
-public class LoginTwoFactorAuthenticatorResult {
-
- @ApiModelProperty(value = "二步验证二维码")
- private String qrcode;
-
- @ApiModelProperty(value = "二步验证密钥")
- private String secret;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginVerifyImgResult.java b/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginVerifyImgResult.java
deleted file mode 100644
index abc5f00..0000000
--- a/src/main/java/im/zhaojun/zfile/module/login/model/result/LoginVerifyImgResult.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package im.zhaojun.zfile.module.login.model.result;
-
-import io.swagger.annotations.ApiModel;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-/**
- * 生成图片验证码结果类
- *
- * @author zhaojun
- */
-@Data
-@ApiModel(description = "生成图片验证码结果类")
-public class LoginVerifyImgResult {
-
- @ApiModelProperty(value = "验证码图片", example = "data:image/png;base64,iajsiAAA...")
- private String imgBase64;
-
- @ApiModelProperty(value = "验证码 UUID", example = "c140a792-4ca2-4dac-8d4c-35750b78524f")
- private String uuid;
-
-}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/login/request/UserLoginRequest.java b/src/main/java/im/zhaojun/zfile/module/login/request/UserLoginRequest.java
deleted file mode 100644
index 5a24ad3..0000000
--- a/src/main/java/im/zhaojun/zfile/module/login/request/UserLoginRequest.java
+++ /dev/null
@@ -1 +0,0 @@
-package im.zhaojun.zfile.module.login.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 用户登录请求参数参数
*
* @author zhaojun
*/
@Data
@ApiModel(description = "用户登录请求参数类")
public class UserLoginRequest {
@ApiModelProperty(value = "用户名", required = true, example = "admin")
@NotBlank(message = "用户名不能为空")
private String username;
@ApiModelProperty(value = "密码", required = true, example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
@ApiModelProperty(value = "验证码", example = "123456")
private String verifyCode;
@ApiModelProperty(value = "验证码 UUID", notes = "用于图形验证码确认每个验证码图片请求的唯一值.", example = "c140a792-4ca2-4dac-8d4c-35750b78524f")
private String verifyCodeUUID;
}
\ No newline at end of file
diff --git a/src/main/java/im/zhaojun/zfile/module/onlyoffice/controller/OnlyOfficeController.java b/src/main/java/im/zhaojun/zfile/module/onlyoffice/controller/OnlyOfficeController.java
new file mode 100644
index 0000000..b1d245a
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/onlyoffice/controller/OnlyOfficeController.java
@@ -0,0 +1,222 @@
+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.URL;
+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("文件不存在");
+ }
+
+ boolean hasUploadPermission = userStorageSourceService.hasCurrentUserStorageOperatorPermission(storageId, FileOperatorTypeEnum.UPLOAD);
+ return Pair.of(fileItem, hasUploadPermission);
+ } catch (Exception e) {
+ throw new BizException("获取文件信息失败: " + e.getMessage());
+ }
+ }
+
+
+ /**
+ * 生成 OnlyOffice 预览所需的 JSON 数据. 配置参考:
+ *
+ * @param fileItemResult
+ * 文件信息
+ *
+ * @param key
+ * OnlyOffice JWT 密钥
+ *
+ * @param hasUploadPermission
+ * 是否有上传(编辑)权限
+ *
+ * @return OnlyOffice 预览所需的 JSON 数据, 包含 JWT 密钥
+ */
+ private JSONObject createOnlyOfficePayload(FileItemResult fileItemResult, String key, boolean hasUploadPermission) {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("width", "100%");
+ jsonObject.put("height", "100%");
+
+ jsonObject.put("document", new JSONObject()
+ .fluentPut("fileType", FileUtils.getExtension(fileItemResult.getName()))
+ .fluentPut("key", key)
+ .fluentPut("permissions", new JSONObject()
+ .fluentPut("edit", hasUploadPermission))
+ .fluentPut("title", fileItemResult.getName())
+ .fluentPut("url", fileItemResult.getUrl())
+ .fluentPut("lang", "zh-CN"));
+
+ User currentUser = ZFileAuthUtil.getCurrentUser();
+
+ SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
+ String onlyOfficeSecret = systemConfig.getOnlyOfficeSecret();
+
+
+ jsonObject.put("editorConfig", new JSONObject()
+ .fluentPut("callbackUrl", StringUtils.concat(systemConfigService.getAxiosFromDomainOrSetting(), "/onlyOffice/callback"))
+ .fluentPut("lang", "zh-CN")
+ .fluentPut("user", new JSONObject()
+ .fluentPut("id", currentUser.getId())
+ .fluentPut("name", StringUtils.firstNonNull(currentUser.getNickname(), currentUser.getUsername()))));
+
+ if (StringUtils.isNotEmpty(onlyOfficeSecret)) {
+ String token = JWTUtil.createToken(jsonObject, onlyOfficeSecret.getBytes(StandardCharsets.UTF_8));
+ jsonObject.put("token", token);
+ }
+
+ return jsonObject;
+ }
+
+ @RequestMapping("/callback")
+ public String callBack(@RequestBody OnlyOfficeCallback onlyOfficeCallback) {
+ log.debug("OnlyOffice 回调信息: {}, {}", onlyOfficeCallback.getStatus(), onlyOfficeCallback);
+ boolean useOnlyOfficeSecret = StrUtil.isNotBlank(systemConfigService.getSystemConfig().getOnlyOfficeSecret());
+ if (useOnlyOfficeSecret) {
+
+ if (StrUtil.isBlank(onlyOfficeCallback.getToken())) {
+ log.error("OnlyOffice 回调 Token 为空: {}", onlyOfficeCallback);
+ return CALLBACK_ERROR_MSG;
+ }
+
+ if (!JWTUtil.verify(onlyOfficeCallback.getToken(), StrUtil.bytes(systemConfigService.getSystemConfig().getOnlyOfficeSecret(), StandardCharsets.UTF_8))) {
+ log.error("OnlyOffice 回调 Token 验证失败: {}", onlyOfficeCallback);
+ return CALLBACK_ERROR_MSG;
+ }
+
+ }
+ // 文件发送了变化,清空缓存中该文件的 key 信息.
+ if (SUPPORTED_STATUS.contains(onlyOfficeCallback.getStatus())) {
+ String key = onlyOfficeCallback.getKey();
+ OnlyOfficeFile onlyOfficeFile = OnlyOfficeKeyCacheUtils.removeByKey(key);
+ ReentrantLock lock = OnlyOfficeKeyCacheUtils.getLock(onlyOfficeFile);
+ lock.lock();
+ log.debug("开始处理 OnlyOffice 文件: {}, 加锁", key);
+ try {
+ // 文件不存在或者存储策略不存在, 直接返回错误信息.
+ if (onlyOfficeFile == null) {
+ return CALLBACK_ERROR_MSG;
+ }
+ AbstractBaseFileService> storageServiceByKey = StorageSourceContext.getByStorageKey(onlyOfficeFile.getStorageKey());
+ if (storageServiceByKey == null) {
+ return CALLBACK_ERROR_MSG;
+ }
+
+ String userId = CollUtil.getFirst(onlyOfficeCallback.getUsers());
+ if (StringUtils.isNotBlank(userId)) {
+ StpUtil.login(userId);
+ }
+
+ log.debug("开始保存 OnlyOffice 文件: {}, {}", onlyOfficeFile.getStorageKey(), onlyOfficeFile.getPathAndName());
+
+ if (Beans.isInstanceOf(storageServiceByKey, AbstractProxyTransferService.class)) {
+ // 进行上传.
+ AbstractProxyTransferService> proxyUploadService = (AbstractProxyTransferService>) storageServiceByKey;
+ try (InputStream inputStream = new URL(onlyOfficeCallback.getUrl()).openStream()) {
+ String pathAndName = onlyOfficeFile.getPathAndName();
+ proxyUploadService.uploadFile(pathAndName, inputStream);
+ } catch (Exception e) {
+ log.error("回调保存 OnlyOffice 文件失败", e);
+ return CALLBACK_ERROR_MSG;
+ }
+ }
+
+ log.debug("完成保存 OnlyOffice 文件: {}, {}", onlyOfficeFile.getStorageKey(), onlyOfficeFile.getPathAndName());
+ } finally {
+ log.debug("完成处理 OnlyOffice 文件: {}, 解锁", key);
+ lock.unlock();
+ }
+ }
+ return CALLBACK_SUCCESS_MSG;
+ }
+}
diff --git a/src/main/java/im/zhaojun/zfile/module/onlyoffice/model/OnlyOfficeCallback.java b/src/main/java/im/zhaojun/zfile/module/onlyoffice/model/OnlyOfficeCallback.java
new file mode 100644
index 0000000..aa93e67
--- /dev/null
+++ b/src/main/java/im/zhaojun/zfile/module/onlyoffice/model/OnlyOfficeCallback.java
@@ -0,0 +1,92 @@
+package im.zhaojun.zfile.module.onlyoffice.model;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Data
+public class OnlyOfficeCallback {
+
+ /**
+ * 定义编辑的文档标识符。
+ */
+ private String key;
+
+ /**
+ * 定义文档的状态。 可以有以下值:
+ * 1 - 正在编辑文档,
+ * 2 - 文档已准备好保存,
+ * 3 - 发生文档保存错误,
+ * 4 - 文档已关闭,没有任何更改,
+ * 6 - 正在编辑文档,但保存了当前文档状态,
+ * 7 - 强制保存文档时发生错误。
+ */
+ private int status;
+
+ /**
+ * 定义已编辑的要由文档存储服务保存的文档的链接。 仅当 status 值等于 2, 3, 6 或 7 时,链接才存在。
+ */
+ private String url;
+
+ /**
+ * 定义有文档更改历史的对象。 仅当 status 值等于 2 或 3 时,对象才存在。
+ * 它包含对象 changes 和 serverVersion,它们必须作为对象的属性 changes 和 serverVersion 以参数形式发送给 refreshHistory 方法。
+ */
+ private Map history;
+
+ /**
+ * 定义打开文档进行编辑的用户的标识符列表;
+ * 当文档被更改时,用户将返回最后编辑文档的用户的标识符(对于 status 2 和 status 6 的应答)。
+ */
+ private List