package com.yj.earth.business.controller; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; import com.drew.imaging.ImageMetadataReader; import com.drew.imaging.ImageProcessingException; import com.drew.lang.Rational; import com.drew.metadata.Directory; import com.drew.metadata.Metadata; import java.io.*; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import cn.hutool.crypto.digest.DigestUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.drew.metadata.MetadataException; import com.drew.metadata.exif.GpsDirectory; import com.yj.earth.annotation.CheckAuth; import com.yj.earth.business.domain.FileInfo; import com.yj.earth.business.service.FileInfoService; import com.yj.earth.common.util.ApiResponse; import com.yj.earth.common.util.JsonUtil; import com.yj.earth.vo.FileInfoVo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.util.DigestUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; @Tag(name = "文件数据管理") @CheckAuth @RestController @RequestMapping("/fileInfo") public class FileInfoController { @Resource private FileInfoService fileInfoService; @Operation(summary = "文件上传") @PostMapping("/upload") public ApiResponse uploadFiles(@Parameter(description = "上传的文件数组", required = true) @RequestParam("files") MultipartFile[] files) throws IOException { // 校验文件数组是否为空 if (files == null || files.length == 0) { return ApiResponse.failure("上传文件不能为空"); } // 获取完整的上传目录路径 String fullUploadPath = fileInfoService.getFullUploadPath(); List fileInfoVoList = new ArrayList<>(); // 遍历处理每个文件 for (MultipartFile file : files) { // 跳过空文件 if (file.isEmpty()) { continue; } // 获取原始文件名和后缀 String originalFilename = file.getOriginalFilename(); String fileSuffix = FileUtil.extName(originalFilename); String contentType = file.getContentType(); // 生成唯一文件名、避免重复 String uniqueFileName = IdUtil.simpleUUID() + "." + fileSuffix; // 创建文件存储目录(如果不存在则自动创建) File uploadDir = new File(fullUploadPath); FileUtil.mkdir(uploadDir); // 构建完整文件路径 String filePath = fullUploadPath + File.separator + uniqueFileName; File destFile = new File(filePath); // 使用Hutool工具类保存文件 FileUtil.writeBytes(file.getBytes(), destFile); // 计算文件MD5(用于校验文件完整性) String fileMd5 = DigestUtil.md5Hex(destFile); // 查询有没有文件名一样并且 MD5 也一样的数据 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(FileInfo::getFileName, originalFilename).eq(FileInfo::getFileMd5, fileMd5); if (fileInfoService.count(queryWrapper) > 0) { return ApiResponse.failure("已存在文件名相同且内容完全一致的文件"); } // 保存文件信息到数据库 FileInfo fileInfo = new FileInfo(); fileInfo.setFileName(originalFilename); fileInfo.setFileSuffix(fileSuffix); fileInfo.setContentType(contentType); fileInfo.setFileSize(file.getSize()); fileInfo.setFilePath(uniqueFileName); fileInfo.setFileMd5(fileMd5); // 保存文件信息并获取ID fileInfoService.save(fileInfo); // 构建并设置预览URL和下载URL String previewUrl = "/fileInfo/preview/" + fileInfo.getId(); String downloadUrl = "/fileInfo/download/" + fileInfo.getId(); FileInfoVo fileInfoVo = new FileInfoVo(); BeanUtils.copyProperties(fileInfo, fileInfoVo); fileInfoVo.setPreviewUrl(previewUrl); fileInfoVo.setDownloadUrl(downloadUrl); fileInfoVoList.add(fileInfoVo); } if (fileInfoVoList.isEmpty()) { return ApiResponse.failure("未成功上传任何文件"); } return ApiResponse.success(fileInfoVoList); } @Operation(summary = "文件下载") @GetMapping("/download/{id}") public void downloadFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException { // 根据ID查询文件信息 FileInfo fileInfo = fileInfoService.getById(id); if (fileInfo == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 构建完整文件路径 String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath(); File file = new File(fullPath); // 校验文件是否存在 if (!file.exists()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 设置响应头 response.setContentType(fileInfo.getContentType()); response.setContentLengthLong(fileInfo.getFileSize()); String encodedFileName = URLEncoder.encode(fileInfo.getFileName(), StandardCharsets.UTF_8.name()); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName); // 使用 Hutool 工具类复制文件流到响应输出流 try (OutputStream os = response.getOutputStream()) { FileUtil.writeToStream(file, os); } } @Operation(summary = "文件预览") @GetMapping("/preview/{id}") public void previewFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException { // 根据ID查询文件信息 FileInfo fileInfo = fileInfoService.getById(id); if (fileInfo == null) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 构建完整文件路径 String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath(); File file = new File(fullPath); // 校验文件是否存在 if (!file.exists()) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); return; } // 设置响应头(不设置 attachment、让浏览器直接显示) response.setContentType(fileInfo.getContentType()); response.setContentLengthLong(fileInfo.getFileSize()); // 使用 Hutool 工具类复制文件流到响应输出流 try (OutputStream os = response.getOutputStream()) { FileUtil.writeToStream(file, os); } } @Operation(summary = "本地文件预览") @GetMapping("/previewLocal") public void previewLocalFile(@Parameter(description = "本地文件绝对路径") @RequestParam String fileAbsolutePath, HttpServletResponse response) { Path targetFilePath = null; try { // 标准化路径 targetFilePath = Paths.get(fileAbsolutePath).toRealPath(); // 校验文件合法性:是否存在、是否为普通文件 BasicFileAttributes fileAttr = Files.readAttributes(targetFilePath, BasicFileAttributes.class); if (!fileAttr.isRegularFile()) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); fileInfoService.writeResponseMessage(response, "目标路径不是有效的文件"); return; } // 设置预览响应头 String fileName = targetFilePath.getFileName().toString(); String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); String contentType = Files.probeContentType(targetFilePath); // 对于文本类型文件、指定字符编码 if (contentType != null && contentType.startsWith("text/")) { response.setContentType(contentType + "; charset=UTF-8"); } else { response.setContentType(contentType != null ? contentType : "application/octet-stream"); } response.setContentLengthLong(fileAttr.size()); // 关键修改:将attachment改为inline实现预览 response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName); // 写入文件流 try (InputStream inputStream = Files.newInputStream(targetFilePath); OutputStream outputStream = response.getOutputStream()) { byte[] buffer = new byte[1024 * 8]; int len; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); } outputStream.flush(); } } catch (NoSuchFileException e) { response.setStatus(HttpServletResponse.SC_NOT_FOUND); fileInfoService.writeResponseMessage(response, "文件不存在:" + fileAbsolutePath); } catch (SecurityException e) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); fileInfoService.writeResponseMessage(response, "访问拒绝:无权限读取该文件"); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); fileInfoService.writeResponseMessage(response, "预览失败:" + e.getMessage()); } } }