mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 00:14:56 +00:00
815 lines
32 KiB
Java
815 lines
32 KiB
Java
package cn.keking.web.controller;
|
||
|
||
import cn.keking.config.ConfigConstants;
|
||
import cn.keking.model.FileType;
|
||
import cn.keking.model.ReturnResponse;
|
||
import cn.keking.utils.CaptchaUtil;
|
||
import cn.keking.utils.DateUtils;
|
||
import cn.keking.utils.KkFileUtils;
|
||
import cn.keking.utils.RarUtils;
|
||
import cn.keking.utils.WebUtils;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import org.springframework.util.ObjectUtils;
|
||
import org.springframework.util.StreamUtils;
|
||
import org.springframework.web.bind.annotation.GetMapping;
|
||
import org.springframework.web.bind.annotation.PostMapping;
|
||
import org.springframework.web.bind.annotation.RequestMapping;
|
||
import org.springframework.web.bind.annotation.RequestParam;
|
||
import org.springframework.web.bind.annotation.RestController;
|
||
import org.springframework.web.multipart.MultipartFile;
|
||
|
||
import javax.imageio.ImageIO;
|
||
import jakarta.servlet.ServletOutputStream;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import jakarta.servlet.http.HttpServletResponse;
|
||
import java.io.File;
|
||
import java.io.IOException;
|
||
import java.io.InputStream;
|
||
import java.io.OutputStream;
|
||
import java.nio.file.DirectoryStream;
|
||
import java.nio.file.Files;
|
||
import java.nio.file.InvalidPathException;
|
||
import java.nio.file.Path;
|
||
import java.nio.file.Paths;
|
||
import java.nio.file.attribute.BasicFileAttributes;
|
||
import java.util.*;
|
||
|
||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_GENERATE_TIME;
|
||
|
||
/**
|
||
* @author yudian-it
|
||
* 2017/12/1
|
||
*/
|
||
@RestController
|
||
public class FileController {
|
||
|
||
private final Logger logger = LoggerFactory.getLogger(FileController.class);
|
||
|
||
private final String fileDir = ConfigConstants.getFileDir();
|
||
private final String demoDir = "demo";
|
||
|
||
private final String demoPath = demoDir + File.separator;
|
||
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
|
||
|
||
// 文件列表性能配置
|
||
private static final int MAX_PAGE_SIZE = 100;
|
||
private static final int MAX_TOTAL_FILES = 20000;
|
||
private static final String DEFAULT_SORT_FIELD = null; // null表示使用默认排序(文件夹优先+时间降序)
|
||
private static final String DEFAULT_ORDER = "desc";
|
||
private static final boolean ENABLE_PERFORMANCE_LOG = true;
|
||
private static final int PERFORMANCE_LOG_THRESHOLD = 1000; // 超过1000个文件记录性能日志
|
||
|
||
// 内部类,用于高效存储文件信息
|
||
private static class FileInfoWrapper implements Comparable<FileInfoWrapper> {
|
||
private final String name;
|
||
private final boolean isDirectory;
|
||
private final long lastModified;
|
||
private final long size;
|
||
private final Path filePath;
|
||
private Long creationTime = null;
|
||
private boolean creationTimeLoaded = false;
|
||
|
||
public FileInfoWrapper(File file) {
|
||
this.name = file.getName();
|
||
this.isDirectory = file.isDirectory();
|
||
this.lastModified = file.lastModified();
|
||
this.size = file.length();
|
||
this.filePath = file.toPath();
|
||
}
|
||
|
||
public void loadCreationTime() {
|
||
if (!creationTimeLoaded) {
|
||
try {
|
||
BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||
creationTime = attrs.creationTime().toMillis();
|
||
} catch (IOException e) {
|
||
creationTime = lastModified; // 如果获取失败,使用修改时间
|
||
}
|
||
creationTimeLoaded = true;
|
||
}
|
||
}
|
||
|
||
public String getName() { return name; }
|
||
public boolean isDirectory() { return isDirectory; }
|
||
public long getLastModified() { return lastModified; }
|
||
public long getSize() { return size; }
|
||
public Long getCreationTime() {
|
||
if (!creationTimeLoaded) {
|
||
loadCreationTime();
|
||
}
|
||
return creationTime;
|
||
}
|
||
public Path getFilePath() { return filePath; }
|
||
|
||
@Override
|
||
public int compareTo(FileInfoWrapper other) {
|
||
return this.name.compareToIgnoreCase(other.name);
|
||
}
|
||
|
||
@Override
|
||
public boolean equals(Object obj) {
|
||
if (this == obj) return true;
|
||
if (obj == null || getClass() != obj.getClass()) return false;
|
||
FileInfoWrapper that = (FileInfoWrapper) obj;
|
||
return name.equalsIgnoreCase(that.name);
|
||
}
|
||
|
||
@Override
|
||
public int hashCode() {
|
||
return name.toLowerCase().hashCode();
|
||
}
|
||
}
|
||
|
||
// 性能统计类
|
||
private static class PerformanceStats {
|
||
private final long startTime;
|
||
private int fileCount;
|
||
private int matchingCount;
|
||
|
||
public PerformanceStats() {
|
||
this.startTime = System.currentTimeMillis();
|
||
}
|
||
|
||
public void incrementFileCount() {
|
||
fileCount++;
|
||
}
|
||
|
||
public void incrementMatchingCount() {
|
||
matchingCount++;
|
||
}
|
||
|
||
public String getStats() {
|
||
long endTime = System.currentTimeMillis();
|
||
long duration = endTime - startTime;
|
||
return String.format("处理 %d 个文件(匹配 %d 个),耗时 %d ms",
|
||
fileCount, matchingCount, duration);
|
||
}
|
||
}
|
||
|
||
@PostMapping("/fileUpload")
|
||
public ReturnResponse<Object> fileUpload(@RequestParam("file") MultipartFile file,
|
||
@RequestParam(value = "path", defaultValue = "") String path) {
|
||
ReturnResponse<Object> checkResult = this.fileUploadCheck(file, path);
|
||
if (checkResult.isFailure()) {
|
||
return checkResult;
|
||
}
|
||
|
||
String uploadPath = fileDir + demoPath;
|
||
if (!ObjectUtils.isEmpty(path)) {
|
||
uploadPath += path + File.separator;
|
||
}
|
||
|
||
File outFile = new File(uploadPath);
|
||
if (!outFile.exists() && !outFile.mkdirs()) {
|
||
logger.error("创建文件夹【{}】失败,请检查目录权限!", uploadPath);
|
||
return ReturnResponse.failure("创建文件夹失败,请检查目录权限!");
|
||
}
|
||
|
||
String fileName = checkResult.getContent().toString();
|
||
logger.info("上传文件:{}{}", uploadPath, fileName);
|
||
|
||
try (InputStream in = file.getInputStream();
|
||
OutputStream out = Files.newOutputStream(Paths.get(uploadPath + fileName))) {
|
||
StreamUtils.copy(in, out);
|
||
return ReturnResponse.success(null);
|
||
} catch (IOException e) {
|
||
logger.error("文件上传失败", e);
|
||
return ReturnResponse.failure("文件上传失败");
|
||
}
|
||
}
|
||
|
||
@PostMapping("/createFolder")
|
||
public ReturnResponse<Object> createFolder(@RequestParam(value = "path", defaultValue = "") String path,
|
||
@RequestParam("folderName") String folderName) {
|
||
if (ConfigConstants.getFileUploadDisable()) {
|
||
return ReturnResponse.failure("文件上传接口已禁用");
|
||
}
|
||
try {
|
||
// 验证文件夹名称
|
||
if (ObjectUtils.isEmpty(folderName)) {
|
||
return ReturnResponse.failure("文件夹名称不能为空");
|
||
}
|
||
|
||
if (KkFileUtils.isIllegalFileName(folderName)) {
|
||
return ReturnResponse.failure("非法文件夹名称");
|
||
}
|
||
String basePath = fileDir + demoPath;
|
||
if (!ObjectUtils.isEmpty(path)) {
|
||
basePath += path + File.separator;
|
||
}
|
||
|
||
File newFolder = new File(basePath + folderName);
|
||
if (newFolder.exists()) {
|
||
return ReturnResponse.failure("文件夹已存在");
|
||
}
|
||
|
||
if (newFolder.mkdirs()) {
|
||
logger.info("创建文件夹:{}", newFolder.getAbsolutePath());
|
||
return ReturnResponse.success();
|
||
} else {
|
||
logger.error("创建文件夹失败:{}", newFolder.getAbsolutePath());
|
||
return ReturnResponse.failure("创建文件夹失败,请检查目录权限");
|
||
}
|
||
} catch (Exception e) {
|
||
logger.error("创建文件夹异常", e);
|
||
return ReturnResponse.failure("创建文件夹失败:" + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@GetMapping("/deleteFile")
|
||
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
||
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
||
if (checkResult.isFailure()) {
|
||
return checkResult;
|
||
}
|
||
fileName = checkResult.getContent().toString();
|
||
|
||
// 构建完整路径
|
||
String fullPath = fileDir + demoPath + fileName;
|
||
File file = new File(fullPath);
|
||
|
||
logger.info("删除文件/文件夹:{}", file.getAbsolutePath());
|
||
if (file.exists()) {
|
||
if (file.isDirectory()) {
|
||
// 删除文件夹及其内容
|
||
if (deleteDirectory(file)) {
|
||
WebUtils.removeSessionAttr(request, CAPTCHA_CODE);
|
||
return ReturnResponse.success();
|
||
} else {
|
||
String msg = String.format("删除文件夹【%s】失败,请检查目录权限!", file.getPath());
|
||
logger.error(msg);
|
||
return ReturnResponse.failure(msg);
|
||
}
|
||
} else {
|
||
// 删除文件
|
||
if (file.delete()) {
|
||
WebUtils.removeSessionAttr(request, CAPTCHA_CODE);
|
||
return ReturnResponse.success();
|
||
} else {
|
||
String msg = String.format("删除文件【%s】失败,请检查目录权限!", file.getPath());
|
||
logger.error(msg);
|
||
return ReturnResponse.failure(msg);
|
||
}
|
||
}
|
||
} else {
|
||
return ReturnResponse.failure("文件或文件夹不存在");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 递归删除目录
|
||
*/
|
||
private boolean deleteDirectory(File dir) {
|
||
if (dir.isDirectory()) {
|
||
File[] children = dir.listFiles();
|
||
if (children != null) {
|
||
for (File child : children) {
|
||
boolean success = deleteDirectory(child);
|
||
if (!success) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return dir.delete();
|
||
}
|
||
|
||
/**
|
||
* 验证码方法
|
||
*/
|
||
@RequestMapping("/deleteFile/captcha")
|
||
public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||
if (!ConfigConstants.getDeleteCaptcha()) {
|
||
return;
|
||
}
|
||
|
||
response.setContentType("image/jpeg");
|
||
response.setHeader("Pragma", "no-cache");
|
||
response.setHeader("Cache-Control", "no-cache");
|
||
response.setDateHeader("Expires", -1);
|
||
String captchaCode = WebUtils.getSessionAttr(request, CAPTCHA_CODE);
|
||
long captchaGenerateTime = WebUtils.getLongSessionAttr(request, CAPTCHA_GENERATE_TIME);
|
||
long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime);
|
||
|
||
// 验证码为空,且生成验证码超过50秒,重新生成验证码
|
||
if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) {
|
||
captchaCode = CaptchaUtil.generateCaptchaCode();
|
||
// 更新验证码
|
||
WebUtils.setSessionAttr(request, CAPTCHA_CODE, captchaCode);
|
||
WebUtils.setSessionAttr(request, CAPTCHA_GENERATE_TIME, DateUtils.getCurrentSecond());
|
||
} else {
|
||
captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode;
|
||
}
|
||
|
||
ServletOutputStream outputStream = response.getOutputStream();
|
||
ImageIO.write(CaptchaUtil.generateCaptchaPic(captchaCode), "jpeg", outputStream);
|
||
outputStream.close();
|
||
}
|
||
|
||
/**
|
||
* 生产环境优化版本:先收集所有路径,按名称排序,再处理
|
||
* 优点:确保不丢失文件,结果确定性,统计准确
|
||
*/
|
||
@PostMapping("/listFiles")
|
||
public Map<String, Object> getFiles(@RequestParam(value = "path", defaultValue = "") String path,
|
||
@RequestParam(value = "searchText", defaultValue = "") String searchText,
|
||
@RequestParam(defaultValue = "0") int page,
|
||
@RequestParam(defaultValue = "20") int size,
|
||
@RequestParam(required = false) String sort,
|
||
@RequestParam(required = false) String order) {
|
||
|
||
Map<String, Object> result = new HashMap<>();
|
||
PerformanceStats stats = new PerformanceStats();
|
||
|
||
try {
|
||
// ==================== 1. 参数验证和限制 ====================
|
||
if (page < 0) page = 0;
|
||
if (size <= 0) size = 20;
|
||
if (size > MAX_PAGE_SIZE) size = MAX_PAGE_SIZE;
|
||
|
||
// 验证排序参数
|
||
if (sort != null && !isValidSortParameter(sort)) {
|
||
logger.warn("无效的排序字段: {}, 使用默认排序", sort);
|
||
sort = DEFAULT_SORT_FIELD;
|
||
}
|
||
|
||
// 验证排序方向
|
||
if (order != null && !isValidOrderParameter(order)) {
|
||
logger.warn("无效的排序方向: {}, 使用默认方向", order);
|
||
order = DEFAULT_ORDER;
|
||
}
|
||
|
||
// ==================== 2. 构建路径和验证 ====================
|
||
Path currentDir;
|
||
try {
|
||
currentDir = resolveDirectoryUnderRoot(Paths.get(fileDir, demoDir), path);
|
||
} catch (InvalidPathException | SecurityException e) {
|
||
logger.warn("拒绝访问 demo 目录之外的文件列表路径");
|
||
result.put("total", 0);
|
||
result.put("data", Collections.emptyList());
|
||
result.put("error", "非法目录路径");
|
||
return result;
|
||
} catch (IOException e) {
|
||
logger.error("解析 demo 目录失败", e);
|
||
result.put("total", 0);
|
||
result.put("data", Collections.emptyList());
|
||
return result;
|
||
}
|
||
|
||
if (!Files.isDirectory(currentDir)) {
|
||
result.put("total", 0);
|
||
result.put("data", Collections.emptyList());
|
||
return result;
|
||
}
|
||
|
||
// ==================== 3. 收集所有文件路径 ====================
|
||
List<Path> allPaths = new ArrayList<>();
|
||
long collectStartTime = System.currentTimeMillis();
|
||
|
||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
|
||
for (Path entry : stream) {
|
||
allPaths.add(entry);
|
||
stats.incrementFileCount();
|
||
}
|
||
} catch (IOException e) {
|
||
logger.error("读取目录失败: {}", currentDir, e);
|
||
result.put("total", 0);
|
||
result.put("data", Collections.emptyList());
|
||
return result;
|
||
}
|
||
|
||
long collectEndTime = System.currentTimeMillis();
|
||
int totalFilesInDirectory = allPaths.size();
|
||
|
||
// ==================== 4. 按名称排序确保确定性 ====================
|
||
// 按文件名排序,确保每次结果一致
|
||
allPaths.sort(Comparator.comparing(pathh -> pathh.getFileName().toString().toLowerCase()));
|
||
long sortEndTime = System.currentTimeMillis();
|
||
|
||
// ==================== 5. 遍历排序后的路径,进行过滤和处理 ====================
|
||
List<FileInfoWrapper> matchedFiles = new ArrayList<>();
|
||
int actualMatchingCount = 0;
|
||
boolean truncated = false;
|
||
|
||
String searchLower = ObjectUtils.isEmpty(searchText) ? null : searchText.toLowerCase();
|
||
|
||
for (Path entry : allPaths) {
|
||
File file = entry.toFile();
|
||
|
||
// 搜索过滤
|
||
if (searchLower != null) {
|
||
if (!file.getName().toLowerCase().contains(searchLower)) {
|
||
continue; // 不匹配,跳过
|
||
}
|
||
}
|
||
|
||
// 统计匹配总数
|
||
actualMatchingCount++;
|
||
stats.incrementMatchingCount();
|
||
|
||
// 如果还没达到最大处理限制,添加到结果集
|
||
if (matchedFiles.size() < MAX_TOTAL_FILES) {
|
||
matchedFiles.add(new FileInfoWrapper(file));
|
||
} else {
|
||
// 超过限制,标记截断但继续计数
|
||
truncated = true;
|
||
}
|
||
}
|
||
|
||
long filterEndTime = System.currentTimeMillis();
|
||
int displayedCount = matchedFiles.size(); // 实际显示的数量
|
||
|
||
// ==================== 6. 根据排序需求加载额外属性 ====================
|
||
if ("creationTime".equals(sort)) {
|
||
// 如果需要按创建时间排序,提前加载创建时间
|
||
for (FileInfoWrapper info : matchedFiles) {
|
||
info.loadCreationTime();
|
||
}
|
||
}
|
||
|
||
// ==================== 7. 排序 ====================
|
||
Comparator<FileInfoWrapper> comparator = getFileInfoComparator(sort, order);
|
||
if (comparator != null) {
|
||
// 根据文件数量选择合适的排序策略
|
||
if (matchedFiles.size() > 1000) {
|
||
// 大文件集使用并行排序
|
||
matchedFiles.sort(comparator);
|
||
} else {
|
||
matchedFiles.sort(comparator);
|
||
}
|
||
}
|
||
|
||
long sortingEndTime = System.currentTimeMillis();
|
||
|
||
// ==================== 8. 分页处理 ====================
|
||
List<Map<String, Object>> fileList = new ArrayList<>();
|
||
int start = page * size;
|
||
int end = Math.min(start + size, displayedCount);
|
||
|
||
if (start < displayedCount) {
|
||
for (int i = start; i < end; i++) {
|
||
FileInfoWrapper info = matchedFiles.get(i);
|
||
Map<String, Object> fileInfo = convertFileInfoToMap(info, path);
|
||
fileList.add(fileInfo);
|
||
}
|
||
}
|
||
|
||
long pagingEndTime = System.currentTimeMillis();
|
||
|
||
// ==================== 9. 构建返回结果 ====================
|
||
result.put("total", actualMatchingCount); // 实际匹配总数
|
||
result.put("displayedCount", displayedCount); // 实际显示的数量
|
||
result.put("totalInDirectory", totalFilesInDirectory); // 目录中总文件数
|
||
result.put("data", fileList);
|
||
result.put("page", page);
|
||
result.put("size", size);
|
||
result.put("totalPages", (int) Math.ceil((double) actualMatchingCount / size));
|
||
|
||
// 添加性能统计(如果需要)
|
||
if (ENABLE_PERFORMANCE_LOG && totalFilesInDirectory > PERFORMANCE_LOG_THRESHOLD) {
|
||
result.put("performance", buildPerformanceStats(
|
||
collectEndTime - collectStartTime,
|
||
sortEndTime - collectEndTime,
|
||
filterEndTime - sortEndTime,
|
||
sortingEndTime - filterEndTime,
|
||
pagingEndTime - sortingEndTime,
|
||
pagingEndTime - collectStartTime
|
||
));
|
||
}
|
||
|
||
// 如果有限制,告知用户
|
||
if (truncated) {
|
||
result.put("warning", String.format(
|
||
"文件数量过多(匹配 %d 个文件),仅显示前 %d 个文件(按名称排序)。请使用搜索功能缩小范围。",
|
||
actualMatchingCount, MAX_TOTAL_FILES
|
||
));
|
||
result.put("truncated", true);
|
||
}
|
||
|
||
// 记录性能日志
|
||
if (ENABLE_PERFORMANCE_LOG && totalFilesInDirectory > PERFORMANCE_LOG_THRESHOLD) {
|
||
logger.info("文件列表查询性能: {}", stats.getStats());
|
||
}
|
||
|
||
} catch (Exception e) {
|
||
logger.error("获取文件列表失败", e);
|
||
result.put("total", 0);
|
||
result.put("data", Collections.emptyList());
|
||
result.put("error", "获取文件列表失败: " + e.getMessage());
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Resolve an existing directory below the configured demo root.
|
||
*
|
||
* <p>Both lexical normalization and real-path checks are required: the
|
||
* former blocks traversal and absolute paths, while the latter prevents a
|
||
* symlink inside the demo directory from escaping the configured root.</p>
|
||
*/
|
||
static Path resolveDirectoryUnderRoot(Path root, String requestedPath) throws IOException {
|
||
Path normalizedRoot = root.toAbsolutePath().normalize();
|
||
String relativePath = requestedPath == null ? "" : requestedPath.replace('\\', '/');
|
||
|
||
if (relativePath.indexOf('\0') >= 0
|
||
|| relativePath.startsWith("/")
|
||
|| relativePath.matches("^[A-Za-z]:.*")) {
|
||
throw new SecurityException("Absolute paths are not allowed");
|
||
}
|
||
|
||
Path relative = Paths.get(relativePath);
|
||
if (relative.isAbsolute()) {
|
||
throw new SecurityException("Absolute paths are not allowed");
|
||
}
|
||
for (Path segment : relative) {
|
||
if ("..".equals(segment.toString())) {
|
||
throw new SecurityException("Parent path segments are not allowed");
|
||
}
|
||
}
|
||
|
||
Path resolved = normalizedRoot.resolve(relative).normalize();
|
||
if (!resolved.startsWith(normalizedRoot)) {
|
||
throw new SecurityException("Path escapes the configured root");
|
||
}
|
||
|
||
Path realRoot = normalizedRoot.toRealPath();
|
||
Path realResolved = resolved.toRealPath();
|
||
if (!realResolved.startsWith(realRoot)) {
|
||
throw new SecurityException("Path escapes the configured root through a symbolic link");
|
||
}
|
||
return realResolved;
|
||
}
|
||
|
||
/**
|
||
* 构建性能统计信息
|
||
*/
|
||
private Map<String, Object> buildPerformanceStats(long collectTime, long sortTime,
|
||
long filterTime, long sortingTime,
|
||
long pagingTime, long totalTime) {
|
||
Map<String, Object> perfStats = new HashMap<>();
|
||
perfStats.put("collectPaths", collectTime + "ms");
|
||
perfStats.put("sortPaths", sortTime + "ms");
|
||
perfStats.put("filterFiles", filterTime + "ms");
|
||
perfStats.put("sortResults", sortingTime + "ms");
|
||
perfStats.put("paging", pagingTime + "ms");
|
||
perfStats.put("total", totalTime + "ms");
|
||
return perfStats;
|
||
}
|
||
|
||
/**
|
||
* 验证排序参数
|
||
*/
|
||
private boolean isValidSortParameter(String sort) {
|
||
if (sort == null || sort.trim().isEmpty()) {
|
||
return true;
|
||
}
|
||
|
||
Set<String> validSorts = new HashSet<>(Arrays.asList(
|
||
"name", "lastModified", "creationTime", "size", "isDirectory"
|
||
));
|
||
|
||
return validSorts.contains(sort.trim().toLowerCase());
|
||
}
|
||
|
||
/**
|
||
* 验证排序方向参数
|
||
*/
|
||
private boolean isValidOrderParameter(String order) {
|
||
if (order == null || order.trim().isEmpty()) {
|
||
return true;
|
||
}
|
||
|
||
String orderLower = order.trim().toLowerCase();
|
||
return "asc".equals(orderLower) || "desc".equals(orderLower);
|
||
}
|
||
|
||
/**
|
||
* 获取文件信息比较器
|
||
*/
|
||
private Comparator<FileInfoWrapper> getFileInfoComparator(String sort, String order) {
|
||
if (sort == null || sort.trim().isEmpty()) {
|
||
// 默认排序:文件夹优先,然后按修改时间降序
|
||
return (f1, f2) -> {
|
||
// 空安全检查
|
||
if (f1 == null && f2 == null) return 0;
|
||
if (f1 == null) return 1;
|
||
if (f2 == null) return -1;
|
||
|
||
// 文件夹优先
|
||
boolean f1IsDir = f1.isDirectory();
|
||
boolean f2IsDir = f2.isDirectory();
|
||
|
||
if (f1IsDir && !f2IsDir) return -1;
|
||
if (!f1IsDir && f2IsDir) return 1;
|
||
|
||
// 都是文件夹或都是文件时,按修改时间降序
|
||
long time1 = f1.getLastModified();
|
||
long time2 = f2.getLastModified();
|
||
|
||
if (time1 != time2) {
|
||
// 使用 Long.compare 返回 int,避免转换
|
||
return Long.compare(time2, time1); // 降序:time2 - time1
|
||
}
|
||
|
||
// 修改时间相同,按名称升序(忽略大小写)
|
||
return String.CASE_INSENSITIVE_ORDER.compare(f1.getName(), f2.getName());
|
||
};
|
||
}
|
||
|
||
boolean isDesc = "desc".equalsIgnoreCase(order);
|
||
String sortLower = sort.toLowerCase();
|
||
|
||
switch (sortLower) {
|
||
case "name":
|
||
Comparator<FileInfoWrapper> nameComparator = Comparator.comparing(
|
||
FileInfoWrapper::getName, String.CASE_INSENSITIVE_ORDER
|
||
);
|
||
return isDesc ? nameComparator.reversed() : nameComparator;
|
||
|
||
case "lastmodified":
|
||
Comparator<FileInfoWrapper> timeComparator = Comparator.comparing(FileInfoWrapper::getLastModified);
|
||
return isDesc ? timeComparator.reversed() : timeComparator;
|
||
|
||
case "creationtime":
|
||
Comparator<FileInfoWrapper> creationTimeComparator = Comparator.comparing(FileInfoWrapper::getCreationTime);
|
||
return isDesc ? creationTimeComparator.reversed() : creationTimeComparator;
|
||
|
||
case "size":
|
||
// 文件夹优先,然后按大小排序
|
||
Comparator<FileInfoWrapper> sizeComparator = (f1, f2) -> {
|
||
if (f1 == null && f2 == null) return 0;
|
||
if (f1 == null) return 1;
|
||
if (f2 == null) return -1;
|
||
|
||
boolean f1IsDir = f1.isDirectory();
|
||
boolean f2IsDir = f2.isDirectory();
|
||
|
||
if (f1IsDir && !f2IsDir) return -1;
|
||
if (!f1IsDir && f2IsDir) return 1;
|
||
if (f1IsDir && f2IsDir) {
|
||
// 都是文件夹按名称排序
|
||
return f1.getName().compareToIgnoreCase(f2.getName());
|
||
}
|
||
|
||
// 都是文件时按大小排序
|
||
return Long.compare(f1.getSize(), f2.getSize());
|
||
};
|
||
return isDesc ? sizeComparator.reversed() : sizeComparator;
|
||
|
||
case "isdirectory":
|
||
// 按文件夹优先排序
|
||
return (f1, f2) -> {
|
||
if (f1 == null && f2 == null) return 0;
|
||
if (f1 == null) return 1;
|
||
if (f2 == null) return -1;
|
||
|
||
boolean f1IsDir = f1.isDirectory();
|
||
boolean f2IsDir = f2.isDirectory();
|
||
|
||
if (f1IsDir && !f2IsDir) return -1;
|
||
if (!f1IsDir && f2IsDir) return 1;
|
||
|
||
// 都是文件夹或都是文件时,按修改时间降序
|
||
long time1 = f1.getLastModified();
|
||
long time2 = f2.getLastModified();
|
||
|
||
if (time1 != time2) {
|
||
return Long.compare(time2, time1); // 降序
|
||
}
|
||
|
||
// 修改时间相同,按名称升序
|
||
return String.CASE_INSENSITIVE_ORDER.compare(f1.getName(), f2.getName());
|
||
};
|
||
|
||
default:
|
||
logger.warn("未知的排序字段: {}, 使用默认排序", sort);
|
||
// 返回默认比较器
|
||
return getFileInfoComparator(null, order);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将 FileInfoWrapper 转换为 Map
|
||
*/
|
||
private Map<String, Object> convertFileInfoToMap(FileInfoWrapper info, String path) {
|
||
Map<String, Object> fileMap = new HashMap<>();
|
||
|
||
fileMap.put("name", info.getName());
|
||
fileMap.put("isDirectory", info.isDirectory());
|
||
fileMap.put("lastModified", info.getLastModified());
|
||
fileMap.put("size", info.getSize());
|
||
fileMap.put("creationTime", info.getCreationTime());
|
||
|
||
// 构建路径信息
|
||
String relativePath = demoDir + "/" + (ObjectUtils.isEmpty(path) ? "" : path + "/") + info.getName();
|
||
fileMap.put("relativePath", relativePath);
|
||
|
||
// 如果是目录,保存完整的相对路径用于导航
|
||
if (info.isDirectory()) {
|
||
String fullPath = ObjectUtils.isEmpty(path) ? info.getName() : path + "/" + info.getName();
|
||
fileMap.put("fullPath", fullPath);
|
||
}
|
||
|
||
return fileMap;
|
||
}
|
||
|
||
/**
|
||
* 上传文件前校验
|
||
*/
|
||
private ReturnResponse<Object> fileUploadCheck(MultipartFile file, String path) {
|
||
if (ConfigConstants.getFileUploadDisable()) {
|
||
return ReturnResponse.failure("文件上传接口已禁用");
|
||
}
|
||
|
||
String fileName = WebUtils.getFileNameFromMultipartFile(file);
|
||
if (fileName.lastIndexOf(".") == -1) {
|
||
return ReturnResponse.failure("不允许上传的类型");
|
||
}
|
||
if (!KkFileUtils.isAllowedUpload(fileName)) {
|
||
return ReturnResponse.failure("不允许上传的文件类型: " + fileName);
|
||
}
|
||
if (KkFileUtils.isIllegalFileName(fileName)) {
|
||
return ReturnResponse.failure("不允许上传的文件名: " + fileName);
|
||
}
|
||
FileType type = FileType.typeFromFileName(fileName);
|
||
if (Objects.equals(type, FileType.OTHER)) {
|
||
return ReturnResponse.failure("该文件格式还不支持预览,请联系管理员,添加该格式: " + fileName);
|
||
}
|
||
|
||
// 判断是否存在同名文件
|
||
if (existsFile(fileName, path)) {
|
||
return ReturnResponse.failure("存在同名文件,请先删除原有文件再次上传");
|
||
}
|
||
|
||
return ReturnResponse.success(fileName);
|
||
}
|
||
|
||
/**
|
||
* 删除文件前校验
|
||
*/
|
||
private ReturnResponse<Object> deleteFileCheck(HttpServletRequest request, String fileName, String password) {
|
||
if (ObjectUtils.isEmpty(fileName)) {
|
||
return ReturnResponse.failure("文件名为空,删除失败!");
|
||
}
|
||
try {
|
||
fileName = WebUtils.decodeUrl(fileName, "base64");
|
||
} catch (Exception ex) {
|
||
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, fileName);
|
||
return ReturnResponse.failure(errorMsg + "删除失败!");
|
||
}
|
||
|
||
if (ObjectUtils.isEmpty(fileName)) {
|
||
return ReturnResponse.failure("文件名为空,删除失败!");
|
||
}
|
||
if (fileName.contains("/")) {
|
||
fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
|
||
}
|
||
if (KkFileUtils.isIllegalFileName(fileName)) {
|
||
return ReturnResponse.failure("非法文件名,删除失败!");
|
||
}
|
||
|
||
if (ObjectUtils.isEmpty(password)) {
|
||
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
||
}
|
||
|
||
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
|
||
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
||
ConfigConstants.getPassword();
|
||
|
||
if (!password.equalsIgnoreCase(expectedPassword)) {
|
||
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
||
return ReturnResponse.failure("删除文件失败,密码错误!");
|
||
}
|
||
|
||
return ReturnResponse.success(fileName);
|
||
}
|
||
|
||
@GetMapping("/directory")
|
||
public Object directory(String urls) {
|
||
String fileUrl;
|
||
try {
|
||
fileUrl = WebUtils.decodeUrl(urls,"base64");
|
||
} catch (Exception ex) {
|
||
String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
|
||
return ReturnResponse.failure(errorMsg);
|
||
}
|
||
fileUrl = fileUrl.replaceAll("http://", "");
|
||
if (KkFileUtils.isIllegalFileName(fileUrl)) {
|
||
return ReturnResponse.failure("不允许访问的路径:");
|
||
}
|
||
return RarUtils.getTree(fileUrl);
|
||
}
|
||
|
||
private boolean existsFile(String fileName, String path) {
|
||
String fullPath = fileDir + demoPath;
|
||
if (!ObjectUtils.isEmpty(path)) {
|
||
fullPath += path + File.separator;
|
||
}
|
||
File file = new File(fullPath + fileName);
|
||
return file.exists();
|
||
}
|
||
}
|