php实现文件分片上传

1,191 阅读1分钟

背景

在网站开发中,经常会有上传文件的需求,

  1. 有时会遇到非常大的文件上传,上传过程中耗时非常久,
  2. 可能服务器的限制设置了上传文件尺寸,返回“413 request entity too large”

实现

整体逻辑

  1. 前端:上传文件时,进行文件分片;发起请求时,带上第几次分片上传、总片数。

  2. 后端:按照分片进行文件保存,当上传完最后一片数据时,进行文件合并,并删除分片文件。

前端

js可以非常容易的获取到浏览器端用户选择的文件尺寸。

let totalSize = file.size;

定义好每一片的尺寸,就可以计算出总片数

let chunk = 1024 * 1024; // 每一片大小 1M
let totalPage = Math.ceil(totalSize / chunk); // 总共上传的片数,注意:需要向上取整

使用 js的 slice 方法进对文件进行分片

var page = 1;
while(page <= totalPage) {
    file.slice((page - 1) * chunk, page * chunk);
}

代码示例

示例使用了layui进行布局,若你需要在本地运行示例代码,请注意layui的路径。
示例中用的js为ES5

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>layui</title>
    <meta name="renderer" content="webkit">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link rel="stylesheet" href="/slice/static/layui/css/layui.css" media="all">
    <script src="/slice/static/layui/layui.js" charset="utf-8"></script>
    <!-- 注意:如果你直接复制所有代码到本地,上述css路径需要改成你本地的 -->
    <style type="text/css">
        .mask {
            position: fixed;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
            background: #000;
            opacity: 0.8;
            filter: alpha(Opacity=80);
            -moz-opacity: 0.8;
            z-index: 999;
            display: none;
        }
 
        .loading {
            position: fixed;
            width: 300px;
            left: 50%;
            margin-left: -150px;
            top: 200px;
            height: 18px;
            border-radius: 10px;
            background: #fff;
            z-index: 9999;
            overflow: hidden;
            display: none;
        }
    </style>
</head>
<body>
<div class="layui-main">
    <form class="layui-form" method="post" action="">
        <input type="hidden" name="form_submit" value="ok"/>
        <div class="layui-form-item">
            <label class="layui-form-label">选择:</label>
            <input type="hidden" id="totalPage" value="0"/>
            <input type="hidden" id="page" value="1"/>
            <input type="hidden" id="status" value="0"/>
            <div class="layui-input-block">
                <button type="button" class="layui-btn" id="fileUpload"><i class="layui-icon"></i>上传文件</button>
            </div>
        </div>
        <div class="layui-form-item">
            <label class="layui-form-label">文件名:</label>
            <div class="layui-input-block">
                <input type="text" name="name" id="name" value="" lay-verify="title" autocomplete="off" readonly="true"
                       class="layui-input">
            </div>
        </div>
        <div class="layui-form-item layui-form-text">
            <label class="layui-form-label">下载地址</label>
            <div class="layui-input-block">
                <input type="text" name="downUrl" id="downUrl" value="" lay-verify="downUrl" autocomplete="off"
                       readonly="true" placeholder="" class="layui-input" />
            </div>
        </div>
        <div class="layui-form-item">
            <div class="layui-input-block">
                <button class="layui-btn" lay-submit="" lay-filter="submit">立即提交</button>
                <button type="reset" class="layui-btn layui-btn-primary">重置</button>
            </div>
        </div>
    </form>
</div>
<div class="mask"></div>
<div class="loading">
    <div class="layui-progress layui-progress-big" lay-showpercent="true" lay-filter="uploadProgress">
        <div class="layui-progress-bar layui-bg-red" lay-percent="0%"></div>
    </div>
