Django大文件分块上传和分块下载

下面是简单的demo,可能有Bug,需要的看官拿去自己拓展吧。

views.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import stat
import base64
import shutil
import pickle
import rehash
import mimetypes
import posixpath
from django.conf import settings
from urllib.parse import unquote
from django.core.cache import caches
from django.utils._os import safe_join
from django.utils.http import http_date
from django.views.static import was_modified_since
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, Http404, FileResponse, HttpResponseNotModified


def remove_file(file_path):
    if not os.path.isfile(file_path):
        return
    os.remove(file_path)


def write_file(file_path_temp, file):
    if not os.path.isdir(os.path.dirname(file_path_temp)):
        os.makedirs(os.path.dirname(file_path_temp))
    try:
        with open(file_path_temp, 'ab') as destination:
            destination.write(file['code'])
    except OSError as exc:
        return exc.errno


def check_file(file, file_path, file_path_temp):
    # is_end表示整个文件都传输完毕了

    if file['percent'][0] == '0' and os.path.exists(file_path_temp):
        remove_file(file_path_temp)  # 开始上传时删除已有的暂存文件
    elif file['percent'][0] != '0' and not os.path.exists(file_path_temp):
        # 上传过程中暂存文件丢失
        # firefox浏览器调用接口删除文件并取消传输后还会上传一个文件块,这里做兼容
        return {'code': '1', 'msg': '暂存文件丢失,上传失败', 'is_end': True}

    # 校验文件内容及大小
    if not file['code']:
        return {'code': '1', 'msg': '不能上传空文件'}
    if not 0 < file['size'] <= settings.max_size:
        return {'code': '1', 'msg': '文件大小不符合要求'}

    # 获得已传输的文件大小
    file_size = len(file['code'])
    if os.path.exists(file_path_temp):
        file_size += os.path.getsize(file_path_temp)

    # 校验文件大小
    if not 0 < file_size <= settings.max_size:
        return {'code': '1', 'msg': '文件大小不符合要求'}
    if (file['percent'][1] == '100' and file_size != file['size']) or file_size > file['size']:
        return {'code': '1', 'msg': '文件大小校验失败'}

    # 生成已传输的文件块的md5值
    if file['percent'][0] == '0':
        file_hash = rehash.md5()
    else:
        file_hash = caches['default'].get(file_path_temp)
        if not file_hash:
            return {'code': '1', 'msg': '文件校验值过期'}
        file_hash = pickle.loads(base64.b64decode(file_hash.encode()))
    file_hash.update(file['code'])

    # 校验md5值,保证内容一致性
    if file['md5'] != file_hash.hexdigest():
        return {'code': '1', 'msg': '文件校验失败'}

    if file['percent'][1] == '100' or file_size == file['size']:  # 传输完毕的状态
        if write_file(file_path_temp, file) == 36:
            return {'code': '1', 'msg': '文件名过长'}
        shutil.move(file_path_temp, file_path)
        return {'code': '0', 'msg': '上传并提交成功', 'is_end': True}
    else:
        if write_file(file_path_temp, file) == 36:
            return {'code': '1', 'msg': '文件名过长'}

        # 暂存md5的hash值
        caches['default'].set(file_path_temp, base64.b64encode(pickle.dumps(file_hash)).decode(), 60)
        return {'code': '0', 'msg': '上传并提交文件块成功', 'is_end': False}


