【浏览器实现文件下载到指定的目录中,或者多级创建目录】

声明:以下方式仅限用谷歌64位浏览器,别的浏览器不能保证可以完美适配,

此种写法只有在浏览器的访问地址是**https://开头或者localhost://开头的时候才可以使用,如果http://**开头的网页也想下载文件到指定的目录下请看文章最后面的操作

一、首先请求接口获取要下载的文件列表

返回的数据结构如下:

  1. fileDir : 文件下载的位置 “a/b/c/d
  2. filePath: 下载的文件链接 “http://192.168.10.25:9000//mjhpre-pro/052543/07/02/test.pdf
    http://192.168.10.25:9000 表示minio的访问地址 我这里用的文件服务器是minio,所以是minio的文件下载链接
    mjhpre-pro 表示文件存储桶名称
    /052543/07/02/test.pdf 表示文件实际存储的位置
  3. fileName: 文件名称 “test.pdf
List<Map<String, String>> resultList = new ArrayList<>();
//模拟一条数据
Map<String, String> obj = new HashMap<>();
obj.put("fileDir", "a/b/c/d");
obj.put("filePath", "http://192.168.10.25:9000//mjhpre-pro/052543/07/02/test.pdf");
obj.put("fileName", "test.pdf");
resultList.add(obj);
return resultList;

二、接口数据通过前端Js下载

<!-- 文件下载下来保存的路径 -->
    <div>
      <el-input v-model="fileDir" size="small" :disabled="true" style="width: 500px">
        <template slot="prepend">文件下载目录为:</template>
      </el-input>
      <el-button type="success" icon="el-icon-download" size="small" :disabled="isDownloadDisabled" style="margin-left: 20px" @click="selectFileType">下载文件</el-button>
    </div>

    <div v-if="selectXmId" class="downloadProgress" style="margin-top: 10px;">
      <el-progress :text-inside="true" :stroke-width="18" :percentage="totalProgressPercent" status="success"></el-progress>
      <p>文件下载总进度:{{ totalProgressPercent }}%</p>
    </div>
<!-- 选择文件下载的类型弹窗 -->
    <div>
      <el-dialog title="选择归档文件类型" :visible.sync="dialogVisible" style="height: 400px" width="40%" :close-on-click-modal="false" :show-close="false">
        <div slot="title" class="dialog-title">
          <span>选择归档文件类型</span>
        </div>
        <div style="margin-top: 10%;margin-bottom: 10%;">
          <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox>
          <div style="margin: 15px 0;"></div>
          <el-checkbox-group v-model="checkedFileTypes" @change="handleCheckedCitiesChange">
            <el-checkbox v-for="item in fileTypes" :label="item.label" :key="item.value">{{item.label}}</el-checkbox>
          </el-checkbox-group>
        </div>
        <div slot="footer" class="dialog-footer" style="text-align: center;">
          <el-button type="primary" icon="el-icon-success" size="small" @click="downloadArchivedFile">确认</el-button>
          <el-button icon="el-icon-error" size="small" @click="cancel">取消</el-button>
        </div>
      </el-dialog>
    </div>
  data() {
    return {
      fileDir: "",
      selectXmId: "",
      dialogVisible: false,
      isIndeterminate: true,
      checkAll: false,
      checkedFileTypes: ['支持证明文件','辅助工具文件','其他文件'],
      fileTypes: [
        {label: '支持证明文件', value: '2'},
        {label: '辅助工具文件', value: '1'},
        {label: '其他文件', value: '3'},
      ],
      totalProgressPercent: 0,   // 总进度百分比
    };
  },
  computed: {
    isDownloadDisabled() {
      return !this.selectXmId;
    }
  },
methods: {
// 点击打开选择下载文件类型的弹窗
selectFileType(){
      if (!this.selectXmId) {
        return this.$message.warning("请先选择项目进行下载");
      }
      //先打开弹窗选择下载归档文件的类型
      this.dialogVisible = true;
},
//点击全选
handleCheckAllChange(val){
      this.checkedFileTypes = val ? this.fileTypes.map(item => item.label) : [];
      this.isIndeterminate = false;
},
//点击单个选择
handleCheckedCitiesChange(value) {
      let checkedCount = value.length;
      this.checkAll = checkedCount === this.fileTypes.length;
      this.isIndeterminate = checkedCount > 0 && checkedCount < this.fileTypes.length;
},

}

