文件存储系统中有一个必不可少的功能就是文件的下载功能,下载的应该支持各种格式的文件。本项目中客户端使用的是vue3、axios和vuex,服务端使用的是Gin框架,下边就介绍文件下载部分的代码和主要步骤。

一、Gin框架部分:

  • 前两个函数的调用都是根据参数查找files表和user_files表中是否存在文件记录,如果文件不存在,则返回404,如果存在再执行后边的操作。
  • 第二部分是Header的配置,这里重点说三个配置信息。

    • c.Header("Access-Control-Expose-Headers", "Content-Disposition"):Content-Disposition中配置有文件名信息,通过Content-Disposition可以把文件名传递给客户端,客户端在取出文件名作为下载文件的名称。但是这一项默认是不公开的,如果不设置c.Header("Access-Control-Expose-Headers", "Content-Disposition")的话content-type就暴露不出去,客户端也接收不了,这一项配置的作用就是暴露该项,也是必须配置的。
    • c.Header("response-type", "blob"):如果客服端是以文件流的形式下载文件,服务端必须配置该项,否则下载下来的文件会报如下错误:文件格式不正确或已损坏。
    • c.File(fm.FileAddress):参数是文件的存储地址,这一项也是必须的,否则下载的文件是空的。
  • 请求的方式post和get都可以。
// DownloadHandler文件下载接口
func DownloadHandler(c *gin.Context) {
    fileSha1 := c.Request.FormValue("file_sha1")
    userId, _ := strconv.ParseInt(c.Request.FormValue("user_id"), 10, 64)
    userFile, err := se.FindUserFileByUserAndSha1(c, userId, fileSha1)
    if err != nil {
        fmt.Printf("Failed to find file, err is %s\n", err.Error())
        c.JSON(http.StatusNotFound, gin.H{
            "code": 0,
            "msg":  "找不到文件",
            "data": nil,
        })
        return
    }
    fm, err := se.FindFileMetaByFileSha1(c, fileSha1)
    if err != nil {
        fmt.Printf("Failed to find file, err is %s\n", err.Error())
        c.JSON(http.StatusNotFound, gin.H{
            "code": 0,
            "msg":  "找不到文件",
            "data": nil,
        })
        return
    }

    c.Header("Content-Type", "application/octet-stream")
    // 强制浏览器下载
    c.Header("Content-Disposition", "attachment;filename=\""+userFile.FileName+"\"")
    // 浏览器下载或预览
    c.Header("Content-Disposition", "inline;filename=\""+userFile.FileName+"\"")
    c.Header("Content-Transfer-Encoding", "binary")
    c.Header("Cache-Control", "no-cache")
    c.Header("Access-Control-Expose-Headers", "Content-Disposition")
    c.Header("response-type", "blob") // 以流的形式下载必须设置这一项,否则前端下载下来的文件会出现格式不正确或已损坏的问题
    c.File(fm.FileAddress)
}

二、vue配置

vue使用的是3版本,由于下载文件的图标和文件夹下的文件列表不在一个vue文件中,需要传递全局变量,所以对变量的状态管理使用vuex来进行,很方便。

import {
  createStore
} from "vuex";

export default createStore({
  // 变量
  state: {
    downloadFileSha1: "",
    radioId: 0,
  },
  mutations: {
    // 修改下载的文件的sha1
    EditDownloadFileSha1(state, param) {
      state.downloadFileSha1 = param
    },
    // 修改radioId的值
    EditRadioId(state, value) {
      state.radioId = value;
    }
  },
  actions: {
    // 修改下载的文件的sha1
    editDownloadFileSha1(context, payload) {
      context.commit("EditDownloadFileSha1", payload);
    },
    // 修改radioId的值
    editRadioId(context, payload) {
      context.commit("EditRadioId", payload);
    }
  },
  modules: {},
})
  • 文件列表页面选择要下载的文件,修改全局变量downloadFileSha1和radioId