@login_required
def upload_file(request):
    file = {
        'name': unquote(request.META['HTTP_FILE_UPLOAD_NAME']),
        'percent': request.META['HTTP_FILE_UPLOAD_PERCENT'].replace('NaN-Infinity', '0-100').split('-'),
        'size': int(request.META['HTTP_FILE_UPLOAD_SIZE']),
        'md5': request.META['HTTP_FILE_UPLOAD_MD5'],
        'code': request.body
    }  # 文件分为不大于100KB的小块传输

    # 传输过程中直接跳到其他页面
    if file['percent'][1].startswith('http'):
        return JsonResponse({'code': '-4', 'msg': '传输已终止', 'is_end': True})

    file_path = os.path.join(settings.BASE_DIR, 'upload_files/%s' % file['name'])
    file_path_temp = file_path + '.temp'

    if file['percent'][0] == '0' and caches['default'].get(file_path):  # 同名文件上传冲突,不进行其它操作
        return JsonResponse({'code': '1', 'msg': '同名文件正在上传'})
    else:  # 标记文件正在上传
        caches['default'].set(file_path, 'uploading', 2)

    response = check_file(file, file_path, file_path_temp)
    # 文件上传失败或者上传完毕后,清理暂存文件和缓存
    if response['code'] != '0' or response['is_end']:
        remove_file(file_path_temp)
        caches['default'].delete_pattern(file_path + '*')
    return JsonResponse(response)


@login_required
def download_file(request, path, document_root=None):
    # 防止目录遍历漏洞
    path = posixpath.normpath(path).lstrip('/')
    fullpath = safe_join(document_root, path)
    if os.path.isdir(fullpath):
        raise Http404('Directory indexes are not allowed here.')
    if not os.path.exists(fullpath):
        raise Http404('"%(path)s" does not exist' % {'path': fullpath})

    statobj = os.stat(fullpath)

    # 判断下载过程中文件是否被修改过
    if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'),
                              statobj.st_mtime, statobj.st_size):
        return HttpResponseNotModified()

    # 获取文件的content_type
    content_type, encoding = mimetypes.guess_type(fullpath)
    content_type = content_type or 'application/octet-stream'

    # 计算读取文件的起始位置
    start_bytes = re.search(r'bytes=(\d+)-', request.META.get('HTTP_RANGE', ''), re.S)
    start_bytes = int(start_bytes.group(1)) if start_bytes else 0

    # 打开文件并移动下标到起始位置,客户端点击继续下载时,从上次断开的点继续读取
    the_file = open(fullpath, 'rb')
    the_file.seek(start_bytes, os.SEEK_SET)

    # status=200表示下载开始,status=206表示下载暂停后继续,为了兼容火狐浏览器而区分两种状态
    # 关于django的response对象,参考:https://www.cnblogs.com/scolia/p/5635546.html
    # 关于response的状态码,参考:https://www.cnblogs.com/DeasonGuan/articles/Hanami.html
    # FileResponse默认block_size = 4096,因此迭代器每次读取4KB数据
    response = FileResponse(the_file, content_type=content_type, status=206 if start_bytes > 0 else 200)

    # 'Last-Modified'表示文件修改时间,与'HTTP_IF_MODIFIED_SINCE'对应使用,参考:https://www.jianshu.com/p/b4ecca41bbff
    response['Last-Modified'] = http_date(statobj.st_mtime)

    # 这里'Content-Length'表示剩余待传输的文件字节长度
    if stat.S_ISREG(statobj.st_mode):
        response['Content-Length'] = statobj.st_size - start_bytes
    if encoding:
        response['Content-Encoding'] = encoding

    # 'Content-Range'的'/'之前描述响应覆盖的文件字节范围,起始下标为0,'/'之后描述整个文件长度,与'HTTP_RANGE'对应使用
    # 参考:http://liqwei.com/network/protocol/2011/886.shtml
    response['Content-Range'] = 'bytes %s-%s/%s' % (start_bytes, statobj.st_size - 1, statobj.st_size)

    # 'Cache-Control'控制浏览器缓存行为,此处禁止浏览器缓存,参考:https://blog.csdn.net/cominglately/article/details/77685214
    response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
    return response

upload.js

import SparkMD5 from 'spark-md5'
import ajax_fetch from '@/config/ajax'
import { MessageBox, Message } from 'element-ui'

function onUpload(file, size, name, start, end, md5) {
    return new Promise((resolve, reject) => {
        ajax_fetch('/upload/', file, 'POST', {
            'file-upload-name': encodeURIComponent(name),
            'file-upload-percent': start + '-' + end,
            'file-upload-size': size,
            'file-upload-md5': md5
            'x-csrftoken': window.localStorage.getItem('csrfmiddlewaretoken'),
        }).then((res) => {
            resolve(res);
        }).catch(err => {
            reject(err)
        });
    })
}

