上传PDF,预览PDF,下载PDF,定时清理冗余文件的实现

此次实现是基于SpringBoot、vue3、elementplus。

目录

1、上传文件前端组件

2 、文件上传后端处理

3、PDF文件预览前端组件

4、后端提供的下载接口

5、定时删除冗余的文件


1、上传文件前端组件

<el-form-item label="上传文件">
        <el-upload
            ref="uploadRef"
            :action="uploadUrl"
            :before-upload="beforeUpload"
            :on-success="handleSuccess"
            :on-error="handleError"
            :limit="1"
            :show-file-list="false"
            :on-change="handleFileChange"
        >
          <template #trigger>
            <el-button type="primary">点击选择PDF文件</el-button>
          </template>
          <template #tip>
            <div class="el-upload__tip">
              PDF格式,小于2M。
            </div>
          </template>
        </el-upload>
</el-form-item>

1.1
 ref="uploadRef"上传组件的引用
在上传成功后,清空文件列表上次上传的文件时用到

1.2
:action上传的url地址,

组件里面:
uploadUrl: this.$uploadUrl,//上传文件的url,在环境变量里面配置,生产环境和开发环境的地址不一样

main.js:

app.config.globalProperties.$uploadUrl = import.meta.env.VITE_UPLOAD_URL;


.env.development:

VITE_UPLOAD_URL=http://localhost:8083/api/files/upload
#后端文件上传接口

.env.production:

VITE_UPLOAD_URL=/apis/api/files/up

1.3
:before-upload上传前的处理方法,主要用来验证文件格式和文件大小限制

    // 在上传之前进行的检查,包括文件大小和格式,返回值:false阻止上传,true放行
    beforeUpload() {
      console.log(this.fileList[0])//输出查看文件的类型和文件大小如何定义的
      const isPDF = this.fileList[0].raw.type === 'application/pdf';
      const isLessThan2M = this.fileList[0].size / 1024 / 1024 < 2;
      if (!isPDF) {
        messageTip('请上传PDF格式的文件', 'error');
        return false;
      }
      if (!isLessThan2M) {
        messageTip('文件大小不能超过2M', 'error');
        return false;
      }
      return true;
    },

1.4
:on-success 上传成功的处理方法

    // 处理上传成功的回调
    handleSuccess(response, file) {
      messageTip('文件上传成功', 'success')
      //当文件上传成功后,清空选择的文件,以便下次上传
      this.$refs.uploadRef.clearFiles();
      if (this.dialogQua) {
        //资质名称
        this.QQualification.qualificationName = response.data.filename;
        this.QQualification.qualificationStoragePath = response.data.uploadFile;
      } else if (this.dialogEditChar) {
        this.QChargeItem.proofMaterialPath = response.data.uploadFile;
      }
    },

1.5
:on-error上传失败的处理方法

    // 处理上传失败的回调
    handleError(error, file) {
      messageTip('文件上传失败', 'error');
      // 可以在这里对上传失败的原因进行分析和处理,比如记录错误日志等
    },

1.6
:limit限制文件的上传数量
:limit="1"   单个文件上传

1.7
:show-file-list是否已经上传的文件列表
:show-file-list="false"   不在组件下面显示已经上传的文件列表

1.8
:on-change文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用

     //选择框改变时,拿到要上传的文件列表
    handleFileChange(file, fileList) {
      this.fileList = fileList;
    },

2 、文件上传后端处理

2.1controller

      //处理文件上传
    @PostMapping(value = "/upload")
    public R upload(@RequestParam(value = "file") MultipartFile file) {
        //真正的文件上传方法
        String uploadFile = fileUploadDownloadUtils.uploadFile(file);
        //uploadFile:D://pointProject/uploads/2024/11/22/43979372.pdf
        //获取文件原来的名称,不包含扩展名
        String originalFilename = file.getOriginalFilename();
        int indexOf = originalFilename.lastIndexOf(".");
        String filename =originalFilename.substring(0,indexOf);
        Map<String, String> map = new HashMap<>();
        map.put("filename", filename);
        map.put("uploadFile", uploadFile);
        return R.OK(map);
    }

