在工作中经常用对文件的CRUD,这里我整理了基础的功能方法提供大家使用参考, 可直接调用接口,可注入调用方法,方法均为基础功能实现,可根据自己的业务进行对应的微调,直接cv到自己项目中即可使用。后续会扩展文件格式转换,实现多种格式的在线预览等相关方法。
该篇文章会持续优化代码,欢迎大家一起参与,希望大家能在评论区一起学习优化补充代码,让开发变得更简单更高效。
新增断点续传,分片上传下载等功能。
#再yml中配置相关文件的路径信息,文件上传路径
iot:
activeFile:
path: /home/10jqka/file/file
package com.10jqka.iot.common.poi.file;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
/**
* 公共文件的 CRUD
*
* @author 10jqka
* @version 1.0
*/
@RestController
@RequestMapping("/file")
public class FileController {
@Value("${iot.activeFile.path}")
private String uploadPath; // 文件上传路径
@Autowired
private NonStaticResourceHttpRequestConfig nonStaticResourceHttpRequestConfig;
/**
* 文件上传
*
* @param file 文件
* @return 上传成功: 响应文件名称和文件路径的集合, 上传失败: 响应错误信息
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Map UploadFile(MultipartFile file) {
//处理单个文件
Map resultMap = new HashMap<>();//创建储存容器
//获取文件、原始文件名称和文件后缀
String uploadPath = winLinux();
String filename = file.getOriginalFilename();
//创建新且唯一文件名(格式:"当前时间戳-文件原始名称")
long lName = System.currentTimeMillis();
String uuname = lName + "-" + filename;
//判断文件路径是否为空,如果路径不存在,创建新目录
File filepath = new File(uploadPath);
if (!filepath.exists()) {
if (!filepath.mkdirs()) {
resultMap.put("错误信息:", filename + "文件目录创建失败!");
}
}
//将临时文件转存到指定为位置
try {
file.transferTo(new File(uploadPath, uuname));
} catch (IOException e) {
e.printStackTrace();
resultMap.put("错误信息:", filename + "文件上传失败!");
}
resultMap.put("path", uploadPath + "/" + uuname);
resultMap.put("name", filename);
return resultMap;
}
/**
* 文件下载
*
* @param filePath 文件的存放路径
* @param response 响应
*/
@GetMapping("/download")
public void DownloadFile(@RequestParam("filePath") String filePath, HttpServletResponse response) throws IOException {
File file = new File(filePath);
String uuName = file.getName();
// 设置响应头
// String uploadPath = winLinux();//获取路径
FileInputStream inputStream = new FileInputStream(file);
ServletOutputStream outputStream = response.getOutputStream();
// 根据文件类型设置响应内容类型
response.setContentType("application/octet-stream");
response.setContentLength((int) file.length());
// 设置Content-Disposition头,指定下载文件名
uuName = URLEncoder.encode(uuName, "UTF-8");//处理中文乱码
response.setHeader("Content-Disposition", "attachment; filename=" + uuName);
// 将文件数据写入响应输出流
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
inputStream.close();
outputStream.flush();
}
/**
* 文件查看(浏览)
*
* @param filePath 文件路径
* @param response 响应预览
* @throws IOException IO异常
*/
@GetMapping("/look")
public void LookFile(@RequestParam("filePath") String filePath, HttpServletRequest request, HttpServletResponse response) throws IOException {
File file = new File(filePath);
response.setCharacterEncoding("UTF-8");
if (!file.exists()) {
response.getWriter().write("文件不存在!");
response.getWriter().close();
return;
}
String uuName = file.getName();
//获取临时文件路径
String pdfPath = suffix(uuName);
if (pdfPath.equals("false")) {
String[] parts = uuName.split("\\.");//获取文件名后缀
String extension = parts[parts.length - 1];
String allowedExtensions = ".pdf.jpg.jpeg.png.gif";
if (allowedExtensions.contains(extension)) {//默认支持在线预览格式
//回显文件
lookFile(filePath, response);
} else if (extension.contains("mp4")) {//MP4视频在线预览
// getVideo(filePath, request, response);//普通在线预览
fileChunkDownload(filePath, request, response);//分片预览+断点续传+分片下载
} else {
response.getWriter().write("文件查看失败!");
response.getWriter().close();
}
} else {
//回显临时文件
lookFile(pdfPath, response);
}
}
/**
* 文件删除
*
* @param filePath 文件的路径
* @return 响应信息
*/
@GetMapping("/delete")
public String DeleteFile(@RequestParam("filePath") String filePath) {
File file = new File(filePath);
if (!file.exists()) {
return "";
}
String uuName = file.getName();
String result = file.delete() ? uuName + "删除成功!" : uuName + "删除失败!";
//判断是否有临时文件
if (!suffix(uuName).equals("false")) {
result += file.delete() ? "临时" + uuName + "删除成功!" : "临时" + uuName + "删除失败!";
}
return result;
}
/**
* 件路径处理方法
*
* @return 返回文件的路径
*/
public String winLinux() {
String os = System.getProperty("os.name").toLowerCase();
String uploadPaths = "";
if (os.contains("win")) {
uploadPaths = "E:" + uploadPath;
} else if (os.contains("nux")) {
uploadPaths = uploadPath;
}
return uploadPaths;
}
/**
* 生成临时文件路径方法(pdf格式)
*
* @param uuName 文件名称(uuName)
* @return 返回pdf格式的临时文件路径
*/
public String suffix(String uuName) {
String[] parts = uuName.split("\\.");//获取文件名后缀
String extension = parts[parts.length - 1];
String allowedExtensions = ".pdf.jpg.jpeg.png.gif";
if (allowedExtensions.contains(extension)) {
return "false";
}
//生成临时pdf文件路径
String uploadPath = winLinux();
String fileName = parts[0] + ".pdf";
String pdfPath = uploadPath + "/" + fileName;
File file = new File(pdfPath);
if (!file.exists()) {
return "false";
}
return pdfPath;
}
/**
* 回显文件数据方法
*
* @param filePath 文件路径
* @param response 响应
* @throws IOException IO异常
*/
public void lookFile(@RequestParam("filePath") String filePath, HttpServletResponse response) throws IOException {
//读取文件,输入流
try (FileInputStream uunameStream = new FileInputStream(filePath);
//写输出流对象,输出流
ServletOutputStream outputStream = response.getOutputStream()) {
//写回数据
int i;
byte[] buffer = new byte[4096];
while ((i = uunameStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, i);
outputStream.flush();
}
}
}
/**
* MP4视频在线预览方法
*
* @param videoPath 视频文件的路径
* @param request 请求
* @param response 响应
* @throws ServletException 异常
* @throws IOException 异常
*/
public void getVideo(String videoPath, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//保存视频磁盘路径
Path filePath = Paths.get(videoPath);
//Files.exists:用来测试路径文件是否存在
if (Files.exists(filePath)) {
//获取视频的类型,比如是MP4这样
File file = new File(videoPath);
String mimeType = Files.probeContentType(filePath);
if (StringUtils.isNotEmpty(mimeType)) {
//判断类型,根据不同的类型文件来处理对应的数据
response.setContentType(mimeType);
response.addHeader("Content-Length", "" + file.length());
}
//转换视频流部分
request.setAttribute(NonStaticResourceHttpRequestConfig.ATTR_FILE, filePath);
nonStaticResourceHttpRequestConfig.handleRequest(request, response);
} else {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
/**
* 大文件分片上传
* 目前是不含有断点续传的,如果需要断点续传需要改动失败重发相关的代码
*
* @param file 上传的文件
* @param fileName 文件的名称
* @param chunkNumber 分块的数量
* @param totalChunks 分块的总数
* @return 返回文件的路径和文件的名称
* @throws IOException IO异常
*/
@PostMapping("/uploadingFragment")
public Map uploadingFragment(@RequestParam("file") MultipartFile file,
@RequestParam("fileName") String fileName,
@RequestParam("chunkNumber") int chunkNumber,
@RequestParam("totalChunks") int totalChunks) throws IOException {
//处理前端响应无数据问题(无法加载响应数据: Request content was evicted from inspector cache)
// response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // 禁用缓存
// response.setHeader("Pragma", "no-cache");
// response.setHeader("Expires", "0");
// response.setCharacterEncoding("UTF-8");
// response.getWriter().write("文件不存在!");
// response.getWriter().close();
//处理返回结果
Map result = new HashMap<>();
//获取文件
File uploadDirectory = new File(uploadPath);
if (!uploadDirectory.exists()) {
uploadDirectory.mkdirs();
}
//处理分段文件数据
File destFile = new File(uploadPath + File.separator + fileName + ".part" + chunkNumber);
FileUtils.copyInputStreamToFile(file.getInputStream(), destFile);//写入流文件
// 如果所有部分都已上传,则将它们组合成一个完整的文件
if (chunkNumber == totalChunks) {
long timestamp = System.currentTimeMillis();//获取当前时间戳
String newFileName = timestamp + "-" + fileName; // 新文件名为时间戳-文件名称
String xinFilePath = uploadPath + "/" + newFileName;//新且不唯一的文件名称 uuname
for (int i = 1; i <= totalChunks; i++) {
File partFile = new File(uploadPath + File.separator + fileName + ".part" + i);
try (FileOutputStream fos = new FileOutputStream(xinFilePath, true)) {
//设置重试次数
int maxRetries = 3;
int retryCount = 0;
boolean success = false;
while (retryCount < maxRetries && !success) {
try {
FileUtils.copyFile(partFile, fos);//合并所有分片数据
success = true;
} catch (IOException e) {
System.out.println("文件复制失败。重试...");
retryCount++;
//每次重试睡眠5秒
if (retryCount < maxRetries) {
int sleepDurationInSeconds = 5; // 设置睡眠时间为3秒
System.out.println("睡眠" + sleepDurationInSeconds + " 重试前几秒…");
try {
Thread.sleep(sleepDurationInSeconds * 1000); // 将秒转换为毫秒
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
//如果还是上传失败,删除临时文件,告诉错误信息
if (!success) { //删除临时文件
File folder = new File(uploadPath);
File[] files = folder.listFiles();
if (files != null) {
for (File file1 : files) {
String fileName1 = file1.getName();
if (fileName1.matches(".*\\.part\\d+")) {
boolean delete = file1.delete();//删除临时文件
System.out.println(delete);
}
}
}
fos.close();//关闭流
File file1 = new File(xinFilePath);
String name = file1.getName();
boolean delete = file1.delete();//删除失败的合并文件
System.out.println(delete);
System.out.println("删除的文件名称::::" + name);
result.put("消息:", "网络问题," + name + "上传失败!");
return result;
}
boolean delete = partFile.delete();//删除临时文件
System.out.println(delete);
}
}
//处理合并成功后的返回结果
result.put("path", xinFilePath);
result.put("name", fileName);
return result;
}
//处理临时文件的返回结果
result.put("消息:", "临时文件上传成功!");
return result;
}
/**
* 大文件分块下载和断点续传
*
* @param filePath 文件的路径
* @param request 请求
* @param response 响应
*/
public void fileChunkDownload(String filePath, HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
String range = request.getHeader("Range");//设置Range的响应头信息
log.info("当前请求范围:" + range);
File file = new File(filePath);
//开始下载位置
long startByte = 0;
//结束下载位置
long endByte = file.length() - 1;
log.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, file.length());
//如果有 Range 的话 设置Range
if (range != null && range.contains("bytes=") && range.contains("-")) {
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split("-");
try {
//判断range的类型
if (ranges.length == 1) {
//类型一:bytes=-2343
if (range.startsWith("-")) {
endByte = Long.parseLong(ranges[0]);
}
//类型二:bytes=2343-
else if (range.endsWith("-")) {
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;
log.error("范围发生错误,错误消息:{}", e.getLocalizedMessage());
}
}
//要下载的长度
long contentLength = endByte - startByte + 1;
//文件名
String fileName = file.getName();
//获取文件类型
String contentType = request.getServletContext().getMimeType(fileName);
//解决下载文件时文件名乱码问题
fileName = URLEncoder.encode(fileName, "UTF-8");
// byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
// fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
//各种响应头设置
//支持断点续传,获取部分字节内容:
response.setHeader("Accept-Ranges", "bytes");
//http状态码要为206:表示获取部分内容
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setContentType(contentType);//文件类型设置
response.setHeader("Content-Type", contentType);
//inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
response.setHeader("Content-Disposition", "inline;filename=" + fileName);
response.setHeader("Content-Length", String.valueOf(contentLength));
// Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());
BufferedOutputStream outputStream = null;
RandomAccessFile randomAccessFile = null;
//已传送数据大小
long transmitted = 0;
try {
randomAccessFile = new RandomAccessFile(file, "r");
outputStream = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[4 * 4096];
int len = 0;
randomAccessFile.seek(startByte);
//判断是否到了最后不足4096(buff的length)个byte这个逻辑,否则((transmitted + len) <= contentLength)要放前面
//不然会会先读取randomAccessFile,造成后面读取位置出错
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
outputStream.write(buff, 0, len);
transmitted += len;
}
//处理不足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.info("下载完毕:" + startByte + "-" + endByte + ":" + transmitted);
} catch (ClientAbortException e) {
log.warn("用户停止下载:" + startByte + "-" + endByte + ":" + transmitted);
//捕获此异常表示拥护停止下载
} catch (IOException e) {
e.printStackTrace();
log.error("用户下载IO异常,错误消息:{}", e.getLocalizedMessage());
} finally {
try {
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
NonStaticResourceHttpRequestConfig 类
@Component
public class NonStaticResourceHttpRequestConfig extends ResourceHttpRequestHandler {
public final static String ATTR_FILE = "NON-STATIC-FILE";
@Override
protected Resource getResource(HttpServletRequest request) {
final Path filePath = (Path) request.getAttribute(ATTR_FILE);
return new FileSystemResource(filePath);
}
}
Controller层调用方法
@Autowired
private FileController fileController;//公共的文件操作
//预览
fileController.LookFile(filePath, response);
//删除
fileController.DeleteFile(filePath);
//新增
fileController.UploadFile(files)