vue django实现大文件切片上传功能

1、前端代码

<template>
  <vxe-modal
    v-model="show"
    class="add-mark-dialog"
    :title="title"
    width="650"
    height="600"
    :show-footer="true"
    destroy-on-close
    @close="handleClose"
  >
    <div class="main-modal-body">
      <div class="form-wrap">
        <el-form ref="form" :model="form" :rules="rules" label-width="140px">
          <el-row>
            <el-col :span="24">
              <el-form-item label="任务名称:" prop="task_name">
                <el-input v-model="form.task_name" placeholder="请输入任务名称"></el-input>
              </el-form-item>
            </el-col>
            <el-col :span="24">
              <el-form-item v-if="type == 'add'" label="上传文件:" prop="file_address">
                <el-upload action :auto-upload="false" :show-file-list="false" :on-change="handleChange">
                  <div class="el-upload__text" style="color:#409EFF;"><i class="el-icon-upload"></i><em>点击上传</em></div>
                  <div class="el-upload__tip" slot="tip">只能上传zip格式文件,且大小不超过 10 GB 的视频</div>
                </el-upload>
                <div class="progress-box">
                  <!-- <span>上传进度:{{ percent.toFixed() }}%</span> -->
                  <el-progress :percentage="percent"></el-progress>
                  <!-- <el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter }}</el-button> -->
                </div>
              </el-form-item>
              <el-form-item v-else-if="type == 'edit'" label="已上传文件:" prop="file_address">
                <span style="word-break: break-all" v-for="(file, index) in imgUrlList" :key="index">{{
                  file.real_address
                }}</span>
              </el-form-item>
            </el-col>
          </el-row>
        </el-form>
      </div>
    </div>
    <template v-slot:footer>
      <div class="a-r">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="handleSubmit" :loading="btnLoading" :disabled="btnLoading">保存</el-button>
      </div>
    </template>
  </vxe-modal>
</template>

