php 后台合并分片,文件分片上传之后端PHP合成文件

filename: file.name,

identifier: file.uniqueIdentifier,

totalSize: file.size,

totalChunks: chunk.offset + 1

}).then( function(res){

if(res.code === 0) {

console.log( '上传成功')

} else{

console.log(res.message);

}

})

.catch( function(error){

console.log(error);

});

}

},

从后台返回的 response 包含了是否需要合并的指令 merge ,如果 resp.merge === true ,那就发送合并请求,告诉后端可以合成分片了。如果上传的文件只有一片,就不需要合并。

Uplader.php

我们计划用PHP写一个处理上传的类,负责检测文件、接收上传分片、合并分片等。当中还要用到数据库存储文件信息,这些我们在后面章节完成,先看结构:

classUploader

{

privatestatic$tmpDir = 'D:wwwhellowebademofiles_tmp'; //分片临时文件目录

privatestatic$saveDir = 'D:wwwhellowebademofiles'; //文件最终保存目录

privatestatic$mysql = null;

public$fileInfo = [

'identifier'=> '', //文件的唯一标识

'chunkNumber'=> 1, //当前是第几个分片

'totalChunks'=> 1, //总分片数

'filename'=> '', //文件名称

'totalSize'=> 0//文件总大小

];

//检测断点和md5

publicfunctioncheckFile

{

//

}

//上传分片

publicfunctionupload

{

//

}

//合并文件

publicfunctionmerge

{

}

//计算时间

privatefunctiongetmicrotime

{

list($usec, $sec) = explode( " ",microtime);

return((float)$usec + (float)$sec);

}

//返回提示消息

privatefunctionmessage($code, $msg)

{

$res = [

'code'=> $code,

'message'=> $msg

];

return$res;

}

}

我们先定义上传目录,整个目录可以是在你的web目录,也可以是web访问不到的目录,一个临时目录files_tmp/用来保存临时分片文件,一个是真正保存文件的目录files/,注意我们是在Wind平台运行,如果是为Linux下,路径应该写成像这样:/opt/data/files。此外这两个目录要有写权限。

上传分片

首先我们接收前端上传上来的分片文件,当然在正式接收上传分片前,应该检测文件是否已经上传过了,检测文件合法性等等,这些我们在后续文章中会讲到。我们先来看PHP如何接收分片文件。

publicfunctionupload

{

if(! empty($_FILES)) {

$in = @fopen($_FILES[ "file"][ "tmp_name"], "rb");

if(!$in = @fopen($_FILES[ "file"][ "tmp_name"], "rb")) {

return$this->message( 1002, '打开临时文件失败');

}

} else{

if(!$in = @fopen( "php://input", "rb")) {

return$this->message( 1003, '打开输入流失败');

}

}

if( $this->fileInfo[ 'totalChunks'] === 1) {

//如果只有1片,则不需要合并,直接将临时文件转存到保存目录下

$filename = $this->fileInfo[ 'filename'];

$saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date( 'Y-m-d');

if(!is_dir($saveDir)) {

@mkdir($saveDir);

}

$uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename;

$res[ 'merge'] = false;

} else{ //需要合并

$filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo[ 'identifier']; //临时分片文件路径

$uploadPath = $filePath . '_'. $this->fileInfo[ 'chunkNumber']; //临时分片文件名

$res[ 'merge'] = true;

}

if(!$out = @fopen($uploadPath, "wb")) {

return$this->message( 1004, '文件不可写');

}

while($buff = fread($in, 4096)) {

fwrite($out, $buff);

}

@fclose($in);

@fclose($out);

$res[ 'code'] = 0;

return$res;

}

前端是通过 multipart/form-data; 将文件以二进制形式传给PHP,所以我们用 $_FILES 接收文件信息。

接收到文件后,我们判断这个文件是否就只有1个分片,如果只有1个分片就没必要再合成了,直接将该分片保存到files/下,并且告诉前端不需要合并文件: $res['merge'] = false; 。

如果是有多个分片,那就将这些分片保存到临时目录下,分片的命名应该是“文件唯一标识_当前分片”,如abcd_1,标识文件abcd的第一个分片,这样我们接下来合并文件就好办了。

合并文件

合并之前,先检查下该文件的所有分片是否都上传完毕,就是检测分片文件是否都存在。

publicfunctionmerge