</div>
<!-- 注意:如果你直接复制所有代码到本地,上述js路径需要改成你本地的 -->
<script>
    layui.use(['form', 'upload', 'element'], function () {
        var form = layui.form;
        var upload = layui.upload;
        var element = layui.element;
        var $ = layui.$;
        upload.render({
            elem: '#fileUpload',
            url: '/slice/upload.php', //处理上传文件接口
            accept: 'file',
            auto: false,
            acceptMime: '*.*',//允许上传的文件类型
            choose: function (obj) {
                element.progress('uploadProgress', '0%');//进度条清0
                $('.mask').show();//遮罩层
                $('.loading').show();//显示进度条
                var data = this.data;
                var files = obj.pushFile();//选择的文件推入obj
                var chunk = 1024 * 1024; //每片文件大小
                obj.preview(function (index, file, result) {
                    var totalSize = file.size;//文件总大小
                    var totalPage = Math.ceil(totalSize / chunk);//总共上传的片数
                    $('#totalPage').val(totalPage);
                    $('#page').val('1');
                    $('#status').val('1');
                    var fileName = file.name;//获取文件名
                    $('#name').val(fileName);
                    var fileExt = fileName.substr(fileName.lastIndexOf('.') + 1);//获取文件后缀
                    fileName = fileName.substr(0, fileName.lastIndexOf('.'));//获取不带后缀的文件名
                    var progressTimer = setInterval(function () {
                        var totalPage = parseInt($('#totalPage').val());
                        var page = parseInt($('#page').val());
                        var status = $('#status').val();
                        if (parseInt(totalPage) == parseInt(page) && (parseInt(status) == 2 || parseInt(status) == -1)) {
                            clearInterval(progressTimer);//上传成功或失败停止上传
                        } else {
                            //状态为1的时候上传
                            if (status == 1) {
                                $('#status').val('0');
                                data.fileName = fileName;
                                data.page = page;
                                data.totalPage = totalPage;
                                data.fileExt = fileExt;
                                obj.upload(index, file.slice((page - 1) * chunk, page * chunk));//从文件中截取进行分片上传
                            }
                        }
                    }, 100);
                });
            },
            done: function (res) {
                if (res.status == 1) { //分片上传
                    var page = parseInt($('#page').val());
                    var totalPage = parseInt($('#totalPage').val());
                    element.progress('uploadProgress', Math.ceil(page * 100 / totalPage) + '%');//更新进度条
                    page = page + 1;//上传下一片
                    console.log(page);
                    $('#page').val(page);
                    $('#status').val('1');
                } else if (res.status == 2) { //上传完成
                    element.progress('uploadProgress', '100%');
                    $('#status').val('2');
                    console.log(res.downUrl);
                    $('#downUrl').val(res.downUrl);
                    layer.msg('上传成功', {time: 1000, anim: 0}, function () {
                        $('.mask').hide();//隐藏遮罩层
                        $('.loading').hide();//隐藏进度条
                    });
                } else { //上传错误
                    $('#status').val('-1');
                    element.progress('uploadProgress', '0%');
                    console.log(!typeof (res.downUrl) == "undefined");
                    if (typeof (res.downUrl) == "undefined") {
                    } else {
                        $('#downUrl').val(res.downUrl);
                    }
                    layer.msg("上传失败,请重试", {time: 3000, anim: 0}, function () {
                        $('.mask').hide();
                        $('.loading').hide();
                    });
                }
            },

            error: function () {
                $('.mask').hide();
                $('.loading').hide();
            }
        });
    });
</script>
</body>
</html>

后端

后端语言使用php开发。

  1. 使用file_get_contents获取上传文件的内容
  2. 使用file_put_contents保存文件,
    该函数的第二个参数 FILE_APPEND,可以向文件追加文件内容,可用于文件合并
  3. 使用unlink删除文件

代码示例

upload.php

<?php
if (empty($_POST)) {
    $res = ['status' => 500];
    echo json_encode($res);
    exit;
}

$fileName          = isset($_POST['fileName'])?$_POST['fileName']:'';
$fileExt           = isset($_POST['fileExt'])?$_POST['fileExt']:'';
$page              = isset($_POST['page'])?$_POST['page']:'';
$totalPage         = isset($_POST['totalPage'])?$_POST['totalPage']:'';
$fileTmpName       = isset($_FILES['file'])?$_FILES['file']['tmp_name']:'';

if ($fileName== '' || $fileExt =='' || $page == '' || $totalPage == '' || $fileTmpName == '') {
    $res = ['status' => 500];
    echo json_encode($res);
    exit;
}

$status      = 1;
$downUrl = '';

// 上传文件要保存的路径
$fname = sprintf('./tmp/%s-%s.%s', $fileName, $page, $fileExt);
$data  = file_get_contents($fileTmpName);
file_put_contents($fname, $data);

// 最后一片文件
if ($totalPage  == $page ) {
    $status         = 2;
    $uploadFileName = sprintf('./upload/%s.%s', $fileName, $fileExt);

    // 合并文件,删除分片文件  
    for ($i = 1; $i<=$totalPage; $i++) {
        $tmp =  sprintf('./tmp/%s-%s.%s', $fileName, $i, $fileExt);
        $data = file_get_contents($tmp);
        file_put_contents($uploadFileName, $data, FILE_APPEND);
        @unlink($tmp);
    }

    $dir = trim(dirname($_SERVER['PHP_SELF']), '/');
    if ($dir!='') {
      $dir .= '/';
    }

    $downUrl = sprintf('%s://%s/%s%s', $_SERVER['REQUEST_SCHEME'], $_SERVER['HTTP_HOST'], $dir,trim($uploadFileName, './'));
}

// 返回上传状态
$res = ['status' => $status,'downUrl' => $downUrl];
echo json_encode($res);

运行效果

未上传文件

image.png

文件上传中

image.png

文件上传完成

image.png