mirror of
https://github.com/zfile-dev/zfile.git
synced 2025-04-19 05:34:52 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73b42cf654 | ||
|
|
1fac59c4cd | ||
|
|
6ed6b4a019 | ||
|
|
7bd02437f0 | ||
|
|
57aeb5771c | ||
|
|
695c03a530 | ||
|
|
6ebc403572 | ||
|
|
7ff6fe43b5 | ||
|
|
b34f141181 | ||
|
|
87dd7b58d1 | ||
|
|
2a949db5d2 | ||
|
|
b889e91fb5 | ||
|
|
b5c757f9f0 | ||
|
|
a465f48b94 | ||
|
|
797a0a0e4c | ||
|
|
e7ff159b6d | ||
|
|
a9fbf54bb2 | ||
|
|
81f9e262f5 | ||
|
|
23bb3960ab | ||
|
|
c4a17985a4 | ||
|
|
75ddcd47f4 | ||
|
|
2dd03ae490 | ||
|
|
5b383c8741 | ||
|
|
73198d7852 | ||
|
|
fb0d9721aa | ||
|
|
b24c663fd6 | ||
|
|
6e62cfc84d | ||
|
|
eee22e9dc9 |
106
README.md
106
README.md
@@ -1,32 +1,47 @@
|
||||
# Z-File
|
||||
<p align = "center">
|
||||
<img alt="ZFile" src="https://cdn.jun6.net/2021/04/21/69a89344e2a84.png" height="150px">
|
||||
<br><br>
|
||||
基于 Java 的在线网盘程序,支持对接 S3、OneDrive、SharePoint、又拍云、本地存储、FTP 等存储源,支持在线浏览图片、播放音视频,文本文件等文件类型。
|
||||
<br><br>
|
||||
<img src="https://img.shields.io/badge/license-MIT-blue.svg?longCache=true&style=flat-square">
|
||||
<img src="https://api.codacy.com/project/badge/Grade/70b793267f7941d58cbd93f50c9a8e0a">
|
||||
<img src="https://img.shields.io/github/last-commit/zhaojun1998/zfile.svg?style=flat-square">
|
||||
<img src="https://img.shields.io/github/downloads/zhaojun1998/zfile/total?style=flat-square">
|
||||
<img src="https://img.shields.io/github/v/release/zhaojun1998/zfile?style=flat-square">
|
||||
<img src="https://img.shields.io/github/commit-activity/y/zhaojun1998/zfile?style=flat-square">
|
||||
<br>
|
||||
<img src="https://img.shields.io/github/issues/zhaojun1998/zfile?style=flat-square">
|
||||
<img src="https://img.shields.io/github/issues-closed-raw/zhaojun1998/zfile?style=flat-square">
|
||||
<img src="https://img.shields.io/github/forks/zhaojun1998/zfile?style=flat-square">
|
||||
<img src="https://img.shields.io/github/stars/zhaojun1998/zfile?style=flat-square">
|
||||
<img src="https://img.shields.io/github/watchers/zhaojun1998/zfile?style=flat-square">
|
||||
</p>
|
||||
|
||||