async function uploadFile() {
    if(!document.getElementsByName('uploadForm')[0].files[0]){
        return;
    }
    return new Promise(function (resolve, reject) {
        var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,  // 兼容切片方法
            file = document.getElementsByName('uploadForm')[0].files[0],  // 文件对象
            chunkSize = 2 * 1024 * 1024,                             // 每块文件2MB
            chunks = Math.ceil(file.size / chunkSize),               // 文件分多少块
            currentChunk = 0,                                        // 已经传输了多少块
            md5 = '',
            spark = new SparkMD5.ArrayBuffer(),
            fileReader = new FileReader();
        fileReader.onload = async function (e) {
            spark.append(e.target.result);
            md5 = spark.digest();
            var start_p = (currentChunk / chunks) * 100,
                end_p = ((currentChunk + 1) / chunks) * 100;
                let response = await onUpload(e.target.result, file.size, file.name, start_p, end_p, md5);
            if (response.code == 0 && response.is_end) {
                // 传输文件结束
                resolve(response);
                Message.success(file.name + '文件上传成功!');
            } else if (response.code == 0 && !response.is_end) {
                if (/upload-file/.test(window.location.href)) {  //文件块传输成功,传输下一块
                    currentChunk++;
                    loadNext();
                } else {  // 传输过程中跳转到其他页面了
                    onUpload(e.target.result, file.size, file.name, 1, window.location.href, md5);
                }
            } else if (response.code == -4) {
                spark.end();  // 释放内存
            } else {
                spark.end();  // 释放内存
                Message.error(response.msg);
            }
        };
        function loadNext() {
            var start = currentChunk * chunkSize,
                end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
            fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));  // 这里会触发onload事件
        }
        loadNext();
    }).catch((err)=>{
        console.log('文件传输失败')
    })
}

export default uploadFile

urls.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from site import views
from django.urls import re_path

urlpatterns = [
    re_path(r'^/upload/?$', views.upload_file),
    re_path(r'^/download/(?P<path>.*)$', views.download_file, {'document_root': settings.MEDIA_ROOT}),
]

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现上文件到指定路径,可以使用Django自带的FileField和Form组合来完成。具体步骤如下: 1.在forms.py中定义一个Form类,用于处理上的表单数据,例如: ```python from django import forms class UploadForm(forms.Form): file = forms.FileField() ``` 2.在views.py中定义一个视图函数,用于接收用户提交的表单数据,并将上文件保存到指定路径中,例如: ```python import os from django.conf import settings from django.shortcuts import render from .forms import UploadForm def upload(request): if request.method == 'POST': form = UploadForm(request.POST, request.FILES) if form.is_valid(): file = request.FILES['file'] path = os.path.join(settings.MEDIA_ROOT, file.name) with open(path, 'wb') as f: for chunk in file.chunks(): f.write(chunk) return render(request, 'upload_success.html') else: form = UploadForm() return render(request, 'upload.html', {'form': form}) ``` 这里使用了Python的open函数来打开一个文件并写入上文件内容,通过chunks方法将文件内容分块写入,以避免一次性读取大文件时占用过多内存。 3.在upload.html模板中定义一个表单,用于上文件,并将其提交到视图函数中处理,例如: ```html <form method="post" enctype="multipart/form-data"> {% csrf_token %} {{ form.as_p }} <button type="submit">上</button> </form> ``` 4.在settings.py中设置MEDIA_ROOT和MEDIA_URL,用于指定上文件的存储路径和访问路径,例如: ```python MEDIA_ROOT = os.path.join(BASE_DIR, 'media') MEDIA_URL = '/media/' ``` 5.最后,需要在项目的urls.py中添加一个URL模式,用于将上文件的访问路径映射到MEDIA_ROOT下的实际文件路径,例如: ```python from django.conf.urls.static import static from django.conf import settings urlpatterns = [ # ... ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ``` 这样就可以实现上文件到指定路径了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值