2.2fileUploadDownloadUtils

     //自定义一个文件上传路径
    private static final String UPLOAD_DIR = "D:/pointProject/uploads";


    // 真正的文件上传方法
    public String uploadFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("上传文件不能为空");
        }

        try {
            //获取文件上传的全路径,单独写一个方法
            String filePath = generateFilePath(file);
            int indexOf = filePath.lastIndexOf("/");
            //上传位置的目录字符串
            String fileDir = filePath.substring(0,indexOf );
            //文件的全路径,保存文件使用
            Path path = Paths.get(filePath);
            //文件存放目录的路径,创建目录使用
            Path dir=Paths.get(fileDir);
            //如果文件目录不存在,就创建文件目录
            if (!Files.exists(dir)) {
                try {
                    Files.createDirectories(dir);
                    System.out.println("目录 " + fileDir + " 不存在,已成功创建。");
                } catch (IOException e) {
                    System.out.println("创建目录 " + fileDir + " 时出错:" + e.getMessage());
                }
            } else {
                System.out.println("目录 " + fileDir + " 已经存在。");
            }
            //保存上传的文件
            file.transferTo(path);
            return filePath;
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败", e);
        }
    }


    // 生成文件存储路径,格式为:年/月/日/时间戳+原来文件扩展名
    private String generateFilePath(MultipartFile file) {
        LocalDateTime now = LocalDateTime.now();

        //日期格式
        DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("yyyy");
        DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MM");
        DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("dd");

        //获取文件名
        String originalFileName = file.getOriginalFilename();
        //获取文件扩展名
        String fileExtension = StringUtils.getFilenameExtension(originalFileName);
        //获取时间戳,用来生成唯一的文件名
        long timestamp = new Date().getTime();

        return UPLOAD_DIR + "/" + now.format(yearFormatter) + "/"  +
                now.format(monthFormatter) + "/" + now.format(dayFormatter) +
                "/"  + timestamp + "." + fileExtension;
    }

3、PDF文件预览前端组件

3.1定义一个用于展示预览的抽屉

  <!--  PDF预览抽屉-->
  <el-drawer v-model="drawer" size="45%" :show-close="false">
    <template #header>
      <h4>PDF预览</h4>
      <el-button @click="downloadPDF" type="primary">下载</el-button>
    </template>
    <template #default>
<!--      这个div用来挂载需要预览的PDF-->
      <div id="pdfContainer"></div>
    </template>
    <template #footer>
      <div style="flex: auto">
        <el-button @click="cancelClick">关闭</el-button>
      </div>
    </template>
  </el-drawer>

3.2定义一个调用的组件

      <el-table-column label="查看资质" width="140">
        <template #default="scope">
          <div v-if="scope.row.qualificationStoragePath">
            <el-button @click="PDFView(scope.row.qualificationStoragePath)"> 点击预览</el-button>
          </div>
          <div v-else>-</div>
        </template>
      </el-table-column>

