jQuery+PHP 分片上传超大文件

2023年01月01日 1429点热度 0人点赞 0条评论

HTML定义文件上传组件和上传按钮,使用了 Bootstrap ,不满意可以自己美化。

<form id="form1">
  <div class="form-group">
    <div class="custom-file"><input id="fileUpload" class="custom-file-input" type="file" />
      <label class="custom-file-label" for="customFile">选择文件</label></div>
  </div>
  <div class="form-group"><label for="title">视频标题:</label>
    <input id="title" class="form-control" maxlength="32" name="title" type="text" /></div>
  <div class="form-group"><label for="description">视频描述:</label>
    <input id="description" class="form-control" maxlength="32" name="description" type="text" /></div>
  <div class="form-group"><label for="status">上传状态</label>
    <input id="status" class="form-control" name="status" readonly="readonly" type="text" />
    <div class="progress m-t-sm">
      <div id="auth-progress" class="progress-bar progress-bar-striped progress-bar-animated" style="width: 0;">0%</div>
    </div>
  </div>
  <button id="authUpload" class="btn btn-primary" disabled="disabled" type="button"><i class="fa fa-fw fa-upload"></i> Start Upload</button>
</form>

Javascript 脚本内容,这里使用了 jQuery 库,也完全可以使用原生代码或其他框架,没多大区别,自己高兴就好。

使用了 SparkMD5 库,目的是获取文件分片的md5值

<script type="text/javascript">
    // 一个上传视频文件的例子
    let allow_type = ["mp4","avi","mkv"]; // 定义允许上传的文件类型
    var chunk_size = 1024 * 100; // 上传文件分片大小 100K
    var slice_count = 0; // 文件分片块数
    var slice_data = Array(); // 文件分片数据
    var file_size = 0; // 文件大小
    var file_name = "";

    $('#fileUpload').on('change', function (e) {
        var file = e.target.files[0];
        if (!file) {
            alert("请先选择需要上传的文件!");
            return;
        }
        $("#title").val(file.name);
        $('#authUpload').prop("disabled", false);
    });

    $('#authUpload').on('click', function () {
        if ($("#title").val().length < 2) {
            toastr.warning("视频标题填写太短!");
            return false;
        }

        const file = $("#fileUpload")[0].files[0];
        file_size = file.size;
        file_name = file.name;
        if (allow_type.indexOf(file_name.substr(file_name.lastIndexOf(".")+1).toLowerCase()) == -1) {
            toastr.warning("只允许上传视频文件格式: mp4 avi mkv");
            return;
        }

        var timestamp_start = new Date().getTime();
        $('#authUpload').prop("disabled", true);
        $("#status").val("开始处理本地文件 ...");

        slice_count = Math.ceil(file.size / chunk_size);
        const fileReader = new FileReader();
        var index = 0;
        const loadFile = () => {
            var start = index === 0 ? 0 : index * chunk_size;
            var end = start + chunk_size;
            if (end > file_size) end = file_size;
            //console.log(start + " / " + end);
            const slice = file.slice(start, end);
            fileReader.readAsArrayBuffer(slice);
        }
        loadFile();
        fileReader.onload = e => {
            //console.log(index + " / " + slice_count);
            //console.log(e.target.result);
            // 上传分片数据
            const spark = new SparkMD5.ArrayBuffer();
            spark.append(e.target.result)
            upload_slice_data(index, e.target.result, spark.end(), function () {
                index ++;
                if (index >= slice_count) {
                    // 分片处理结束
                    $("#auth-progress").html("100%").css("width", "100%");
                    var timestamp_end = new Date().getTime();
                    $("#status").val("上传完成,消耗时间:" + ((timestamp_end - timestamp_start) / 1000).toFixed(3) + "秒");

                } else {
                    loadFile();
                }
            });

        };

    });

    // 开始分片上传,单线程上传,
    // 如果需要多线程上传会麻烦一些,前后台都要有此小改动,需要的可以私聊交流。
    function upload_slice_data(index, binrary, chunk_md5, callback) {
        if (index > file_size) index = file_size
        let file = new File([binrary], file_name);
        file.type = "application/octet-stream";

        let formData = new FormData();
        formData.append("file", file);
        formData.append("name", file_name);
        formData.append("index", index);
        formData.append("chunk_md5", chunk_md5);
        formData.append("total_size", file_size);
        formData.append("slice_count", slice_count);
        formData.append("title", $("#title").val());
        formData.append("description", $("#description").val());

        $.ajax({
            url: "resources_upload_chunk.php",
            type: "POST",
            timeout: 25000,
            processData:false,
            contentType: false,
            data: formData,
            error: function (xhr, textStatus) {
                alert(textStatus);
            },
            success: function (data, textStatus, jqXHR) {
                if (data.status === "error") {
                    alert(data.msg);
                } else {
                    let current = (index + 1) * chunk_size;
                    if (current > file_size) current = file_size;
                    let progress = (current / file_size * 100).toFixed(2);
                    $("#status").val(current.toLocaleString() + " / " + file_size.toLocaleString());
                    $("#auth-progress").html(progress + "%").css("width", progress + "%");

                    callback();
                }
            }
        });
    }
