|
|
@@ -0,0 +1,212 @@
|
|
|
+package com.emoon.filedownloader.controller;
|
|
|
+
|
|
|
+import cn.hutool.core.io.FileUtil;
|
|
|
+import cn.hutool.core.util.IdUtil;
|
|
|
+import cn.hutool.crypto.digest.DigestUtil;
|
|
|
+import com.emoon.common.core.domain.R;
|
|
|
+import com.emoon.common.core.utils.StringUtils;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.web.bind.annotation.*;
|
|
|
+
|
|
|
+import java.io.File;
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.util.Base64;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 文件下载控制器
|
|
|
+ *
|
|
|
+ * @author destiny
|
|
|
+ * @description: 提供文件base64编码下载到服务器和删除文件的功能
|
|
|
+ * @date 2025/12/20 21:29
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequestMapping("/api/file")
|
|
|
+public class DownloadController {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 文件存储根目录,配置在application.yml中
|
|
|
+ */
|
|
|
+ @Value("${file.download.path:/tmp/downloads}")
|
|
|
+ private String basePath;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载文件到服务器
|
|
|
+ * 接收文件的base64编码,保存到指定目录并返回文件路径
|
|
|
+ *
|
|
|
+ * @param requestData 请求数据,包含base64编码的文件内容和文件名
|
|
|
+ * @return 文件在服务器上的完整路径
|
|
|
+ */
|
|
|
+ @PostMapping("/download")
|
|
|
+ public R<Map<String, String>> downloadFile(@RequestBody Map<String, String> requestData) {
|
|
|
+ try {
|
|
|
+ String base64Content = requestData.get("content");
|
|
|
+ String fileName = requestData.get("fileName");
|
|
|
+
|
|
|
+ // 参数校验
|
|
|
+ if (StringUtils.isEmpty(base64Content)) {
|
|
|
+ return R.fail("文件内容不能为空");
|
|
|
+ }
|
|
|
+ if (StringUtils.isEmpty(fileName)) {
|
|
|
+ return R.fail("文件名不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全性检查:防止路径遍历攻击
|
|
|
+ if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
|
|
+ return R.fail("文件名包含非法字符");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建存储目录
|
|
|
+ File baseDir = new File(basePath);
|
|
|
+ if (!baseDir.exists()) {
|
|
|
+ baseDir.mkdirs();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成唯一文件名(防止文件名冲突)
|
|
|
+ String fileExtension = FileUtil.extName(fileName);
|
|
|
+ String baseFileName = FileUtil.mainName(fileName);
|
|
|
+ String uniqueFileName = baseFileName + "_" + IdUtil.simpleUUID() + "." + fileExtension;
|
|
|
+
|
|
|
+ // 构建完整的文件路径
|
|
|
+ Path filePath = Paths.get(basePath, uniqueFileName);
|
|
|
+
|
|
|
+ // 解码base64并写入文件
|
|
|
+ String cleanBase64 = base64Content.replaceAll("^data:[^;]+;base64,", "");
|
|
|
+ byte[] fileBytes = Base64.getDecoder().decode(cleanBase64);
|
|
|
+ Files.write(filePath, fileBytes);
|
|
|
+
|
|
|
+ // 计算文件MD5(用于后续校验)
|
|
|
+ String fileMd5 = DigestUtil.md5Hex(fileBytes);
|
|
|
+
|
|
|
+ // 构建返回结果
|
|
|
+ Map<String, String> result = new HashMap<>();
|
|
|
+ result.put("fileName", uniqueFileName);
|
|
|
+ result.put("filePath", filePath.toString());
|
|
|
+ result.put("originalFileName", fileName);
|
|
|
+ result.put("fileSize", String.valueOf(fileBytes.length));
|
|
|
+ result.put("md5", fileMd5);
|
|
|
+
|
|
|
+ log.info("文件下载成功: 原文件名={}, 存储路径={}, 大小={}bytes", fileName, filePath, fileBytes.length);
|
|
|
+
|
|
|
+ return R.ok("文件下载成功", result);
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("文件下载失败", e);
|
|
|
+ return R.fail("文件下载失败:" + e.getMessage());
|
|
|
+ } catch (IllegalArgumentException e) {
|
|
|
+ log.error("base64解码失败", e);
|
|
|
+ return R.fail("文件内容格式错误,请检查base64编码");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("系统异常", e);
|
|
|
+ return R.fail("系统异常,请稍后重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除服务器上的文件
|
|
|
+ * 只能删除配置的basePath下的文件,防止误删
|
|
|
+ *
|
|
|
+ * @param requestData 请求数据,包含要删除的文件名
|
|
|
+ * @return 删除结果
|
|
|
+ */
|
|
|
+ @DeleteMapping("/delete")
|
|
|
+ public R<String> deleteFile(@RequestBody Map<String, String> requestData) {
|
|
|
+ try {
|
|
|
+ String fileName = requestData.get("fileName");
|
|
|
+
|
|
|
+ // 参数校验
|
|
|
+ if (StringUtils.isEmpty(fileName)) {
|
|
|
+ return R.fail("文件名不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 安全性检查:防止路径遍历攻击
|
|
|
+ if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
|
|
+ return R.fail("文件名包含非法字符");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建文件路径(确保只能删除basePath下的文件)
|
|
|
+ Path filePath = Paths.get(basePath, fileName);
|
|
|
+ File file = filePath.toFile();
|
|
|
+
|
|
|
+ // 检查文件是否存在
|
|
|
+ if (!file.exists()) {
|
|
|
+ return R.fail("文件不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件是否在允许的目录下(双重安全保障)
|
|
|
+ String canonicalBasePath = new File(basePath).getCanonicalPath();
|
|
|
+ String canonicalFilePath = file.getCanonicalPath();
|
|
|
+ if (!canonicalFilePath.startsWith(canonicalBasePath)) {
|
|
|
+ log.warn("尝试删除不在允许目录下的文件: {}", canonicalFilePath);
|
|
|
+ return R.fail("无权限删除该文件");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除文件
|
|
|
+ boolean deleted = FileUtil.del(file);
|
|
|
+ if (deleted) {
|
|
|
+ log.info("文件删除成功: {}", filePath);
|
|
|
+ return R.ok("文件删除成功");
|
|
|
+ } else {
|
|
|
+ log.error("文件删除失败: {}", filePath);
|
|
|
+ return R.fail("文件删除失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("删除文件时发生IO异常", e);
|
|
|
+ return R.fail("删除文件失败:" + e.getMessage());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("系统异常", e);
|
|
|
+ return R.fail("系统异常,请稍后重试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件信息(可选功能,用于检查文件是否存在)
|
|
|
+ *
|
|
|
+ * @param fileName 文件名
|
|
|
+ * @return 文件信息
|
|
|
+ */
|
|
|
+ @GetMapping("/info/{fileName}")
|
|
|
+ public R<Map<String, Object>> getFileInfo(@PathVariable String fileName) {
|
|
|
+ try {
|
|
|
+ // 安全性检查
|
|
|
+ if (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\")) {
|
|
|
+ return R.fail("文件名包含非法字符");
|
|
|
+ }
|
|
|
+
|
|
|
+ Path filePath = Paths.get(basePath, fileName);
|
|
|
+ File file = filePath.toFile();
|
|
|
+
|
|
|
+ if (!file.exists()) {
|
|
|
+ return R.fail("文件不存在");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件是否在允许的目录下
|
|
|
+ String canonicalBasePath = new File(basePath).getCanonicalPath();
|
|
|
+ String canonicalFilePath = file.getCanonicalPath();
|
|
|
+ if (!canonicalFilePath.startsWith(canonicalBasePath)) {
|
|
|
+ return R.fail("无权限访问该文件");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建文件信息
|
|
|
+ Map<String, Object> fileInfo = new HashMap<>();
|
|
|
+ fileInfo.put("fileName", fileName);
|
|
|
+ fileInfo.put("filePath", filePath.toString());
|
|
|
+ fileInfo.put("fileSize", file.length());
|
|
|
+ fileInfo.put("lastModified", file.lastModified());
|
|
|
+ fileInfo.put("exists", file.exists());
|
|
|
+
|
|
|
+ return R.ok("获取文件信息成功", fileInfo);
|
|
|
+
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.error("获取文件信息失败", e);
|
|
|
+ return R.fail("获取文件信息失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|