• Python实现大文件分片上传和断点续传[转]


    原文链接:https://blog.csdn.net/jinixin/article/details/77545140
    版权声明:本文为CSDN博主「jinixin」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

    原文链接:https://www.pythonheidong.com/blog/article/147214/1d8572ec6eba60a325b3/

    作者:加班是一种习惯

    一、引言

    想借着这篇文章简要谈谈WebUploader大文件上传与Python结合的实现。

    WebUploader是百度团队对大文件上传的前端实现,而后端需要根据不同的语言自己实现。这里我采用Python语言的Flask框架搭建后端,配合使用Bootstrap前端框架渲染上传进度条,效果图在文章底部。

    WebUploader官网:http://fex.baidu.com/webuploader/

    WebUploader API:http://fex.baidu.com/webuploader/doc/index.html

    二、实施

    http协议并不是非常适合上传大文件,所以要考虑分片,即把大文件分割后再上传,而WebUploader所做的事,正是将一个大文件分片,一部分一部分的上传到服务器。在上传每个分片的http请求中,需要同时携带:

    1)该文件的唯一标识:task_id;
    2)该文件的分片总数:chunks;
    3)该分片在该文件所有分片中的位置:chunk;
    其中后两个WebUploader已经替我们自动上传了,而第一个task_id仅需要我们调用对应函数即可产生,然后再将其写入form-data。


    WebUploader是一个前端框架,所以接收文件的部分需要我们自己实现,而我选用了Python和其的Flask框架。