<script>
import SparkMD5 from 'spark-md5'
import axios from 'axios'
import { mapState } from 'vuex'
import util from '@/libs/util'
import { upload, createImgMarkTask, updateImgMarkTask, chunkMerge } from '../api'
const defaultForm = {
  task_name: ''
}
export default {
  name: 'AddModal',
  components: {},
  props: {
  },
  filters: {
    btnTextFilter(val) {
      return val ? '暂停' : '继续'
    }
  },
  data() {
    return {
      title: '新增',
      type: 'add', // add  edit view
      btnLoading: false,
      show: false,
      form: Object.assign({}, defaultForm),
      detail: null,
      tagarr: [],
      addTagVisible: false,
      typearr: [],
      addTypeVisible: false,
      inputValue: '',
      inputValue1: '',
      upHeaders: null,
      fileList: [], // el-upload绑定值
      imgUrlList: [], // 真实上传值
      fileAddress: '', // 手动输入上传文件地址
      rules: {
        task_name: [
          { required: true, message: '请输入任务名称', trigger: 'blur' },
          { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
        ]
      },
      percent: 0,
      // fileAddress: '',
      upload: true,
      percentCount: 0
    }
  },
  computed: {
    ...mapState('admin', {
      base_url: state => state.settings.base_url
    })
  },
  watch: {},
  methods: {
    async handleChange(file) {
      if (!file) return
      this.percent = 0
      this.fileAddress = ''
      // 获取文件并转成 ArrayBuffer 对象
      const fileObj = file.raw
      let buffer
      try {
        buffer = await this.fileToBuffer(fileObj)
      } catch (e) {
        console.log(e)
      }

      // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
      const chunkSize = 2097152
      const chunkList = [] // 保存所有切片的数组
      const chunkListLength = Math.ceil(fileObj.size / chunkSize) // 计算总共多个切片
      const suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名

      // 根据文件内容生成 hash 值
      const spark = new SparkMD5.ArrayBuffer()
      spark.append(buffer)
      const hash = spark.end()

      // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
      let curChunk = 0 // 切片时的初始位置
      for (let i = 0; i < chunkListLength; i++) {
        const item = {
          chunk: fileObj.slice(curChunk, curChunk + chunkSize),
          fileName: `${hash}_${i}.${suffix}`, // 文件名规则按照 hash_1.jpg 命名
          chunkNumber: i, // 当前分片索引
          totalChunks: chunkListLength, // 总共分片
          identifier: hash // 文件hash 唯一值
        }
        curChunk += chunkSize
        chunkList.push(item)
      }
      this.chunkList = chunkList // sendRequest 要用到
      this.hash = hash // sendRequest 要用到
      this.sendRequest()
    },
    // 发送请求
    sendRequest() {
      const requestList = [] // 请求集合
      this.chunkList.forEach((item, index) => {
        const fn = () => {
          const formData = new FormData()
          formData.append('chunk', item.chunk)
          formData.append('fileName', item.fileName)
          formData.append('chunkNumber', item.chunkNumber)
          formData.append('totalChunks', item.totalChunks)
          formData.append('identifier', item.identifier)
          return axios({
            baseURL: util.baseURL(),
            url: '/api/img_mark_task/chunk_upload/',
            method: 'post',
            headers: { 'Content-Type': 'multipart/form-data', Authorization: 'JWT ' + util.cookies.get('token') },
            data: formData
          }).then(res => {
            console.log('res=====1', res)
            if (res.data.code === 2000) {
              // 成功
              if (this.percentCount === 0) {
                // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
                this.percentCount = 100 / this.chunkList.length
              }
              this.percent += this.percentCount // 改变进度
              this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
            }
          })
        }
        requestList.push(fn)
      })

      let i = 0 // 记录发送的请求个数
      // 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
      const complete = () => {
        chunkMerge({ hash: this.hash, totalChunks: requestList.length }).then(res => {
          console.log('res===', res)
          this.fileAddress = res.data.url
        })
        // axios({
        //   url: '/merge',
        //   method: 'get',
        //   params: { hash: this.hash }
        // }).then(res => {
        //   if (res.data.code === 0) {
        //     // 请求发送成功
        //     this.fileAddress = res.data.path
        //   }
        // })
      }
      const send = async () => {
        if (!this.upload) return
        if (i >= requestList.length) {
          // 发送完毕
          complete()
          return
        }
        await requestList[i]()
        i++
        send()
      }
      send() // 发送请求
    },
    // 按下暂停按钮
    handleClickBtn() {
      this.upload = !this.upload
      // 如果不暂停则继续上传
      if (this.upload) this.sendRequest()
    },
    // 将 File 对象转为 ArrayBuffer
    fileToBuffer(file) {
      return new Promise((resolve, reject) => {
        const fr = new FileReader()
        fr.onload = e => {
          resolve(e.target.result)
        }
        fr.readAsArrayBuffer(file)
        fr.onerror = () => {
          reject(new Error('转换文件格式发生错误'))
        }
      })
    },
    // 执行人保存
    handleSubmit() {
    },
  }
}
</script>

<style scoped lang="scss">
.main-modal-body {
  .form-wrap {
    padding: 16px 0 0 0;
  }
}
</style>
<style>
.add-mark-dialog.vxe-modal--wrapper.type--modal .vxe-modal--body {
  padding: 0;
}
</style>

2、后端代码

# 大文件分片上传
def deldir(dir):
    if not os.path.exists(dir):
        return False
    if os.path.isfile(dir):
        os.remove(dir)
        return
    for i in os.listdir(dir):
        t = os.path.join(dir, i)
        if os.path.isdir(t):
            deldir(t)#重新调用次方法
        else:
            os.unlink(t)
    os.removedirs(dir)#递归删除目录下面的空文件夹
class ChunkUploadViewSet(CustomModelViewSet):
	def chunk_upload(self, request, *args, **kwargs):
		file_name = request.POST.get('identifier')
		chunk_index = int(request.POST.get("chunkNumber"))
		total_chunk = int(request.POST.get("totalChunks"))
		upload_file = request.FILES["chunk"] # 二进制数据
		file_path = os.path.join(settings.MEDIA_ROOT, 'chunk_file', file_name)
		chunk_path = os.path.join(file_path, str(chunk_index))
		if not os.path.exists(file_path):
			os.makedirs(file_path)
		with open(chunk_path, 'wb+') as destination:
			for chunk in upload_file.chunks():
				destination.write(chunk)

		# print('file_name====',upload_file)
		res = {
			'file_name': file_name,
			"chunk_index": chunk_index,
			'status': 1
		}
		return DetailResponse(data=res, msg="获取成功")
	# 合并分片
	def chunk_merge(self, request, *args, **kwargs):
		file_name = request.GET.get("hash") # 文件hash
		total_chunk = int(request.GET.get("totalChunks")) # 总共分片
		file_path = os.path.join(settings.MEDIA_ROOT, 'chunk_file', file_name)
		chunks_list = list(set(os.listdir(file_path)))
		is_over = False
		# print('total_chunk===', total_chunk, 'chunks_list', len(chunks_list))
		if len(chunks_list) == total_chunk:
			is_over = True
		if is_over:
			# 所有的分片 必须按照分块顺序排序,否则 可能合并的文件顺序被打乱
			all_chunk = os.listdir(file_path)
			all_chunk.sort(key=lambda x: int(x))  # fig bug:  默认是按 '0' '11'这种字符串类型排序,会导致分片顺序错乱。
			target_path = os.path.join(settings.MEDIA_ROOT, 'chunk_file', file_name+".zip")
			target_path_temp = os.path.join(settings.MEDIA_ROOT, 'chunk_file', file_name+"temp")
			with open(target_path, "wb+") as f:
				for chunk in all_chunk:
					chunk_path = os.path.join(file_path, chunk)
					with open(chunk_path, "rb") as g:
						data = g.read()
						f.write(data)
			deldir(file_path)
		# print('file_name====', file_name)
		file_url = os.sep.join([settings.MEDIA_ROOT, 'chunk_file', file_name+".zip"])
		res = {
			"url": file_url,  # os.path.join(settings.MEDIA_ROOT, 'chunk_file', file_name+".zip")
			"fileName": file_name,
		}
		return DetailResponse(data=res, msg="获取成功")
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
文件切片上传是一种常见的文件上传方式,可以通过将大文件分成多个小文件,然后分别上传,最后在服务器端将这些小文件合并成一个完整的文件。这种方式可以避免上传文件时网络不稳定或中断的情况,同时也可以减轻服务器端的压力。 下面是一个基于 PHP 7.3 和 Vue 的大文件切片上传的示例: 前端部分: 1. 在 Vue 组件中定义一个 input 标签,用户选择需要上传文件后触发上传事件: ``` <template> <div> <input type="file" @change="uploadFile" /> </div> </template> ``` 2. 在 uploadFile 方法中,使用 FileReader API 将文件分割成多个小文件,然后使用 axios 库将这些小文件逐个上传到服务器: ``` <script> import axios from 'axios'; export default { methods: { uploadFile(event) { const file = event.target.files[0]; const chunkSize = 1024 * 1024; // 将文件分割为 1MB 的小文件 const totalChunks = Math.ceil(file.size / chunkSize); const chunks = []; for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); chunks.push(chunk); } const formData = new FormData(); formData.append('filename', file.name); formData.append('totalChunks', totalChunks); chunks.forEach((chunk, index) => { formData.append(`chunk${index}`, chunk); }); axios.post('/upload', formData).then(response => { console.log(response.data); }); } } }; </script> ``` 后端部分: 1. 创建一个 upload.php 文件,用于处理文件上传请求: ``` <?php if ($_SERVER['REQUEST_METHOD'] === 'POST') { $filename = $_POST['filename']; $totalChunks = $_POST['totalChunks']; $chunks = []; for ($i = 0; $i < $totalChunks; $i++) { $chunkName = "chunk{$i}"; if (isset($_FILES[$chunkName])) { $chunk = file_get_contents($_FILES[$chunkName]['tmp_name']); $chunks[] = $chunk; } } $content = implode('', $chunks); file_put_contents($filename, $content); echo 'File uploaded successfully'; } ``` 2. 在 PHP 配置文件 php.ini 中设置上传文件的最大大小和上传文件的最大数量: ``` upload_max_filesize = 100M max_file_uploads = 20 ``` 这样,就完成了一个基于 PHP 7.3 和 Vue 的大文件切片上传功能。需要注意的是,这里只是一个简单的示例,实际应用中还需要考虑上传进度、上传失败重试等问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值