{

$filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo[ 'identifier'];

$totalChunks = $this->fileInfo[ 'totalChunks']; //总分片数

$filename = $this->fileInfo[ 'filename']; //文件名

$done = true;

//检查所有分片是否都存在

for($index = 1; $index <= $totalChunks; $index++ ) {

if(!file_exists( "{$filePath}_{$index}")) {

$done = false;

break;

}

}

if($done === false) {

return$this->message( 1005, '分片信息错误');

}

//如果所有文件分片都上传完毕,开始合并

$timeStart = $this->getmicrotime; //合并开始时间

$saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date( 'Y-m-d');

if(!is_dir($saveDir)) {

@mkdir($saveDir);

}

$uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename;

if(!$out = @fopen($uploadPath, "wb")) {

return$this->message( 1004, '文件不可写');

}

if(flock($out, LOCK_EX) ) { // 进行排他型锁定

for($index = 1; $index <= $totalChunks; $index++ ) {

if(!$in = @fopen( "{$filePath}_{$index}", "rb")) {

break;

}

while($buff = fread($in, 4096)) {

fwrite($out, $buff);

}

@fclose($in);

@unlink( "{$filePath}_{$index}"); //删除分片

}

flock($out, LOCK_UN); // 释放锁定

}

@fclose($out);

$timeEnd = $this->getmicrotime; //合并完成时间

$res[ 'code'] = 0;

$res[ 'time'] = $timeEnd - $timeStart; //合并总耗时

return$res;

}

如果分片文件都存在,开始合并所有分片,现将要最终合并的文件锁定,然后遍历所有分片,将分片文件依次写入合并的文件中,最后释放锁定。

每个分片被合并后,应当立即删除该分片。

这里我测试用了计算合并过程的耗时,真实应用可以将计时代码去掉。

合并大文件

我用自己的机器测试(8G内存,SSD),上传了一个约800MB的文件,2M一个分片,约400个分片,合并总耗时3秒钟,合并一个3G的文件耗时30秒钟。也就是说文件越大,分片越多,合成文件所花费的时间越长。但是通过观察内存变化,上面的代码在合并文件时内存消耗很低。那如果是特别大的文件,就会有大量分片,那这样的话合并过程是不是很耗时耗性能呢?

对于特大号的文件合并,有人提出建立一套算法,一个文件有N个分片,先建立一个序列,序列分成N个片段,每个分片占用一个片段,文件上传时就把对应的分片塞到对应的片段中,最终分片文件上传完了文件也就合成好了。这个方法也不错,将合并的时间分摊到每个分片上传上去了。

还有人提出,使用追加的方式将分片一片片往文件里塞,整个方法不可取,因为如果设置并发数大的话,不能保证文件是否按分片顺序合成的,最终有可能得到的文件是个乱序的不可用的文件。

那么我给大家建议使用Swoole来处理文件合成这一步,让耗时的操作在后台运行,不让前端等待,悄悄的在后台合成文件即可,如何?

up.php

up.php用来实例化Uploader上传类,接收前端请求,并且获取相关参数实例化Uploader后,分别调用上传分片、合并文件和检测文件方法。

require_once( 'Uploader.php');

$action = isset($_GET[ 'action']) ? $_GET[ 'action'] : '';

$up = newUploader;

if($action == 'merge') { //合并

$post = file_get_contents( 'php://input');

$data = json_decode($post, true);

$up->fileInfo = [

'filename'=> htmlentities($data[ 'filename']), //文件名称

'identifier'=> htmlentities($data[ 'identifier']), //文件唯一标识

'totalSize'=> intval($data[ 'totalSize']), //文件总大小

'totalChunks'=> intval($data[ 'totalChunks']) //总分片数

];

$res = $up->merge;

} else{

$method = $_SERVER[ 'REQUEST_METHOD'];

if($method === 'POST') { //上传

$up->fileInfo = [

'identifier'=> htmlentities($_POST[ 'identifier']), //每个文件的唯一标识

'filename'=> htmlentities($_POST[ 'filename']), //文件名称

'totalSize'=> intval($_POST[ 'totalSize']), //文件总大小

'chunkNumber'=> intval($_POST[ 'chunkNumber']), //当前是第几个分片

'totalChunks'=> intval($_POST[ 'totalChunks']) //总分片数

];

$res = $up->upload;

} else{ //上传前检测文件md5和分片

$res = $up->checkFile;

}

}

echojson_encode($res);

注意前后端交互涉及到跨域的问题请参照此文设置:PHP处理Ajax请求与Ajax跨域。

好了,接下来我们要了解文件上传前计算MD5的操作以便实现秒传的功能,以及超大文件如何快速计算出md5值呢?敬请关注后续文章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值