后端要做的是接收这一大堆分片,然后将它们重新合并成一个文件,那么有如下三个问题:
    1)如何判定某个分片上传后,是不是整个文件也上传结束了?
    WebUploader已经为我们解决了,详见下面代码。

    <script type="text/javascript">
    $(document).ready(function() {
        var task_id = WebUploader.Base.guid();        //产生task_id,唯一标识该文件
        var uploader = WebUploader.create({
            server: '/upload/accept',                 //服务器接收并处理分片的url地址
            formData: {
                task_id: task_id,                     //上传分片的http请求携带的数据
            },
        });
     
        uploader.on('uploadSuccess', function(file) { //当该文件所有分片均上传成功时调用该函数
            //上传的信息(文件唯一标识符,文件后缀名)
            var data = { 'task_id': task_id, 'ext': file.source['ext'], 'type': file.source['type'] };
            $.get('/upload/complete', data);          //ajax携带data向该url发请求
        });
    });
    </script>


    2)如何处理接收分片和将分片内容写入文件的关系?
    方案一:开一个字符串,一边接收分片,一边将分片里的内容读取出来后添加到字符串末尾;全部分片接收完毕后,再将字符串写入新文件中。

    方案二:创建一个文件,一边接收分片,一边将分片里的内容读取出来写入文件末尾。

    方案三:为每个分片创建一个新的临时文件来保存其内容;待全部分片上传完毕后,再按顺序读取所有临时文件的内容,将数据写入新文件中。

    前两个方案看似不错,但其实有些问题。方案一因等待所有分片的时间过长容易造成内存溢出;由于分片不一定是按序上传,所以方案二也不行;故只能选择方案三了。

    3)后端如何区分不同用户的文件?如何区分同个文件不同分片的先后顺序?
    通过http请求携带的task_id可以区分不同的文件。同个文件分片的先后顺序,可以通过http请求携带的chunk来区分。因此,task_id+chunk的组合可以在众多不同用户不同文件的分片中唯一标记某个分片,即某个文件的某个分片名称是task_id+chunk。

    三、关键代码

    前端代码

    <!DOCTYPE html>
    <html>
     
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
        <script src="./static/jquery-1.11.1.min.js"></script>
        <script src="./static/bootstrap/js/bootstrap.min.js"></script>
        <script src="./static/webuploader/webuploader.min.js"></script>
        <link rel="stylesheet" type="text/css" href="./static/webuploader/webuploader.css">
        <link rel="stylesheet" type="text/css" href="./static/bootstrap/css/bootstrap.min.css">
    </head>
     
    <body>
        <div>
            <div id="picker">请选择</div>   <!-- 上传按钮,必须指定id选择器的值 -->
            <div class="progress">         <!-- 进度条 -->
                <div class="progress-bar progress-bar-striped active" role="progressbar" style="0%;"></div>
            </div>
        </div>
        <script type="text/javascript">
        $(document).ready(function() {
            var task_id = WebUploader.Base.guid();        //产生task_id
            var uploader = WebUploader.create({           //创建上传控件
                swf: './static/webuploader/Uploader.swf', //swf位置,这个可能与flash有关
                server: '/upload/accept',                 //接收每一个分片的服务器地址
                pick: '#picker',                          //填上传按钮的id选择器值
                auto: true,                               //选择文件后,是否自动上传
                chunked: true,                            //是否分片
                chunkSize: 20 * 1024 * 1024,              //每个分片的大小,这里为20M
                chunkRetry: 3,                            //某分片若上传失败,重试次数
                threads: 1,                               //线程数量,考虑到服务器,这里就选了1
                duplicate: true,                          //分片是否自动去重
                formData: {                               //每次上传分片,一起携带的数据
                    task_id: task_id,
                },
            });
     
            uploader.on('startUpload', function() {       //开始上传时,调用该方法
                $('.progress-bar').css('width', '0%');
                $('.progress-bar').text('0%');
            });
     
            uploader.on('uploadProgress', function(file, percentage) { //一个分片上传成功后,调用该方法
                $('.progress-bar').css('width', percentage * 100 - 1 + '%');
                $('.progress-bar').text(Math.floor(percentage * 100 - 1) + '%');
            });
     
            uploader.on('uploadSuccess', function(file) { //整个文件的所有分片都上传成功,调用该方法
                //上传的信息(文件唯一标识符,文件名)
                var data = {'task_id': task_id, 'filename': file.source['name'] };
                $.get('/upload/complete', data);          //ajax携带data向该url发请求
                $('.progress-bar').css('width', '100%');
                $('.progress-bar').text('上传完成');
            });
     
            uploader.on('uploadError', function(file) {   //上传过程中发生异常,调用该方法
                $('.progress-bar').css('width', '100%');
                $('.progress-bar').text('上传失败');
            });
     
            uploader.on('uploadComplete', function(file) {//上传结束,无论文件最终是否上传成功,该方法都会被调用
                $('.progress-bar').removeClass('active progress-bar-striped');
            });
     
        });
        </script>
    </body>
     
    </html>

    后端代码

    @app.route('/upload/accept', methods=['GET', 'POST'])
    def index():                                        # 一个分片上传后被调用
        if request.method == 'POST':
            upload_file = request.files['file']
            task = request.form.get('task_id')          # 获取文件唯一标识符
            chunk = request.form.get('chunk', 0)        # 获取该分片在所有分片中的序号
            filename = '%s%s' % (task, chunk)           # 构成该分片唯一标识符
            upload_file.save('./upload/%s' % filename)  # 保存分片到本地
        return rt('./index.html')
     
     
    @app.route('/upload/complete', methods=['GET'])
    def upload_success():                               # 所有分片均上传完后被调用
        target_filename = request.args.get('filename')  # 获取上传文件的文件名
        task = request.args.get('task_id')              # 获取文件的唯一标识符
        chunk = 0                                       # 分片序号
        with open('./upload/%s' % target_filename, 'wb') as target_file:  # 创建新文件
            while True:
                try:
                    filename = './upload/%s%d' % (task, chunk)
                    source_file = open(filename, 'rb')                    # 按序打开每个分片
                    target_file.write(source_file.read())                 # 读取分片内容写入新文件
                    source_file.close()
                except IOError:
                    break
                chunk += 1
                os.remove(filename)                     # 删除该分片,节约空间
        return rt('./index.html')

    优化思路:将上传的分片组合成新文件时,需将分片内容全部读出,会造成内存紧张。可以考虑不合并分片,下载时直接按序读取并回传

    四、断点续传

    前端代码

    /* 在点击上传前,调用ajax获取后台文件名列表 */
    // 点击选择,获取所有文件名称
    $("#picker").on("click",function(){
        $.ajax({
            url: "/check",   // 对应flask中的路由
            type: "POST", // 请求方法
            data: 'POST TEST',   // 传送的数据
            dataType: "json", // 传送的数据类型
            success: function (data) {  // 成功得到返回数据后回调的函数
                file_list_finish = data.file_list.toString().split(',');
            }
        })
    });
    
    
    /* 每个分片上传前查看是否有重名文件,分片的命名由框架加密,相同则跳过此分片。*/
    WebUploader.Uploader.register({
        "before-send": "beforeSend",  // 每个分片上传前
    },
    {
        beforeSend:function(block){  //如果有分块上传,则每个分块上传之前调用此函数
            var deferred = WebUploader.Deferred();
            block_name = task_id + block.chunk;
            for (var i=0;i<file_list_finish.length;i++){
                if (file_list_finish[i] == block_name){
                    //取消分片发送
                    deferred.reject();
                    break;
                }
            }
            deferred.resolve();
            return deferred.promise();
        }
    });

    后端代码

    """后台返回文件名的函数"""
    @app.route('/check',methods=['POST'])   # 第一个参数是路由,第二个是请求方法
    def check_file():
        recv_data = request.get_data()  # 得到前端传送的数据
        file_list = []
        for root, dirs, files in os.walk(file_dir):
            file_list.append(files) # 当前路径下所有非目录子文件
        return jsonify({'file_list':file_list }) # 返回数据
  • 相关阅读:
    在非控制台程序中打印出printf
    如何将动态链接库(C++ DLL)中的printf显示在其被调用的程序控制台上
    Android NDK Build 参数
    查找包含××××××字符的文件名
    Ubuntu安装Fcitx(小企鹅五笔输入法)
    查询所有表的记录数SQLServer
    查询某个表或者所有表的字段说明 SQLServer
    安卓64位Ubuntu的32位包安装
    gen already exists but is not a source folder ZT
    mysql中查看datadir目录
  • 原文地址:https://www.cnblogs.com/Zzbj/p/16373953.html
Copyright © 2020-2023  润新知