</script>

后台 resources_upload_chunk.php 文件

<?php
$file_name = clear_string($_POST["name"] ?? "");
$index = intval($_POST["index"] ?? -1);
$slice_count = intval($_POST["slice_count"] ?? -1);
$total_size = intval($_POST["total_size"] ?? -1);
$chunk_md5 = clear_string($_POST["chunk_md5"] ?? "");

$upload_file = $_FILES["file"];
$md5 = md5_file($upload_file["tmp_name"]);
if ($md5 != $chunk_md5) ajax_result(AJAX_RETURN_TYPE::ERROR, "上传文件验证失败!");

$upload_tmp_path = dirname(__FILE__). "/tmp/upload/". md5(session_id(). $file_name);

// 上传第一块,清理临时文件
if ($index == 0 && file_exists($upload_tmp_path)) clear_upload_tmp();
if (!file_exists($upload_tmp_path)) mkdir($upload_tmp_path, 0755, true);

// 将上传的分片文件放到上传临时目录
move_uploaded_file($upload_file["tmp_name"], $upload_tmp_path . "/{$index}.part");

// 上传完所有分片后,组合文件。
// 因前端是单线程上传,index 是按顺序上传过来的
if ($index == $slice_count - 1) {
    $ext = strrchr($file_name,'.');
    $fp_target = fopen($upload_tmp_path. "/merge{$ext}", "wb");
    for ($i=0; $i<$slice_count; $i++) {
        $part_file = $upload_tmp_path. "/{$i}.part";
        $fp_source = fopen($part_file, "rb");
        $source = fread($fp_source, filesize($part_file));
        fclose($fp_source);

        fwrite($fp_target, $source);
        unlink($part_file);
    }
    fclose($fp_target);

    if (filesize($upload_tmp_path. "/merge{$ext}") == $total_size) {
        ajax_result(AJAX_RETURN_TYPE::SUCCESS, "上传完成");
    } else {
        ajax_result(AJAX_RETURN_TYPE::ERROR, "上传文件验证失败");
    }
} else {
    // 未上传完成,通知客户端继续
    ajax_result(AJAX_RETURN_TYPE::SUCCESS, "continue");
}

// 清除上传临时目录,这里自己去搞定,就是删除临时目录下所有文件
private function clear_upload_tmp() {
    global $upload_tmp_path;
    $this->file = __base_service::getInstance()->file();
    $this->file->delete($upload_tmp_path);
}

/**
 * 输出ajax提示
 * @param string $status 返回状态,分为 error 和 successful 两种,或在前台javascript中自行判断
 * @param string $msg 直接输出的json字符串
 */
function ajax_result($status="successful", $msg=""){
    // 禁用缓存
    header("Content-type: text/html; charset=utf-8");
    header("Expires: Mon, 26 Jul 1970 05:00:00  GMT");
    header("Last-Modified:" . gmdate("D, d M Y  H:i:s")  . "GMT");
    header("Cache-Control:no-cache, must-revalidate");
    header("Pragma:no-cache");
    echo json_encode(array('status'=>$status, 'msg'=>$msg), JSON_UNESCAPED_UNICODE);
    exit;
}

路灯

这个人很懒,什么都没留下

文章评论