<el-table
        :data="JSON.parse(JSON.stringify(this.$store.state.folderFiles))"
        style="width: 100%"
      >
        <el-table-column width="50" label="单选">
          <template #default="scope">
            <el-radio
              :label="scope.$index + 1"
              v-model="this.$store.state.radioId"
              @change="change(scope)"
            ></el-radio>
          </template>
        </el-table-column>
        <el-table-column prop="name" label="文件名" width="380">
          <template #default="scope" class="cell">
            <span style="margin-left: 10px" class="folder">{{
              scope.row.file_name
            }}</span>
          </template>
        </el-table-column>
        <el-table-column label="类型" width="180">
          <template #default="scope" class="cell">
            {{ scope.row.type }}
          </template>
        </el-table-column>
        <el-table-column prop="size" label="大小" width="180">
          <template #default="scope" class="cell">
            {{ scope.row.file_size }}
          </template>
        </el-table-column>
        <el-table-column label="修改时间" width="180">
          <template #default="scope" class="cell">
            <span>{{ scope.row.upload_at }}</span>
          </template>
        </el-table-column>
      </el-table>

<script>
import { FolderOpened } from "@element-plus/icons";
export default {
  components: {
    FolderOpened,
  },
  data() {
    return {};
  },
  methods: {
    // 单选框绑定值变化时触发的事件(选中的 Radio label 值)
    change(scope) {
      this.$store.dispatch("editRadioId", scope.$index + 1);
      this.$store.dispatch("editDownloadFileSha1", scope.row.file_sha1);
    },
  },
</script>
  • request.js文件中封装下载文件的请求,api.js文件中封装downlod方法
// request.js文件
export function download(url, data) {
  return axios({
    method: "get",
    url: `${baseUrl}${url}`,
    params: data,
    responseType: "blob", // 服务器返回的数据类型,这一项是必须的,流形式下载
  })
}

// api.js文件
import { download } from "./request";
const UserFileDownload = "/api/file/download"
export function DownloadFile(data) {
    return download(UserFileDownload, data);
}
  • header页面点击下载文件的图标根据全局根据全局变量下载发送请求下载文件,请求下载的请求成功返回之后需要对文件名做设置
<template>
<el-tooltip
    class="item"
    effect="light"
    content="下载文件"
    placement="bottom"
    >
    <el-icon :size="23" class="icons" @click="downloadFile">
        <Download />
    </el-icon>
</el-tooltip>
</template>
<script>
import { Download } from "@element-plus/icons";
import { DownloadFile } from "@/axios/api";
export default {
  components: {
    Download,
  },
  methods: {
    downloadFile() {
      var data = {
        file_sha1: this.$store.state.downloadFileSha1,
        user_id: localStorage.getItem("id"),
      };
      this.$confirm("是否确定下载该文件?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          DownloadFile(data)
            .then((res) => {
              if (!res) {
                return;
              }
              // 根据content-disposition获取文件名
              const disposition = res.headers["content-disposition"];
              let file_name = disposition.substring(
                disposition.indexOf("filename=") + 9,
                disposition.length
              );
              // iso8859-1的字符转换成中文
              file_name = decodeURI(escape(file_name));
              // 去掉双引号
              file_name = file_name.replace(/\"/g, "");

              let url = window.URL.createObjectURL(res.data);
              let link = document.createElement("a");
              link.style.display = "none";
              link.href = url;
              link.setAttribute("download", file_name);
              document.body.appendChild(link);
              link.click();
              window.URL.revokeObjectURL(link.href);
              document.body.removeChild(link);
            })
            .catch((err) => console.log(err));
          this.$message({
            type: "success",
            message: "下载成功!",
          });
        })
        .catch(() => {
          this.$message({
            type: "info",
            message: "已取消下载",
          });
        });
      this.$store.dispatch("editDownloadFileSha1", "");
      this.$store.dispatch("editRadioId", 0);
    },
}
</script>

至此,文件下载的的基本功能就开发完了。效果图如下:

项目地址:https://gitlab.com/liquanhui0...;项目还在开发中,功能正在完善中。


苦心僧
18 声望2 粉丝