SpringBoot 文件上传下载预览删除通用珍藏版(可直接cv)

        在工作中经常用对文件的CRUD,这里我整理了基础的功能方法提供大家使用参考, 可直接调用接口,可注入调用方法,方法均为基础功能实现,可根据自己的业务进行对应的微调,直接cv到自己项目中即可使用。

              该篇文章会持续优化代码,欢迎大家一起参与,希望大家能在评论区一起学习优化补充代码,让开发变得更简单更高效。

新增断点续传,分片上传下载等功能。新增了docx转pdf方法,支持默认浏览和docx浏览。

依赖文件:

        <!-- 文件预览 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>4.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>4.1.0</version>
        </dependency>
        <dependency>
            <groupId>fr.opensagres.xdocreport</groupId>
            <artifactId>fr.opensagres.poi.xwpf.converter.pdf-gae</artifactId>
            <version>2.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>ooxml-schemas</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml-schemas</artifactId>
            <version>4.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>4.1.0</version>
        </dependency>

文件路径配置:

#再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<String, String> UploadFile(MultipartFile file) {
        //处理单个文件
        Map<String, String> 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;
        //docx转pdf
        String docxPath = uploadPath + "/" + uuName; //doc文件路径
        try {
            // 读取 DOCX 文件
            FileInputStream docx = new FileInputStream(docxPath);
            XWPFDocument document = new XWPFDocument(docx);
            // 设置 PDF 转换选项
            PdfOptions pdfOptions = PdfOptions.create();
            // 创建PDF输出流
            OutputStream out = Files.newOutputStream(Paths.get(pdfPath));
            // 将.docx转换为PDF
            PdfConverter.getInstance().convert(document, out, pdfOptions);
        } catch (Exception e) {
            log.error("格式转换发生错误,信息为: ", e);
            e.printStackTrace();
        }
        //文件验证
        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<String, String> 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<String, String> 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)

        如有不足支持, 欢迎大神们补充优化!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10JQK炸

如果对您有所帮助,请给点鼓励吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值