当前位置:网站首页>从一次需求改良漫谈php文件分片上传

从一次需求改良漫谈php文件分片上传

2020-11-09 15:25:23 gcudwork

说下本文的前言,以前遇到过需要上传大文件比如视频和视频素材的压缩包的需求,需求方甚至要求G级文件的上传

我是该项目的承接人,原项目的流程是上传文件到服务器后调用ffmpeg给视频加水印,截图,生成预览视频,再用服务器上传oss,这样的方式很容易遇到客户端网络波动,服务器上传限制,php上传限制,服务器内存的影响,经常就是超时还根本没法传几百M的视频,而且上传到oss会占用服务器的上传带宽,ffmpeg处理视频同时会把服务器的效率降低到一个极低的程度

于是我进行了陆续的一些改良:使用阿里云的媒体处理服务来代替服务器的截图,水印添加和生成预览视频,中间过程之一的可见[https://my.oschina.net/u/3470006/blog/3104362],使用,又使用oss的客户端api直接上传到oss,除了需要服务器签名外就不需要服务器去和oss进行交互了,无需消耗大量带宽和高占用cpu,云服务很好地方便了我们实现各种需求

前些日子看到有提问说[如何解决上传大文件内存溢出以及超时问题,上传较大的文件比如几百兆的,会出现溢出和超时问题,如何解决这类问题],最简单的解决方案是改php配置,把upload_max_filesize,max_execution_time和max_input_time改大,这样就能简单直接解决问题

下面有人回答分片,看到这个我想起来其实我自己还没做过分片上传,就花了些时间尝试了下,其实也挺简单的

html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>文件分片上传</title>
</head>
<body>
<input type="file" id="File">
<button id="Upload">上传</button>
<p id="Status"></p>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<script>
    //2M的分片
    let BlockSize=2*1048576;
    let Url="Upload.php";
    //用来对用户显示当前的状态提示信息
    let StatusText=$("#Status");
    //使用顺序队列来发送分片文件
    function QueueSendFile(Url,File,BlockSize,Index,MaxNumber){
        if(Index<MaxNumber){
            let Data=new FormData();
            Data.append("File",File.slice(Index*BlockSize,(Index+1)*BlockSize),File.name);
            StatusText.text(`文件大小${File.size}字节,分片大小${BlockSize}字节,总共${MaxNumber}分片,当前正在上传第${Index+1}块分片`);
            $.ajax(Url,{
                data:Data,
                type:"post",
                //不要处理数据也不要指定类型
                processData:false,
                contentType:false,
                success:function (Result) {
                    console.log(Result)
                    if(Result.Status===1){
                        StatusText.text(`文件大小${File.size}字节,分片大小${BlockSize}字节,总共${MaxNumber}分片,第${Index+1}块分片上传完成`);
                        Index++;
                        QueueSendFile(Url,File,BlockSize,Index,MaxNumber);

                    }
                    else if(Result.Status===0){
                        Index=MaxNumber;
                        StatusText.text("出现错误:"+Result.Message);
                    }
                    else{
                        Index=MaxNumber;
                        StatusText.text("服务器返回不正确的数据");
                    }
                },
                error:function (){
                    Index=MaxNumber;
                    StatusText.text("请求错误");
                }
            });
        }
        else{
            StatusText.text("文件上传完成");
        }
    }
    $("#Upload").click(function (){
        let File=$("#File")[0].files;
        if(File.length>0){
            File=File[0];
            QueueSendFile(Url,File,BlockSize,0,Math.ceil(File.size/BlockSize));
        }
        else{
            StatusText.text("必须选择文件");
        }
    });
</script>
</body>
</html>

Upload.php

<?php
function return_data($Message,$IsSuccess=true){
    header('content-type:application/json');
    exit(json_encode(['Status'=>$IsSuccess?1:0,'Message'=>$Message]));
}
function error_return($Message){
    return_data($Message,false);
}
function success_return($Message=''){
    return_data($Message);
}
//应该使用正确的用户鉴权机制,这里用户鉴权不是主要内容所以手动设定
$UserId=1;
if(isset($_FILES['File'])){
    $File=$_FILES['File'];
    //检测创建保存的文件夹,为保证效率基础文件夹应一次性就创建完成
    $BasePath=__DIR__.DIRECTORY_SEPARATOR.'Files'.DIRECTORY_SEPARATOR;
    if(!file_exists($BasePath)){
        mkdir($BasePath);
    }
    $UserPath=$BasePath.$UserId.DIRECTORY_SEPARATOR;
    if(!file_exists($UserPath)){
        mkdir($BasePath);
    }
    //我在这里采用的方案是直接每次追加文件内容
    $SavePath=$UserPath.$File['name'];
    if(!file_exists($SavePath)){
        file_put_contents($SavePath,'');
    }
    file_put_contents($SavePath,file_get_contents($File['tmp_name']),FILE_APPEND);
    success_return();
}
else{
    error_return('参数错误');
}

其实写此文之前我也尝试过别的分片方案:
可以乱序同时上传的
由前端每次发送分片索引和总大小,php把上传的文件建立文件夹,分片文件按分片索引全放在里面,每次检测文件总大小和总大小是否相同,相同就执行合并分片文件的操作,但这个有个缺点,随着分片文件的增多每次计算大小会越来越慢,有两千多个分片时每个总处理时间都超过了100ms(有我电脑慢的原因)

可乱序但不支持同时上传的
大体与上面方案差不多,就是前段发送总大小改成了总数量,每次检测文件夹文件数量和总数量是否一致,仅仅获取文件数量会比上面快得多

这里再说下其它的几个问题:
合并分片操作费时如何处理?
可以在文件上传完成后单独请求执行分片合并操作,如果时间不是特别长可以同步完成,很长而且此类操作比较多就需要使用定时任务来执行合并分片的操作,更加及时些可以使用任务队列实现,前端可以简单地使用轮询来获取操作是否完成
如何保证上传文件的正确性?
在上传之前计算文件的哈希值,在进行合并分片或上传最后一个分片时将哈希值传给php,php来比对完整文件的哈希是否一致

版权声明
本文为[gcudwork]所创,转载请带上原文链接,感谢
https://my.oschina.net/u/3470006/blog/4710040