|
||||
[](https://www.codacy.com/manual/zhaojun1998/zfile?utm_source=github.com&utm_medium=referral&utm_content=zhaojun1998/zfile&utm_campaign=Badge_Grade)
|
||||

|
||||

|
||||
|
||||
此项目是一个在线文件目录的程序, 支持各种对象存储和本地存储, 使用定位是个人放常用工具下载, 或做公共的文件库. 不会向多账户方向开发.
|
||||
|
||||
前端基于 [h5ai](https://larsjung.de/h5ai/) 的原有功能使用 Vue 重新开发了一遍. 后端采用 SpringBoot, 数据库采用内嵌数据库.
|
||||
## 相关地址
|
||||
|
||||
预览地址: [https://zfile.jun6.net](https://zfile.jun6.net)
|
||||
|
||||
文档地址: [http://docs.zhaojun.im/zfile](http://docs.zhaojun.im/zfile)
|
||||
|
||||
项目源码: [https://github.com/zhaojun1998/zfile](https://github.com/zhaojun1998/zfile)
|
||||
|
||||
前端源码: [https://github.com/zhaojun1998/zfile-vue](https://github.com/zhaojun1998/zfile-vue)
|
||||
|
||||
## 系统特色
|
||||
|
||||
* 内存缓存 (免安装)
|
||||
* 内存数据库 (免安装)
|
||||
* 个性化配置
|
||||
* 自定义目录的 readme 说明文件
|
||||
* 自定义 JS, CSS
|
||||
* 文件夹密码
|
||||
* 目录 README 说明
|
||||
* 文件直链(短链,永久直链,二维码)
|
||||
* 支持在线浏览文本文件, 视频, 图片, 音乐. (支持 FLV 和 HLS)
|
||||
* 文件/目录二维码
|
||||
* 图片模式
|
||||
* Docker 支持
|
||||
* 隐藏指定文件夹(通配符支持)
|
||||
* 自定义 JS, CSS
|
||||
* 自定义目录 README 说明文件和密码文件名称
|
||||
* 同时挂载多个存储策略
|
||||
* 缓存动态开启, ~~缓存自动刷新 (v2.2 及以前版本支持)~~
|
||||
* ~~全局搜索 (v2.2 及以前版本支持)~~
|
||||
* 同时挂载多个存储策略
|
||||
* 支持 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版, OneDrive 世纪互联版, 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
|
||||
* 支持 S3 协议, 阿里云 OSS, FTP, 华为云 OBS, 本地存储, MINIO, OneDrive 国际/家庭/个人版/世纪互联版/SharePoint, , 七牛云 KODO, 腾讯云 COS, 又拍云 USS.
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -96,28 +111,14 @@ chmod +x zfile/bin/*.sh
|
||||
|
||||
## 预览
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 常见问题
|
||||
|
||||
|
||||
### 默认路径
|
||||
|
||||
默认 H2 数据库文件地址: `~/.zfile/db/`, `~` 表示用户目录
|
||||
|
||||
windows 为 `C:/Users/用户名/`
|
||||
|
||||
linux 为 `/home/用户名/`, root 用户为 `/root/`
|
||||
|
||||
> 2.3 及以后版本路径为 `~/.zfile-new/db/`
|
||||
|
||||
### 文档文件和加密文件
|
||||
|
||||
- 目录文档显示文件名为 `readme.md`
|
||||
- 目录需要密码访问, 添加文件 `password.txt` (无法拦截此文件被下载, 但可以改名文件)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 开发计划
|
||||
|
||||
@@ -127,16 +128,35 @@ linux 为 `/home/用户名/`, root 用户为 `/root/`
|
||||
- [x] 后台优化 - 设置按照其功能进行分离
|
||||
- [x] 体验优化 - 支持前后端分离部署
|
||||
- [x] 体验优化 - 文本预览更换 vscode 同款编辑器 monaco editor
|
||||
- [x] 新功能 - Docker 支持
|
||||
- [x] 架构调整 - 支持多存储策略
|
||||
- [x] 体验优化 - 忽略文件列表 (正则表达式)
|
||||
- [ ] 新功能 - 后台支持上传、编辑、删除等操作
|
||||
- [x] 新功能 - Docker 支持
|
||||
- [x] 新功能 - 图片模式
|
||||
- [x] 新功能 - 直链/短链管理
|
||||
- [ ] ~~新功能 - 后台支持上传、编辑、删除等操作 (不再支持)~~
|
||||
- [ ] 体验优化 - 自定义支持预览的文件后缀 (正则表达式)
|
||||
- [ ] 体验优化 - 一键安装脚本
|
||||
- [ ] 新功能 - 分享功能,支持分享密码,文件夹分享
|
||||
- [ ] 新功能 - 直链支持 Referer 防盗链
|
||||
- [ ] 体验优化 - 视频列表支持
|
||||
- [ ] 新功能 - 单独页面打开文件预览
|
||||
- [ ] 新功能 - 在线查看日志功能
|
||||
- [ ] 部署优化 - Docker Compose 支持
|
||||
|
||||
## 支持作者
|
||||
|
||||
如果本项目对你有帮助,请作者喝杯咖啡吧。
|
||||
|
||||
<img src="http://cdn.jun6.net/alipay.png" width="200" height="312">
|
||||
<img src="http://cdn.jun6.net/wechat.png" width="222" height="300">
|
||||
<img src="https://cdn.jun6.net/2021/03/27/152704e91f13d.png" width="400" alt="赞助我">
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/zhaojun1998/zfile.svg)
|
||||
|
||||
## 服务器赞助
|
||||
|
||||
<a href="https://kuline.cn"><img src="https://cdn.jun6.net/2021/05/14/1f6a4f0ad09ce.png" width="100px"></a>
|
||||
|
||||
## 开发工具赞助
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=zfile"><img src="https://cdn.jun6.net/2021/04/21/26e410d60b0b0.png?1=1" width="100px"></a>
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -12,7 +12,7 @@
|
||||
|
||||
<groupId>im.zhaojun</groupId>
|
||||
<artifactId>zfile</artifactId>
|
||||
<version>3.0</version>
|
||||
<version>3.1</version>
|
||||
<name>zfile</name>
|
||||
<packaging>war</packaging>
|
||||
<description>一个在线的文件浏览系统</description>
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
@@ -26,14 +25,14 @@ public class LogController {
|
||||
* 系统日志下载
|
||||
*/
|
||||
@GetMapping("/log")
|
||||
public ResponseEntity<Object> downloadLog(HttpServletResponse response) {
|
||||
public ResponseEntity<Object> downloadLog() {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("下载诊断日志");
|
||||
}
|
||||
String userHome = System.getProperty("user.home");
|
||||
File fileZip = ZipUtil.zip(userHome + "/.zfile/logs");
|
||||
String currentDate = DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss");
|
||||
return FileUtil.export(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
|
||||
return FileUtil.exportSingleThread(fileZip, "ZFile 诊断日志 - " + currentDate + ".zip");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
package im.zhaojun.zfile.controller.home;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import im.zhaojun.zfile.exception.NotAllowedDownloadException;
|
||||
import im.zhaojun.zfile.exception.NotEnabledDriveException;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import im.zhaojun.zfile.model.enums.FileTypeEnum;
|
||||
import im.zhaojun.zfile.service.DriveConfigService;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.util.HttpUtil;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -16,6 +20,9 @@ import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -28,6 +35,9 @@ public class DirectLinkController {
|
||||
@Resource
|
||||
private DriveContext driveContext;
|
||||
|
||||
@Resource
|
||||
private DriveConfigService driveConfigService;
|
||||
|
||||
/**
|
||||
* 获取指定驱动器, 某个文件的直链, 然后重定向过去.
|
||||
* @param driveId
|
||||
@@ -35,9 +45,16 @@ public class DirectLinkController {
|
||||
*
|
||||
* @return 重定向至文件直链
|
||||
*/
|
||||
@GetMapping("/directlink/{driveId}/**")
|
||||
@GetMapping("/${zfile.directLinkPrefix}/{driveId}/**")
|
||||
public String directlink(@PathVariable("driveId") Integer driveId,
|
||||
final HttpServletRequest request) {
|
||||
final HttpServletRequest request,
|
||||
final HttpServletResponse response) throws IOException {
|
||||
DriveConfig driveConfig = driveConfigService.findById(driveId);
|
||||
Boolean enable = driveConfig.getEnable();
|
||||
if (!enable) {
|
||||
throw new NotEnabledDriveException();
|
||||
}
|
||||
|
||||
String path = (String) request.getAttribute(
|
||||
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
@@ -48,15 +65,21 @@ public class DirectLinkController {
|
||||
filePath = "/" + filePath;
|
||||
}
|
||||
|
||||
if (Objects.equals(FileUtil.getName(filePath), ZFileConstant.PASSWORD_FILE_NAME)) {
|
||||
throw new NotAllowedDownloadException("不允许下载此文件");
|
||||
}
|
||||
|
||||
AbstractBaseFileService fileService = driveContext.get(driveId);
|
||||
FileItemDTO fileItem = fileService.getFileItem(filePath);
|
||||
|
||||
String url = fileItem.getUrl();
|
||||
|
||||
if (StrUtil.equalsIgnoreCase(FileUtil.extName(fileItem.getName()), "m3u8")) {
|
||||
String textContent = HttpUtil.getTextContent(url);
|
||||
response.setContentType("application/vnd.apple.mpegurl;charset=utf-8");
|
||||
PrintWriter out = response.getWriter();
|
||||
out.write(textContent);
|
||||
out.flush();
|
||||
out.close();
|
||||
return null;
|
||||
}
|
||||
|
||||
int queryIndex = url.indexOf('?');
|
||||
|
||||
if (queryIndex != -1) {
|
||||
|
||||
@@ -3,9 +3,10 @@ package im.zhaojun.zfile.controller.home;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import im.zhaojun.zfile.exception.NotExistFileException;
|
||||
import im.zhaojun.zfile.exception.NotEnabledDriveException;
|
||||
import im.zhaojun.zfile.exception.PasswordVerifyException;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.DriveListDTO;
|
||||
import im.zhaojun.zfile.model.dto.FileItemDTO;
|
||||
import im.zhaojun.zfile.model.dto.FileListDTO;
|
||||
import im.zhaojun.zfile.model.dto.SystemFrontConfigDTO;
|
||||
@@ -67,7 +68,10 @@ public class FileController {
|
||||
*/
|
||||
@GetMapping("/drive/list")
|
||||
public ResultBean drives() {
|
||||
return ResultBean.success(driveConfigService.listOnlyEnable());
|
||||
List<DriveConfig> driveList = driveConfigService.listOnlyEnable();
|
||||
boolean isInstall = systemConfigService.getIsInstall();
|
||||
DriveListDTO driveListDTO = new DriveListDTO(driveList, isInstall);
|
||||
return ResultBean.success(driveListDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,8 +117,14 @@ public class FileController {
|
||||
// 开始获取参数信息
|
||||
SystemFrontConfigDTO systemConfig = systemConfigService.getSystemFrontConfig(driveId);
|
||||
DriveConfig driveConfig = driveConfigService.findById(driveId);
|
||||
Boolean enable = driveConfig.getEnable();
|
||||
if (!enable) {
|
||||
throw new NotEnabledDriveException();
|
||||
}
|
||||
|
||||
systemConfig.setDebugMode(debug);
|
||||
systemConfig.setDefaultSwitchToImgMode(driveConfig.getDefaultSwitchToImgMode());
|
||||
systemConfig.setDirectLinkPrefix(ZFileConstant.DIRECT_LINK_PREFIX);
|
||||
|
||||
// 如果不是 FTP 模式,则尝试获取当前文件夹中的 README 文件,有则读取,没有则停止
|
||||
if (!Objects.equals(driveConfig.getType(), StorageTypeEnum.FTP)) {
|
||||
@@ -131,24 +141,6 @@ public class FileController {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定路径下的文件信息内容
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @param path
|
||||
* 文件全路径
|
||||
*
|
||||
* @return 该文件的名称, 路径, 大小, 下载地址等信息.
|
||||
*/
|
||||
@GetMapping("/directlink/{driveId}")
|
||||
public ResultBean directlink(@PathVariable(name = "driveId") Integer driveId, String path) {
|
||||
AbstractBaseFileService fileService = driveContext.get(driveId);
|
||||
return ResultBean.successData(fileService.getFileItem(path));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验密码
|
||||
* @param fileItemList
|
||||
|
||||
@@ -5,7 +5,6 @@ import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.service.impl.LocalServiceImpl;
|
||||
import im.zhaojun.zfile.util.FileUtil;
|
||||
import im.zhaojun.zfile.util.StringUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -15,6 +14,7 @@ import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
@@ -32,18 +32,17 @@ public class LocalController {
|
||||
*
|
||||
* @param driveId
|
||||
* 驱动器 ID
|
||||
*
|
||||
* @return 文件
|
||||
*/
|
||||
@GetMapping("/file/{driveId}/**")
|
||||
@ResponseBody
|
||||
public ResponseEntity<Object> downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request) {
|
||||
public void downAttachment(@PathVariable("driveId") Integer driveId, final HttpServletRequest request, final HttpServletResponse response) {
|
||||
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
String bestMatchPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
|
||||
AntPathMatcher apm = new AntPathMatcher();
|
||||
String filePath = apm.extractPathWithinPattern(bestMatchPattern, path);
|
||||
LocalServiceImpl localService = (LocalServiceImpl) driveContext.get(driveId);
|
||||
return FileUtil.export(new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath)));
|
||||
File file = new File(StringUtils.removeDuplicateSeparator(localService.getFilePath() + ZFileConstant.PATH_SEPARATOR + filePath));
|
||||
FileUtil.export(request, response, file);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package im.zhaojun.zfile.controller.home;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.SystemConfigDTO;
|
||||
import im.zhaojun.zfile.model.entity.ShortLinkConfig;
|
||||
import im.zhaojun.zfile.model.support.ResultBean;
|
||||
@@ -36,7 +37,7 @@ public class ShortLinkController {
|
||||
SystemConfigDTO systemConfig = systemConfigService.getSystemConfig();
|
||||
String domain = systemConfig.getDomain();
|
||||
// 拼接直链地址.
|
||||
String fullPath = StringUtils.removeDuplicateSeparator("/directlink/" + driveId + path);
|
||||
String fullPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, driveId, path);
|
||||
ShortLinkConfig shortLinkConfig = shortLinkConfigService.findByUrl(fullPath);
|
||||
|
||||
if (shortLinkConfig == null) {
|
||||
|
||||
@@ -19,12 +19,22 @@ public class GlobleExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobleExceptionHandler.class);
|
||||
|
||||
|
||||
/**
|
||||
* 不存在的文件异常
|
||||
*/
|
||||
@ExceptionHandler({NotEnabledDriveException.class})
|
||||
@ResponseBody
|
||||
public ResultBean notEnabledDrive() {
|
||||
return ResultBean.error("驱动器已关闭");
|
||||
}
|
||||
|
||||
/**
|
||||
* 不存在的文件异常
|
||||
*/
|
||||
@ExceptionHandler({NotExistFileException.class})
|
||||
@ResponseBody
|
||||
public ResultBean notExistFile(Exception ex) {
|
||||
public ResultBean notExistFile() {
|
||||
return ResultBean.error("文件不存在");
|
||||
}
|
||||
|
||||
@@ -35,7 +45,7 @@ public class GlobleExceptionHandler {
|
||||
@ExceptionHandler({HttpMediaTypeNotAcceptableException.class, ClientAbortException.class})
|
||||
@ResponseBody
|
||||
@ResponseStatus
|
||||
public void clientAbortException(Exception ex) {
|
||||
public void clientAbortException() {
|
||||
// if (log.isDebugEnabled()) {
|
||||
// log.debug("出现了断开异常:", ex);
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package im.zhaojun.zfile.exception;
|
||||
|
||||
/**
|
||||
* 未启用的驱动器异常
|
||||
*/
|
||||
public class NotEnabledDriveException extends RuntimeException {
|
||||
|
||||
public NotEnabledDriveException() {
|
||||
}
|
||||
|
||||
public NotEnabledDriveException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public NotEnabledDriveException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public NotEnabledDriveException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public NotEnabledDriveException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -41,4 +41,6 @@ public class StorageConfigConstant {
|
||||
|
||||
public static final String IS_PRIVATE = "isPrivate";
|
||||
|
||||
public static final String PROXY_DOMAIN = "proxyDomain";
|
||||
|
||||
}
|
||||
@@ -16,6 +16,10 @@ public class ZFileConstant {
|
||||
|
||||
public static final String PATH_SEPARATOR = "/";
|
||||
|
||||
/**
|
||||
* 直链前缀名称
|
||||
*/
|
||||
public static String DIRECT_LINK_PREFIX = "directlink";
|
||||
|
||||
/**
|
||||
* 系统产生的临时文件路径
|
||||
@@ -68,5 +72,9 @@ public class ZFileConstant {
|
||||
ZFileConstant.TEXT_MAX_FILE_SIZE_KB = maxFileSizeKb;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setDirectLinkPrefix(@Value("${zfile.directLinkPrefix}") String directLinkPrefix) {
|
||||
ZFileConstant.DIRECT_LINK_PREFIX = directLinkPrefix;
|
||||
}
|
||||
|
||||
}
|
||||
21
src/main/java/im/zhaojun/zfile/model/dto/DriveListDTO.java
Normal file
21
src/main/java/im/zhaojun/zfile/model/dto/DriveListDTO.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package im.zhaojun.zfile.model.dto;
|
||||
|
||||
import im.zhaojun.zfile.model.entity.DriveConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Zhao Jun
|
||||
* 2021/5/26 15:17
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class DriveListDTO {
|
||||
|
||||
private List<DriveConfig> driveList;
|
||||
|
||||
private Boolean isInstall;
|
||||
|
||||
}
|
||||
@@ -46,4 +46,6 @@ public class StorageStrategyConfig {
|
||||
|
||||
private String siteType;
|
||||
|
||||
private String proxyDomain;
|
||||
|
||||
}
|
||||
@@ -46,4 +46,10 @@ public class SystemConfigDTO {
|
||||
private Boolean showAnnouncement;
|
||||
|
||||
private String layout;
|
||||
|
||||
private Boolean showLinkBtn;
|
||||
|
||||
private Boolean showShortLink;
|
||||
|
||||
private Boolean showPathLink;
|
||||
}
|
||||
@@ -45,4 +45,12 @@ public class SystemFrontConfigDTO {
|
||||
|
||||
private Boolean defaultSwitchToImgMode;
|
||||
|
||||
private Boolean showLinkBtn;
|
||||
|
||||
private Boolean showShortLink;
|
||||
|
||||
private Boolean showPathLink;
|
||||
|
||||
private String directLinkPrefix;
|
||||
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package im.zhaojun.zfile.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import im.zhaojun.zfile.cache.ZFileCache;
|
||||
import im.zhaojun.zfile.context.DriveContext;
|
||||
import im.zhaojun.zfile.context.StorageTypeContext;
|
||||
import im.zhaojun.zfile.exception.InitializeDriveException;
|
||||
import im.zhaojun.zfile.model.constant.StorageConfigConstant;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.model.dto.CacheInfoDTO;
|
||||
import im.zhaojun.zfile.model.dto.DriveConfigDTO;
|
||||
import im.zhaojun.zfile.model.dto.StorageStrategyConfig;
|
||||
@@ -18,6 +18,7 @@ import im.zhaojun.zfile.repository.FilterConfigRepository;
|
||||
import im.zhaojun.zfile.repository.ShortLinkConfigRepository;
|
||||
import im.zhaojun.zfile.repository.StorageConfigRepository;
|
||||
import im.zhaojun.zfile.service.base.AbstractBaseFileService;
|
||||
import im.zhaojun.zfile.util.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.domain.Example;
|
||||
@@ -192,22 +193,8 @@ public class DriveConfigService {
|
||||
|
||||
AbstractBaseFileService storageTypeService = StorageTypeContext.getStorageTypeService(storageType);
|
||||
|
||||
List<StorageConfig> storageConfigList;
|
||||
if (updateFlag) {
|
||||
storageConfigList = storageConfigRepository.findByDriveId(driveConfigDTO.getId());
|
||||
// 如果从数据库获取到的数据不为空, 则校验数据是否和当前存储类型一直, 如不一直则进行矫正.
|
||||
if (CollectionUtil.isNotEmpty(storageConfigList)) {
|
||||
StorageConfig storageConfig = storageConfigList.get(0);
|
||||
StorageTypeEnum type = storageConfig.getType();
|
||||
|
||||
if (!Objects.equals(type, storageType)) {
|
||||
storageConfigRepository.deleteByDriveId(driveConfigDTO.getId());
|
||||
storageConfigList = storageTypeService.storageStrategyConfigList();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
storageConfigList = storageTypeService.storageStrategyConfigList();
|
||||
}
|
||||
List<StorageConfig> storageConfigList = storageTypeService.storageStrategyConfigList();
|
||||
storageConfigRepository.deleteByDriveId(driveConfigDTO.getId());
|
||||
|
||||
for (StorageConfig storageConfig : storageConfigList) {
|
||||
String key = storageConfig.getKey();
|
||||
@@ -251,10 +238,10 @@ public class DriveConfigService {
|
||||
public Integer selectNextId() {
|
||||
Integer maxId = driverConfigRepository.selectMaxId();
|
||||
if (maxId == null) {
|
||||
maxId = 1;
|
||||
return 1;
|
||||
} else {
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
return maxId + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +260,12 @@ public class DriveConfigService {
|
||||
driverConfigRepository.updateId(updateId, newId);
|
||||
storageConfigRepository.updateDriveId(updateId, newId);
|
||||
filterConfigRepository.updateDriveId(updateId, newId);
|
||||
shortLinkConfigRepository.updateUrlDriveId("/directlink/" + updateId, "/directlink/" + newId);
|
||||
|
||||
|
||||
String updateSubPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, String.valueOf(updateId));
|
||||
String newSubPath = StringUtils.concatUrl(StringUtils.DELIMITER_STR, ZFileConstant.DIRECT_LINK_PREFIX, String.valueOf(newId));
|
||||
|
||||
shortLinkConfigRepository.updateUrlDriveId(updateSubPath, newSubPath);
|
||||
driveContext.updateDriveId(updateId, newId);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package im.zhaojun.zfile.service;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import im.zhaojun.zfile.cache.ZFileCache;
|
||||
import im.zhaojun.zfile.exception.InvalidDriveException;
|
||||
@@ -163,4 +164,24 @@ public class SystemConfigService {
|
||||
return systemConfigDTO.getUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取站点域名
|
||||
*
|
||||
* @return 站点域名
|
||||
*/
|
||||
public String getDomain() {
|
||||
SystemConfigDTO systemConfigDTO = getSystemConfig();
|
||||
return systemConfigDTO.getDomain();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取是否已安装初始化
|
||||
*
|
||||
* @return 是否已安装初始化
|
||||
*/
|
||||
public boolean getIsInstall() {
|
||||
SystemConfigDTO systemConfigDTO = getSystemConfig();
|
||||
return StrUtil.isNotEmpty(systemConfigDTO.getUsername());
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public abstract class AbstractOneDriveServiceBase extends MicrosoftDriveServiceB
|
||||
add(new StorageConfig("accessToken", "访问令牌"));
|
||||
add(new StorageConfig("refreshToken", "刷新令牌"));
|
||||
add(new StorageConfig("basePath", "基路径"));
|
||||
add(new StorageConfig("proxyDomain", "反代域名"));
|
||||
}};
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,11 @@ public abstract class AbstractS3BaseFileService extends AbstractBaseFileService
|
||||
fileItemDTO.setTime(s.getLastModified());
|
||||
fileItemDTO.setType(FileTypeEnum.FILE);
|
||||
fileItemDTO.setPath(path);
|
||||
fileItemDTO.setUrl(getDownloadUrl(StringUtils.concatUrl(path, fileItemDTO.getName())));
|
||||
|
||||
String fullPathAndName = StringUtils.concatUrl(path, fileItemDTO.getName());
|
||||
String directlink = StringUtils.generatorLink(driveId, fullPathAndName);
|
||||
fileItemDTO.setUrl(directlink);
|
||||
|
||||
fileItemList.add(fileItemDTO);
|
||||
}
|
||||
|
||||
@@ -129,6 +133,7 @@ public abstract class AbstractS3BaseFileService extends AbstractBaseFileService
|
||||
for (FileItemDTO fileItemDTO : list) {
|
||||
String fullPath = StringUtils.concatUrl(fileItemDTO.getPath(), fileItemDTO.getName());
|
||||
if (Objects.equals(fullPath, path)) {
|
||||
fileItemDTO.setUrl(getDownloadUrl(path));
|
||||
return fileItemDTO;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public abstract class AbstractSharePointServiceBase extends MicrosoftDriveServic
|
||||
return new ArrayList<StorageConfig>() {{
|
||||
add(new StorageConfig("accessToken", "访问令牌"));
|
||||
add(new StorageConfig("refreshToken", "刷新令牌"));
|
||||
add(new StorageConfig("proxyDomain", "反代域名"));
|
||||
add(new StorageConfig("basePath", "基路径"));
|
||||
add(new StorageConfig("siteName", "站点名称"));
|
||||
add(new StorageConfig("siteId", "SiteId"));
|
||||
|
||||
@@ -32,12 +32,6 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService {
|
||||
|
||||
|
||||
/**
|
||||
* https://graph.microsoft.com/v1.0/drives/6e4cf6d2f7e15197/root:%2FData%2F%E5%B8%A6%E6%9C%89%E7%AC%A6%E5%8F%B7%E6%96%87%E4%BB%B6%E5%A4%B9%E6%B5%8B%E8%AF%95%2F%25100+%2520:/children
|
||||
* https://graph.microsoft.com/v1.0/me/drive/root:%2FData%2F%E5%B8%A6%E6%9C%89%E7%AC%A6%E5%8F%B7%E6%96%87%E4%BB%B6%E5%A4%B9%E6%B5%8B%E8%AF%95%2F%25100+%2520:/children
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取根文件 API URI
|
||||
*/
|
||||
@@ -63,6 +57,7 @@ public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService
|
||||
*/
|
||||
private static final String ONE_DRIVE_FILE_FLAG = "file";
|
||||
|
||||
protected String proxyDomain;
|
||||
|
||||
@Resource
|
||||
@Lazy
|
||||
@@ -152,7 +147,7 @@ public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService
|
||||
try {
|
||||
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.debug("调用 OneDrive 时出现了网络异常, 已尝试重新刷新 token 后再试.");
|
||||
log.debug("调用 OneDrive 时出现了网络异常, 响应信息: {}, 已尝试重新刷新 token 后再试.", e.getResponseBodyAsString());
|
||||
refreshOneDriveToken();
|
||||
root = oneDriveRestTemplate.exchange(requestUrl, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
|
||||
}
|
||||
@@ -174,7 +169,11 @@ public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService
|
||||
fileItemDTO.setTime(fileItem.getDate("lastModifiedDateTime"));
|
||||
|
||||
if (fileItem.containsKey("file")) {
|
||||
fileItemDTO.setUrl(fileItem.getString("@microsoft.graph.downloadUrl"));
|
||||
String originUrl = fileItem.getString("@microsoft.graph.downloadUrl");
|
||||
if (StringUtils.isNotNullOrEmpty(proxyDomain)) {
|
||||
originUrl = StringUtils.replaceHost(originUrl, proxyDomain);
|
||||
}
|
||||
fileItemDTO.setUrl(originUrl);
|
||||
fileItemDTO.setType(FileTypeEnum.FILE);
|
||||
} else {
|
||||
fileItemDTO.setType(FileTypeEnum.FOLDER);
|
||||
@@ -202,7 +201,7 @@ public abstract class MicrosoftDriveServiceBase extends AbstractBaseFileService
|
||||
try {
|
||||
fileItem = oneDriveRestTemplate.exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.debug("调用 OneDrive 时出现了网络异常, 已尝试重新刷新 token 后再试.");
|
||||
log.debug("调用 OneDrive 时出现了网络异常, 响应信息: {}, 已尝试重新刷新 token 后再试.", e.getResponseBodyAsString());
|
||||
refreshOneDriveToken();
|
||||
fileItem = oneDriveRestTemplate.exchange(DRIVER_ITEM_URL, HttpMethod.GET, entity, JSONObject.class, getGraphEndPoint(), getType(), fullPath).getBody();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import im.zhaojun.zfile.service.base.BaseFileService;
|
||||
import im.zhaojun.zfile.util.StringUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.net.ftp.FTP;
|
||||
import org.apache.commons.net.ftp.FTPClientConfig;
|
||||
import org.apache.commons.net.ftp.FTPFile;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
@@ -65,7 +64,6 @@ public class FtpServiceImpl extends AbstractBaseFileService implements BaseFileS
|
||||
isInitialized = false;
|
||||
} else {
|
||||
ftp = new Ftp(host, Integer.parseInt(port), username, password, StandardCharsets.UTF_8);
|
||||
ftp.getClient().configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX));
|
||||
ftp.getClient().type(FTP.BINARY_FILE_TYPE);
|
||||
testConnection();
|
||||
isInitialized = true;
|
||||
@@ -83,7 +81,6 @@ public class FtpServiceImpl extends AbstractBaseFileService implements BaseFileS
|
||||
ftpFiles = ftp.getClient().listFiles(fullPath);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// ignore
|
||||
}
|
||||
|
||||
List<FileItemDTO> fileItemList = new ArrayList<>();
|
||||
|
||||
@@ -47,6 +47,11 @@ public class OneDriveChinaServiceImpl extends AbstractOneDriveServiceBase implem
|
||||
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
|
||||
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
|
||||
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
|
||||
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
|
||||
if (proxyDomainStorageConfig != null) {
|
||||
super.proxyDomain = proxyDomainStorageConfig.getValue();
|
||||
}
|
||||
|
||||
|
||||
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
|
||||
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
|
||||
|
||||
@@ -47,6 +47,10 @@ public class OneDriveServiceImpl extends AbstractOneDriveServiceBase implements
|
||||
String accessToken = stringStorageConfigMap.get(StorageConfigConstant.ACCESS_TOKEN_KEY).getValue();
|
||||
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
|
||||
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
|
||||
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
|
||||
if (proxyDomainStorageConfig != null) {
|
||||
super.proxyDomain = proxyDomainStorageConfig.getValue();
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
|
||||
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
|
||||
|
||||
@@ -48,6 +48,10 @@ public class SharePointChinaServiceImpl extends AbstractSharePointServiceBase im
|
||||
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
|
||||
super.siteId = stringStorageConfigMap.get(StorageConfigConstant.SHAREPOINT_SITE_ID).getValue();
|
||||
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
|
||||
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
|
||||
if (proxyDomainStorageConfig != null) {
|
||||
super.proxyDomain = proxyDomainStorageConfig.getValue();
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
|
||||
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
|
||||
|
||||
@@ -48,6 +48,10 @@ public class SharePointServiceImpl extends AbstractSharePointServiceBase impleme
|
||||
String refreshToken = stringStorageConfigMap.get(StorageConfigConstant.REFRESH_TOKEN_KEY).getValue();
|
||||
super.siteId = stringStorageConfigMap.get(StorageConfigConstant.SHAREPOINT_SITE_ID).getValue();
|
||||
super.basePath = stringStorageConfigMap.get(StorageConfigConstant.BASE_PATH).getValue();
|
||||
StorageConfig proxyDomainStorageConfig = stringStorageConfigMap.get(StorageConfigConstant.PROXY_DOMAIN);
|
||||
if (proxyDomainStorageConfig != null) {
|
||||
super.proxyDomain = proxyDomainStorageConfig.getValue();
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(refreshToken)) {
|
||||
log.debug("初始化存储策略 [{}] 失败: 参数不完整", getStorageTypeEnum().getDescription());
|
||||
|
||||
@@ -1,21 +1,35 @@
|
||||
package im.zhaojun.zfile.util;
|
||||
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileUtil {
|
||||
|
||||
public static ResponseEntity<Object> export(File file, String fileName) {
|
||||
/**
|
||||
* 文件下载,单线程,直接传
|
||||
* @param file 文件对象
|
||||
* @param fileName 要保存为的文件名
|
||||
* @return 文件下载对象
|
||||
*/
|
||||
public static ResponseEntity<Object> exportSingleThread(File file, String fileName) {
|
||||
if (!file.exists()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("404 FILE NOT FOUND");
|
||||
}
|
||||
@@ -23,7 +37,7 @@ public class FileUtil {
|
||||
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
headers.add(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
|
||||
|
||||
if (StringUtils.isNullOrEmpty(fileName)) {
|
||||
fileName = file.getName();
|
||||
@@ -31,10 +45,10 @@ public class FileUtil {
|
||||
|
||||
headers.setContentDispositionFormData("attachment", URLUtil.encode(fileName));
|
||||
|
||||
headers.add("Pragma", "no-cache");
|
||||
headers.add("Expires", "0");
|
||||
headers.add("Last-Modified", new Date().toString());
|
||||
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
|
||||
headers.add(HttpHeaders.PRAGMA, "no-cache");
|
||||
headers.add(HttpHeaders.EXPIRES, "0");
|
||||
headers.add(HttpHeaders.LAST_MODIFIED, new Date().toString());
|
||||
headers.add(HttpHeaders.ETAG, String.valueOf(System.currentTimeMillis()));
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers(headers)
|
||||
@@ -43,26 +57,133 @@ public class FileUtil {
|
||||
.body(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
public static ResponseEntity<Object> export(File file) {
|
||||
/**
|
||||
* 返回文件给 response,支持断点续传和多线程下载
|
||||
* @param request 请求对象
|
||||
* @param response 响应对象
|
||||
* @param file 下载的文件
|
||||
*/
|
||||
public static void export(HttpServletRequest request, HttpServletResponse response, File file) {
|
||||
export(request, response, file, file.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回文件给 response,支持断点续传和多线程下载
|
||||
* @param request 请求对象
|
||||
* @param response 响应对象
|
||||
* @param file 下载的文件
|
||||
* @param fileName 下载的文件名,为空则默认读取文件名称
|
||||
*/
|
||||
public static void export(HttpServletRequest request, HttpServletResponse response, File file, String fileName) {
|
||||
if (!file.exists()) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("404 FILE NOT FOUND");
|
||||
try {
|
||||
response.getWriter().write("404 FILE NOT FOUND");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM;
|
||||
if (StringUtils.isNullOrEmpty(fileName)) {
|
||||
//文件名
|
||||
fileName = file.getName();
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
headers.setContentDispositionFormData("attachment", URLUtil.encode(file.getName()));
|
||||
String range = request.getHeader(HttpHeaders.RANGE);
|
||||
|
||||
headers.add("Pragma", "no-cache");
|
||||
headers.add("Expires", "0");
|
||||
headers.add("Last-Modified", new Date().toString());
|
||||
headers.add("ETag", String.valueOf(System.currentTimeMillis()));
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.headers(headers)
|
||||
.contentLength(file.length())
|
||||
.contentType(mediaType)
|
||||
.body(new FileSystemResource(file));
|
||||
String rangeSeparator = "-";
|
||||
// 开始下载位置
|
||||
long startByte = 0;
|
||||
// 结束下载位置
|
||||
long endByte = file.length() - 1;
|
||||
|
||||
// 如果是断点续传
|
||||
if (range != null && range.contains("bytes=") && range.contains(rangeSeparator)) {
|
||||
// 设置响应状态码为 206
|
||||
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
|
||||
|
||||
range = range.substring(range.lastIndexOf("=") + 1).trim();
|
||||
String[] ranges = range.split(rangeSeparator);
|
||||
try {
|
||||
// 判断 range 的类型
|
||||
if (ranges.length == 1) {
|
||||
// 类型一:bytes=-2343
|
||||
if (range.startsWith(rangeSeparator)) {
|
||||
endByte = Long.parseLong(ranges[0]);
|
||||
}
|
||||
// 类型二:bytes=2343-
|
||||
else if (range.endsWith(rangeSeparator)) {
|
||||
startByte = Long.parseLong(ranges[0]);
|
||||
}
|
||||
}
|
||||
// 类型三:bytes=22-2343
|
||||
else if (ranges.length == 2) {
|
||||
startByte = Long.parseLong(ranges[0]);
|
||||
endByte = Long.parseLong(ranges[1]);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 传参不规范,则直接返回所有内容
|
||||
startByte = 0;
|
||||
endByte = file.length() - 1;
|
||||
}
|
||||
} else {
|
||||
// 没有 ranges 即全部一次性传输,需要用 200 状态码,这一行应该可以省掉,因为默认返回是 200 状态码
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
|
||||
//要下载的长度(endByte 为总长度 -1,这时候要加回去)
|
||||
long contentLength = endByte - startByte + 1;
|
||||
//文件类型
|
||||
String contentType = request.getServletContext().getMimeType(fileName);
|
||||
|
||||
response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
|
||||
response.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
// 这里文件名换你想要的,inline 表示浏览器可以直接使用
|
||||
// 参考资料:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
|
||||
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + URLUtil.encode(fileName));
|
||||
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
|
||||
// [要下载的开始位置]-[结束位置]/[文件总大小]
|
||||
response.setHeader(HttpHeaders.CONTENT_RANGE, "bytes " + startByte + rangeSeparator + endByte + "/" + file.length());
|
||||
|
||||
BufferedOutputStream outputStream;
|
||||
RandomAccessFile randomAccessFile = null;
|
||||
//已传送数据大小
|
||||
long transmitted = 0;
|
||||
try {
|
||||
randomAccessFile = new RandomAccessFile(file, "r");
|
||||
outputStream = new BufferedOutputStream(response.getOutputStream());
|
||||
byte[] buff = new byte[4096];
|
||||
int len = 0;
|
||||
randomAccessFile.seek(startByte);
|
||||
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
|
||||
outputStream.write(buff, 0, len);
|
||||
transmitted += len;
|
||||
// 本地测试, 防止下载速度过快
|
||||
// Thread.sleep(1);
|
||||
}
|
||||
// 处理不足 buff.length 部分
|
||||
if (transmitted < contentLength) {
|
||||
len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
|
||||
outputStream.write(buff, 0, len);
|
||||
transmitted += len;
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
response.flushBuffer();
|
||||
randomAccessFile.close();
|
||||
// log.trace("下载完毕: {}-{}, 已传输 {}", startByte, endByte, transmitted);
|
||||
} catch (ClientAbortException e) {
|
||||
// ignore 用户停止下载
|
||||
// log.trace("用户停止下载: {}-{}, 已传输 {}", startByte, endByte, transmitted);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (randomAccessFile != null) {
|
||||
randomAccessFile.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class HttpUtil {
|
||||
try {
|
||||
result = restTemplate.getForObject(url, String.class);
|
||||
} catch (Exception e) {
|
||||
throw new TextParseException("文件解析异常");
|
||||
throw new TextParseException("文件解析异常, 请求 url = " + url + ", 异常信息为 = " + e.getMessage());
|
||||
}
|
||||
|
||||
return result == null ? "" : result;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package im.zhaojun.zfile.util;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.URLUtil;
|
||||
import im.zhaojun.zfile.model.constant.ZFileConstant;
|
||||
import im.zhaojun.zfile.service.SystemConfigService;
|
||||
|
||||
/**
|
||||
* @author zhaojun
|
||||
@@ -11,6 +13,8 @@ public class StringUtils {
|
||||
|
||||
public static final char DELIMITER = '/';
|
||||
|
||||
public static final String DELIMITER_STR = "/";
|
||||
|
||||
public static final String HTTP_PROTOCAL = "http://";
|
||||
|
||||
public static final String HTTPS_PROTOCAL = "https://";
|
||||
@@ -101,4 +105,44 @@ public class StringUtils {
|
||||
path = ObjectUtil.defaultIfNull(path, "");
|
||||
return StringUtils.removeDuplicateSeparator(basePath + ZFileConstant.PATH_SEPARATOR + path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 URL 中的 Host 部分,如替换 http://a.com/1.txt 为 https://abc.com/1.txt
|
||||
* @param originUrl
|
||||
* 原 URL
|
||||
* @param replaceHost
|
||||
* 替换的 HOST
|
||||
* @return 替换后的 URL
|
||||
*/
|
||||
public static String replaceHost(String originUrl, String replaceHost) {
|
||||
return concatPath(replaceHost, URLUtil.getPath(originUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接 URL,并去除重复的分隔符 '/',但不会影响 http:// 和 https:// 这种头部
|
||||
* @param strs 拼接的字符数组
|
||||
* @return 拼接结果
|
||||
*/
|
||||
public static String concatUrl(String... strs) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < strs.length; i++) {
|
||||
sb.append(strs[i]);
|
||||
if (i != strs.length - 1) {
|
||||
sb.append(DELIMITER);
|
||||
}
|
||||
}
|
||||
return removeDuplicateSeparator(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼接文件直链生成 URL
|
||||
* @param driveId 驱动器 ID
|
||||
* @param fullPath 文件全路径
|
||||
* @return 生成结果
|
||||
*/
|
||||
public static String generatorLink(Integer driveId, String fullPath) {
|
||||
SystemConfigService systemConfigService = SpringContextHolder.getBean(SystemConfigService.class);
|
||||
String domain = systemConfigService.getDomain();
|
||||
return concatUrl(domain, ZFileConstant.DIRECT_LINK_PREFIX, String.valueOf(driveId), fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,12 @@
|
||||
"name": "zfile.debug",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "是否开启 debug 模式."
|
||||
},
|
||||
{
|
||||
"name": "zfile.directLinkPrefix",
|
||||
"type": "java.lang.String",
|
||||
"defaultValue": "directlink",
|
||||
"description": "直链前缀名称, 默认为 directlink"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
zfile:
|
||||
debug: false
|
||||
directLinkPrefix: directlink
|
||||
log:
|
||||
path: ${user.home}/.zfile/logs
|
||||
db:
|
||||
|
||||
7
src/main/resources/banner.txt
Normal file
7
src/main/resources/banner.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
________ ________ ___ ___ _______
|
||||
|\_____ \|\ _____\\ \|\ \ |\ ___ \
|
||||
\|___/ /\ \ \__/\ \ \ \ \ \ \ ___
|
||||
/ / /\ \ __\\ \ \ \ \ \ \ ___\
|
||||
/ /_/__\ \ \_| \ \ \ \ \____\ \ _____
|
||||
|\________\ \__\ \ \__\ \_______\ \_______\
|
||||
\|_______|\|__| \|__|\|_______|\|_______|
|
||||
@@ -5,9 +5,12 @@ INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (7, 'password', '管理
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (8, 'domain', '站点域名');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (11, 'customCss', '自定义 CSS');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (12, 'customJs', '自定义 JS (可用于统计代码)');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (13, 'tableSize', '表格大小');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (14, 'showOperator', '是否显示操作按钮');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (15, 'showDocument', '是否显示文档');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (13, 'tableSize', '表格大小', 'mini');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (14, 'showOperator', '是否显示操作按钮', 'true');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (15, 'showDocument', '是否显示文档', 'true');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (16, 'announcement', '网站公告');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (17, 'showAnnouncement', '是否显示网站公告');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`) VALUES (18, 'layout', '页面布局');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (17, 'showAnnouncement', '是否显示网站公告', 'true');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (18, 'layout', '页面布局', 'full');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (19, 'showLinkBtn', '是否显示生成直链按钮', 'true');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (20, 'showShortLink', '是否显示短链', 'true');
|
||||
INSERT INTO SYSTEM_CONFIG (`ID`, `k`, `REMARK`, `value`) VALUES (21, 'showPathLink', '是否显示路径直链', 'true');
|
||||
@@ -1 +1 @@
|
||||
.zfile-admin-index[data-v-158be3bf]{height:100%;overflow-y:hidden}.zfile-admin-top[data-v-158be3bf]{background-color:#001529}.zfile-admin-top-logo[data-v-158be3bf]{cursor:pointer;height:100%;line-height:61px;color:#fff;padding-right:20px}.zfile-admin-top-logo[data-v-158be3bf]:hover{color:#1890ff}.zfile-admin-top-content[data-v-158be3bf]{display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;max-width:1200px}.zfile-admin-content[data-v-158be3bf]{background-color:#f0f2f5;height:100%;overflow-y:auto}.zfile-admin-content-view[data-v-158be3bf]{margin:auto;max-width:1200px;margin-top:50px;margin-bottom:100px}.el-menu--horizontal>.el-menu-item.is-active[data-v-158be3bf]{color:#1890ff!important}.el-menu--horizontal>.el-menu-item[data-v-158be3bf],.el-menu.el-menu--horizontal[data-v-158be3bf]{border:none}.zfile-admin-index-version-info[data-v-158be3bf],.zfile-admin-index-version-info[data-v-158be3bf] .el-link--inner{font-size:13px}.zfile-login[data-v-085cced0]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;height:100%}.zfile-login-title[data-v-085cced0]{text-align:center;vertical-align:text-bottom;font-size:30px;font-weight:600;color:red;background-image:linear-gradient(-20deg,#6e45e2,#88d3ce);-webkit-text-fill-color:transparent;-webkit-background-clip:text;line-height:80px}.zfile-login-title small[data-v-085cced0]{font-size:20px}.zfile-login-form[data-v-085cced0]{width:350px;padding:30px 35px 15px;background:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 0 25px #cac6c6;box-shadow:0 0 25px #cac6c6}.el-row[data-v-875b6660]{overflow-y:auto}#siteForm[data-v-875b6660]{margin-top:20px}#siteForm[data-v-875b6660] .el-select{width:70%}.markdown-body[data-v-fad21930]{padding:unset}.el-drive-form-col[data-v-6669110c]{padding-left:0!important}.zfile-site-id-input-site-type-select[data-v-6669110c]{width:100px}.zfile-cache-statistics[data-v-f4e53df0]{margin-bottom:20px}.zfile-cache-table[data-v-f4e53df0]{width:100%;overflow-y:auto}.zfile-filter-delete-btn[data-v-e3077b30]{margin-left:10px;margin-top:5px}.zfile-filter-save-btn[data-v-e3077b30]{text-align:right;margin-top:15px}.el-row[data-v-4162da7e]{padding:20px}.el-form-item[data-v-4162da7e]{margin-right:50px}.card-title[data-v-4162da7e]{color:rgba(0,0,0,.45);font-size:14px}.card-content[data-v-4162da7e]{color:rgba(0,0,0,.85);font-size:25px;line-height:30px}.card-title-button[data-v-4162da7e]{float:right;padding:3px 0}.table-search-input[data-v-4162da7e]{width:300px;float:right}#filterForm .el-row[data-v-4162da7e]{padding:0}#cacheDialog[data-v-4162da7e] .el-dialog__body{padding:20px}.table-edit-icon[data-v-4162da7e]{margin-left:5px;color:#409eff;cursor:pointer}.zfile-admin-short-form[data-v-1513b0ea] .el-form-item:first-child{margin-left:10px}.zfile-admin-short-form[data-v-1513b0ea] .el-form-item:not(:first-child){margin-left:20px}.el-pagination[data-v-1513b0ea]{margin-top:15px}.table-edit-icon[data-v-1513b0ea]{margin-left:10px;cursor:pointer}.input-with-select .el-select[data-v-3ac09bd8]{width:100px}
|
||||
.zfile-admin-index[data-v-158be3bf]{height:100%;overflow-y:hidden}.zfile-admin-top[data-v-158be3bf]{background-color:#001529}.zfile-admin-top-logo[data-v-158be3bf]{cursor:pointer;height:100%;line-height:61px;color:#fff;padding-right:20px}.zfile-admin-top-logo[data-v-158be3bf]:hover{color:#1890ff}.zfile-admin-top-content[data-v-158be3bf]{display:-webkit-box;display:-ms-flexbox;display:flex;margin:auto;max-width:1200px}.zfile-admin-content[data-v-158be3bf]{background-color:#f0f2f5;height:100%;overflow-y:auto}.zfile-admin-content-view[data-v-158be3bf]{margin:auto;max-width:1200px;margin-top:50px;margin-bottom:100px}.el-menu--horizontal>.el-menu-item.is-active[data-v-158be3bf]{color:#1890ff!important}.el-menu--horizontal>.el-menu-item[data-v-158be3bf],.el-menu.el-menu--horizontal[data-v-158be3bf]{border:none}.zfile-admin-index-version-info[data-v-158be3bf],.zfile-admin-index-version-info[data-v-158be3bf] .el-link--inner{font-size:13px}.zfile-login[data-v-085cced0]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;height:100%}.zfile-login-title[data-v-085cced0]{text-align:center;vertical-align:text-bottom;font-size:30px;font-weight:600;color:red;background-image:linear-gradient(-20deg,#6e45e2,#88d3ce);-webkit-text-fill-color:transparent;-webkit-background-clip:text;line-height:80px}.zfile-login-title small[data-v-085cced0]{font-size:20px}.zfile-login-form[data-v-085cced0]{width:350px;padding:30px 35px 15px;background:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 0 25px #cac6c6;box-shadow:0 0 25px #cac6c6}.el-row[data-v-875b6660]{overflow-y:auto}#siteForm[data-v-875b6660]{margin-top:20px}#siteForm[data-v-875b6660] .el-select{width:70%}.markdown-body[data-v-fad21930]{padding:unset}.el-drive-form-col[data-v-da51cbbe]{padding-left:0!important}.zfile-site-id-input-site-type-select[data-v-da51cbbe]{width:100px}.zfile-cache-statistics[data-v-f4e53df0]{margin-bottom:20px}.zfile-cache-table[data-v-f4e53df0]{width:100%;overflow-y:auto}.zfile-filter-delete-btn[data-v-e3077b30]{margin-left:10px;margin-top:5px}.zfile-filter-save-btn[data-v-e3077b30]{text-align:right;margin-top:15px}.el-row[data-v-6b6f5036]{padding:20px}.el-form-item[data-v-6b6f5036]{margin-right:50px}.card-title[data-v-6b6f5036]{color:rgba(0,0,0,.45);font-size:14px}.card-content[data-v-6b6f5036]{color:rgba(0,0,0,.85);font-size:25px;line-height:30px}.card-title-button[data-v-6b6f5036]{float:right;padding:3px 0}.table-search-input[data-v-6b6f5036]{width:300px;float:right}#filterForm .el-row[data-v-6b6f5036]{padding:0}#cacheDialog[data-v-6b6f5036] .el-dialog__body{padding:20px}.table-edit-icon[data-v-6b6f5036]{margin-left:5px;color:#409eff;cursor:pointer}.zfile-admin-short-form[data-v-1513b0ea] .el-form-item:first-child{margin-left:10px}.zfile-admin-short-form[data-v-1513b0ea] .el-form-item:not(:first-child){margin-left:20px}.el-pagination[data-v-1513b0ea]{margin-top:15px}.table-edit-icon[data-v-1513b0ea]{margin-left:10px;cursor:pointer}.input-with-select .el-select[data-v-3ac09bd8]{width:100px}
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
.zfile-install[data-v-3e291e0d]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;height:100%}.zfile-install-form[data-v-3e291e0d]{width:450px;padding:30px 35px 15px;background:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 0 15px #cac6c6;box-shadow:0 0 15px #cac6c6}.zfile-install-title[data-v-3e291e0d]{text-align:center;vertical-align:text-bottom;font-size:30px;font-weight:600;color:red;background-image:linear-gradient(-20deg,#6e45e2,#88d3ce);-webkit-text-fill-color:transparent;-webkit-background-clip:text;line-height:80px}.zfile-install-enter[data-v-3e291e0d]{text-align:right;margin-bottom:0}.zfile-install-title small[data-v-3e291e0d]{font-size:20px}
|
||||
.zfile-install[data-v-66213dd3]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;height:100%}.zfile-install-form[data-v-66213dd3]{width:450px;padding:30px 35px 15px;background:#fff;border:1px solid #eaeaea;-webkit-box-shadow:0 0 15px #cac6c6;box-shadow:0 0 15px #cac6c6}.zfile-install-title[data-v-66213dd3]{text-align:center;vertical-align:text-bottom;font-size:30px;font-weight:600;color:red;background-image:linear-gradient(-20deg,#6e45e2,#88d3ce);-webkit-text-fill-color:transparent;-webkit-background-clip:text;line-height:80px}.zfile-install-enter[data-v-66213dd3]{text-align:right;margin-bottom:0}.zfile-install-title small[data-v-66213dd3]{font-size:20px}
|
||||
4
src/main/resources/static/js/admin.min.js
vendored
4
src/main/resources/static/js/admin.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
src/main/resources/static/js/front.min.js
vendored
2
src/main/resources/static/js/front.min.js
vendored
File diff suppressed because one or more lines are too long
2
src/main/resources/static/js/install.min.js
vendored
2
src/main/resources/static/js/install.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user