3.3PDF预览方法

    //PDF预览
    PDFView(path) {
      //显示抽屉
      this.drawer = true;
      this.pdfPath = path;//为下载提供路径
      if (this.drawer) {
        if (this.pdfPath) {
          axios.get(`/api/files/download?path=${path}`, {responseType: "arraybuffer"})
              .then(resp => {
                //将后端返回的数据转换成一个前端的pdfjs能够使用的url
                const url = window.URL.createObjectURL(new Blob([resp.data], {type: 'application/pdf'}));

                // 使用pdfjsLib的getDocument方法创建一个加载PDF文件的任务
                const loadingTask = pdfjsLib.getDocument(url);
                // 当加载任务成功完成后,会得到一个PDF对象(pdf),这里通过promise.then来处理加载成功的情况
                loadingTask.promise.then(pdf => {
                  //先加载所有页面,防止页面顺序乱了,
                  //定义一个数组,用来存储全部页面
                  let pagePromises = [];
                  // 获取PDF文件的总页数
                  let pageNum = pdf.numPages;
                  // 循环遍历每一页,从第一页(pageNumber = 1)开始,直到总页数(pageNum)
                  for (let pageNumber = 1; pageNumber <= pageNum; pageNumber++) {
                    pagePromises.push(pdf.getPage(pageNumber));
                  }
                  //返回所有页面
                  return Promise.all(pagePromises);
                }).then(pages => {//开始渲染处理
                  //循环全部页面
                  pages.forEach(page => {
                    const scale = 1; // 可调整缩放比例
                    // 根据设置的缩放比例获取页面的视图参数,用于后续在canvas上正确显示页面内容
                    const viewport = page.getViewport({scale});
                    
                    // 创建一个用于渲染PDF页面的canvas元素
                    const canvas = document.createElement('canvas');
                    // 获取canvas的2D绘图上下文,用于在canvas上绘制内容
                    const context = canvas.getContext('2d');

                    // 设置canvas的固定高度和宽度,这里统一设置,不再根据页面视图大小动态设置
                    canvas.height = 800;
                    canvas.width = 600;

                    // 创建一个渲染上下文对象,包含了要在哪个canvas上下文进行渲染以及页面的视图参数
                    const renderContext = {
                      canvasContext: context,
                      viewport: viewport
                    };
                    // 使用页面的render方法,将页面内容按照渲染上下文的设置渲染到canvas上
                    page.render(renderContext);

                    // 获取用于放置所有渲染后的PDF页面的容器元素,id为'pdfContainer'
                    const pdfContainer = document.getElementById('pdfContainer');
                    // 将渲染好的canvas元素添加到pdfContainer中,这样就可以在页面上显示出来了
                    pdfContainer.appendChild(canvas);
                  });
                });

              }).catch(error => {
            console.log(error)
          })
        }
      }
    },

3.4关闭抽屉时清空内容

    //关闭抽屉
    cancelClick() {
      this.drawer = false;
      //清空PDF
      const container = document.getElementById('pdfContainer');
      container.innerHTML = '';
    },

4、前端下载PDF

    //下载pdf
    downloadPDF() {
      if (this.drawer) {
        let path = this.pdfPath;
        axios.get(`/api/files/download?path=${path}`, {responseType: "arraybuffer"})
            .then(resp => {
              //获取文件名
              let fileName = path.substring(path.lastIndexOf("/") + 1);

              // 同时指定该Blob对象的MIME类型为'application/octet-stream',表示它是一个字节流形式的数据,可用于传输各种二进制文件
              const blob = new Blob([resp.data], {type: 'application/octet-stream'});

              // 在文档对象模型(DOM)中创建一个<a>标签元素,通常用于创建超链接
              const link = document.createElement('a');

              // 设置<a>标签的'download'属性,拼接后再加上".pdf"组成的字符串
              // 这样设置后,当用户点击这个链接进行下载时,下载的文件名将以此字符串命名
              link.setAttribute('download', fileName);

              // 通过window.URL.createObjectURL方法为创建的Blob对象生成一个临时的可访问的URL地址,并将这个地址设置为<a>标签的'href'属性
              // 使得该<a>标签指向这个由Blob对象生成的临时URL,以便后续能够通过点击这个链接来访问和下载Blob对象所代表的文件内容
              link.href = window.URL.createObjectURL(blob);

              // 将创建好的<a>标签元素添加到文档的<body>部分,使其成为页面DOM结构中的一部分
              // 这样用户在浏览器页面中就能看到这个下载链接(虽然可能看不到具体的样式,具体样式可根据后续的CSS设置来展现)
              document.body.appendChild(link);

              // 模拟用户点击刚刚添加到页面中的<a>标签,触发下载操作
              // 当点击这个链接时,浏览器会根据之前设置的'download'属性值来命名下载的文件,并从通过'href'属性指定的临时URL地址获取文件内容进行下载
              link.click();
              messageTip("下载成功", "success");
            }).catch(err => {
          messageTip(err, "error")
        })
      }
    },