前端要实现对列下载文件需要安装依赖并引入 npm install p-queue@6.6.2 --save
本人node版本14.19,不指定版本安装p-queue依赖会报错
高版本的node没有实验需不需要指定版本安装
import PQueue from ‘p-queue’;


   // 这个函数中的代码是实现自定义下载文件路径的关键 !!!重点
    // 下载批量文件
    async downloadArchivedFile() {
      try {

        //根据勾选的文件类型名称获取文件类型值
        const fileTypeList = this.checkedFileTypes.map(item => {
          return this.fileTypes.find(type => type.label === item).value;
        });
        if(fileTypeList.length === 0){
          return this.
          $message.warning("请至少选择一种文件类型");
        }
        let fileTypes = fileTypeList.join(",");

        // 弹出系统选择目录对话框,让用户选择保存文件的目录
        //这一步的代码限制了必须是https:// 和  localhost:// 开头的请求才可以打开系统文件目录进行选择,如果不是这两种形式的网页,代码执行到这一行控制台直接报错
        //比如说我这里打开的弹窗选择的是D:\down
        const dirHandle = await window.showDirectoryPicker();
        this.fileDir = dirHandle.name;
        localStorage.setItem(this.username, this.fileDir);
        console.log('所选下载目录为:', dirHandle);

        const params = {
          xmId: this.selectXmId,
          fileTypes: fileTypes,
        }

        this.dialogVisible = false;

        //根据条件获取到需要下载的文件列表,数据结构就是第一点中模拟的数据结构
        // 请求接口获取需要下载的文件生成的下载链接列表
        const res = await fwArchiveFileList(params);
        if (res.code !== 200 || !res.data || res.data.length === 0) {
          this.$message.warning("找不到需要下载的文件");
          return;
        }
        
        //这里是用来分页的,如果用请求接口进行分页也可以,但是我这里的业务逻辑用接口进行分页比较繁琐,我直接用js实现的分页,一次性返回所有的数据,每次只处理100条
        const filelist = res.data; // 接口一次性返回了所有的数据,例如有 10000 条数据
        const TOTAL_FILES = filelist.length;
        const PAGE_SIZE = 100; // 每页处理 100 个文件
        const CONCURRENCY = 5; // 最大并发数

        // 初始化进度条,每次下载完成一个文件就更新一下进度条,这种方式获取下载进度,页面重新进入,进度条就消失了,因为这是通过前端的文件流下载,不是通过接口实时查询的数据,没有持久化操作
        this.totalProgressPercent = 0;
        let completedCount = 0;

        // 创建并发队列,每次只会同时有5个文件下载链接处于下载中,不会一次性下载过多的文件导致资源异常占用
        const downloadQueue = new PQueue({ concurrency: CONCURRENCY });

        // 前端模拟分页
        let pageNum = 0;
        let hasMore = true;


        while (hasMore) {
          const start = pageNum * PAGE_SIZE;
          const end = start + PAGE_SIZE;
          const pageItems = filelist.slice(start, end);

          if (pageItems.length === 0) {
            hasMore = false;
            continue;
          }

          // 创建防抖更新进度函数
          const debouncedUpdate = this.debounce(() => {
            this.totalProgressPercent = Math.round((completedCount / TOTAL_FILES) * 100);
          }, 1000); // 每 1000ms 更新一次进度条

          for (const item of pageItems) {
            downloadQueue.add(async () => {
              try {
               // 这个js中是文件下载的操作
                await this.saveFileToDirectory(dirHandle, item);
              } catch (e) {
                console.error(`下载失败: ${item.fileName}`, e);
              } finally {
                completedCount++;
                debouncedUpdate();
              }
            });
          }

          pageNum++;
        }

        await downloadQueue.onIdle(); // 等待所有任务完成
        this.$message.success("全部文件下载完成");

      }catch (err){
        console.error(err);
        this.$message.info("用户取消选择或下载出错");
      }
    },
    // 自定义防抖函数
    debounce(fn, delay) {
      let timer;
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, args);
        }, delay);
      };
    },

    // 定义一个异步函数 saveFileToDirectory,接收文件 URL 和建议的文件名作为参数
    async saveFileToDirectory(dirHandle,item) {
      const url = item.filePath;
      let basePath = item.fileDir;
      let suggestedName = item.fileName;
      try {
        // 创建所有父级目录
        //先创建指定的文件目录  例如 “”a/b/c/d“”
        const targetDir = await this.createDirectoryRecursively(dirHandle, basePath);
        //处理特殊文件后缀不能下载的问题,如果你没有.dll类型的文件这一步不需要
        const safeName = suggestedName.replace(/\.dll$/i, '.dll_.tmp');

        // 在选定的目录中创建或获取指定名称的文件句柄,{ create: true } 表示如果文件不存在则创建
        const fileHandle = await targetDir.getFileHandle(safeName, {create: true});

        // 创建一个可写入的文件流
        const writableStream = await fileHandle.createWritable();

        // 使用 fetch 获取远程文件的数据流
        const response = await fetch(url);

        // 获取响应体的读取器,用于分块读取文件内容
        const reader = response.body.getReader();

        // 获取可写流的写入器
        const writer = writableStream;

        // 循环读取并写入数据
        while (true) {
          const {done, value} = await reader.read();

          // 如果数据已经读取完毕,则退出循环
          if (done) break;

          // 将读取到的数据块写入本地文件
          await writer.write(value);
        }

        // 关闭写入流,确保所有数据被正确保存
        await writer.close();

      } catch (e) {
        console.error("保存文件出错:" + suggestedName, e);
        this.$message.error("文件保存失败,请检查文件名: " + suggestedName);
      }

    },

    /**
     * 递归创建多级目录
     * @param {FileSystemDirectoryHandle} baseDir 当前目录句柄
     * @param {string} path 多级路径,例如 'a/b/c'
     * @returns {Promise<FileSystemDirectoryHandle>} 最终目录句柄
     */
    async createDirectoryRecursively(baseDir, path) {
      const names = path.split('/').map(name => name.trim()).filter(name => name.length > 0);
      let currentDir = baseDir;

      for (const name of names) {
        currentDir = await currentDir.getDirectoryHandle(name, { create: true });
      }

      return currentDir;
    },

以上代码最终下载下来的文件就保存在D:\down\a\b\c\d\test.pdf

三、http://开头的网页下载文件所需要的配置

注:网页地址不是**https://开头或者localhost://**开头,想用这种方式下载,以下操作也可以实现

1、在谷歌浏览器新的标签页输入地址chrome://flags并打开。

2、在搜索框输入unsafe;

3、把要指定系统位置下载文件的网址输入(复制更为准确)到文本框内,例如:http://192.168.10.25,http://192.168.10.26 多个地址用英文逗号分隔;

4、文本框右边蓝色选项,选择已启用(Enabled);

5、点击右下角的重启(Relaunch)按钮,重启浏览器;
在这里插入图片描述
如果别的小伙伴有不同的看法,可以联系博主讨论,博文中有理解不到位的地方各位小伙伴也可以积极指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值