5、后端提供的下载接口

无论前端是预览还是下载,对于后端来说都是下载接口。用同一个controller解决。

     //处理文件下载 这里前端传过来完整的路径,也可以根据ID从数据库中查询
    @GetMapping("/download")
    public ResponseEntity<FileSystemResource> download(@RequestParam("path") String path) {
        File file = new File(path);
        if (!file.exists()) {
            //没找到文件
            return ResponseEntity.notFound().build();
        }
        String fileName = path.substring(path.lastIndexOf("/") + 1);

        //设置返回的头
        HttpHeaders headers = new HttpHeaders();
        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
        headers.add("Pragma", "no-cache");
        headers.add("Expires", "0");
        headers.add("Content-Disposition", "attachment; filename=" + fileName);

        //构建文件资源
        FileSystemResource resource = new FileSystemResource(file);

        return ResponseEntity.ok()
                .headers(headers)
                .contentLength(file.length())
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(resource);
    }

6、定时删除冗余的文件

6.1清理方法


    // 定时清理冗余文件方法(这里在DataTask定时任务中调用此方法)
    public void cleanRedundantFiles() {
        //查询相关数据库表中的文件路径,集中到一个集合中,文件路径集合1
        //查询出来有空串,null,过滤一下
        //解决一下文件路径斜杠不一致的问题
        
        //查询数据库中表1中包含的文件路径
        List<String> charPaths=qChargeItemsDao.findAllPath().stream().filter(path->{
            return StringUtils.hasText(path);
        }).map(path->{
            return path.replace("/",File.separator);
        }).collect(Collectors.toList());
        
        //查询数据库中表2中包含的文件路径
        List<String> qualificationPaths=qQualificationsDao.findAllPath().stream().filter(path->{
            return StringUtils.hasText(path);
        }).map(path->{
            return path.replace("/",File.separator);
        }).collect(Collectors.toList());

        //合并
        List<String> allDbPaths = new ArrayList<>();
        allDbPaths.addAll(charPaths);
        allDbPaths.addAll(qualificationPaths);

        //编写一个递归遍历指定文件夹下的所有文件路径,并返回一个文件路径的集合,文件路径集合2
        File fileDir = new File(UPLOAD_DIR);
        List<String> allFilesPaths = getAllFilesPaths(fileDir);


        //遍历集合2,如果不在集合1中就删除
        // 遍历所有文件路径,如果不在数据库记录中,则删除该文件
        for (String filePath : allFilesPaths) {
            if (!allDbPaths.contains(filePath)) {
                if (!deleteFile(filePath)) {
                    // 如果文件删除失败,可以在这里进行日志记录等操作
                    System.out.println("无法删除文件:" + filePath);
                }
            }
        }

    }

6.2删除文件的工具方法

    //删除文件的工具方法
    public static boolean deleteFile(String filePath) {
        File file = new File(filePath);
        if (file.exists()) {
            log.info("文件路径:{}=>不在数据库中,已删除。",filePath);
            return file.delete();
        }
        return false;
    }


6.3递归获取指定文件目录下的所有文件的工具方法

    //递归获取指定文件目录下的所有文件的工具方法
    private List<String> getAllFilesPaths(File directory) {
        List<String> filePaths = new ArrayList<>();
        File[] files = directory.listFiles();
        if (files!= null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    // 递归调用,获取子文件夹中的文件路径
                    filePaths.addAll(getAllFilesPaths(file));
                } else {
                    filePaths.add(file.getAbsolutePath());
                }
            }
        }
        return filePaths;
    }


6.4定时任务类中编写方法

    /**
     * 每天晚上2点,定时清理数据库中没记录的冗余文件
     */
    @Scheduled(cron = "0 0 2 * * *")
    public void cleanRedundantFilesScheduled() {
        fileUploadDownloadUtils.cleanRedundantFiles();
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值