首页 > 其他分享 >三、文件上传系列-后端合成文件

三、文件上传系列-后端合成文件

时间:2023-02-23 21:08:34浏览次数:54  
标签:文件 后端 res 合并 filename 分片 上传


在上一节文章中,我们介绍了前端文件分片上传,了解vue-simple-uploader组件自带分片上传功能,大文件一片片依次上传到后端服务器后,后端程序要将分片合成一个完整的文件,那么PHP是如何处理合成分片的呢?请看本节讲解。

前端什么时候发送合并请求

在上一节文章中,我们知道,当所有的分片都上传完成时,会调用​​onFileSuccess()​​方法。

onFileSuccess(rootFile, file, response, chunk) {
let resp = JSON.parse(response);
if (resp.code === 0 && resp.merge === false) {
console.log('上传成功,不需要合并');
} else {
axios.post('http://localhost:9999/up.php?action=merge', {
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写一个处理上传的类,负责检测文件、接收上传分片、合并分片等。当中还要用到数据库存储文件信息,这些我们在后面章节完成,先看结构:

<?php
class Uploader
{
private static $tmpDir = 'D:\www\helloweba\demo\files_tmp'; //分片临时文件目录
private static $saveDir = 'D:\www\helloweba\demo\files'; //文件最终保存目录
private static $mysql = null;
public $fileInfo = [
'identifier' => '', //文件的唯一标识
'chunkNumber' => 1, //当前是第几个分片
'totalChunks' => 1, //总分片数
'filename' => '', //文件名称
'totalSize' => 0 //文件总大小
];

//检测断点和md5
public function checkFile()
{
//
}

//上传分片
public function upload()
{
//
}

//合并文件
public function merge()
{

}

//计算时间
private function getmicrotime()
{
list($usec, $sec) = explode(" ",microtime());
return ((float)$usec + (float)$sec);
}

//返回提示消息
private function message($code, $msg)
{
$res = [
'code' => $code,
'message' => $msg
];
return $res;
}
}

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

上传分片

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

public function upload()
{
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的第一个分片,这样我们接下来合并文件就好办了。

合并文件

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

public function merge()
{
$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后,分别调用上传分片、合并文件和检测文件方法。

<?php
require_once('Uploader.php');

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

$up = new Uploader();
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();
}
}

echo json_encode($res);

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

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

标签:文件,后端,res,合并,filename,分片,上传
From: https://blog.51cto.com/u_15967457/6081724

相关文章

  • 四、文件上传系列-计算文件MD5值
    根据业务需要,在上传文件前我们要读取文件的md5值,将md5值传给后端用作秒传和断点续传文件的唯一标识。那么前端就需要使用js获取文件的md5值,对于普通小文件可以很轻松的读取......
  • 分页和文件上传
    分页:1、为什么要分页?方便用户游览,方便数据的定位2、分页的步骤?1.创建PageUtil类,主要提供了当前页、总页、总条数显示条数四个属性进行操作,需要指定计算lim......
  • Linux文件权限和目录配置
    Linux最大特点与windows不同windows是单用户多任务,而linux使用多用户多任务,所以在使用过程中也严格划分每一个用户,以便于进行更好的管理,同时他也是一个安全防护机制文件拥有......
  • pdf2docx:可将 PDF 转换成 docx 文件的 Python 库
    pdf2docx:https://github.com/dothinking/pdf2docx 可将PDF转换成docx文件的Python库。frompdf2docximportparsepdf_file='/path/to/sample.pdf'docx_fi......
  • 库文件的基础
    什么是函数库?存放粮食的仓库称作粮库。存放函数的仓库称作函数库。系统提供了标准库还有一些其他的库文件。用户也可以自定义函数库根据链接方式的不同,将函数库分为动态库(......
  • 第八章 从源文件到可执行文件
        机器运行的是本地代码(NativeCode)用某种编程语言编写出来的程序是源代码,保存源代码的文件是源文件。源文件只是文本文件,并不能直接运行,因为CPU只能运行本地代......
  • Asp.NET Core 导出数据到 Excel 文件
    在Asp.NetCore开发中,使用NPOI将数据导出到Excel文件中,并返回给前端。service层代码:///<summary>///将数据导出到excel///</summary>......
  • 【转】package.json 文件解析
     package.json文件解析每个项目的根目录下一般都会有一个package.json文件,这个文件定义了当前项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等)。当......
  • dokuwiki支持Word上传
    ​ ueditor粘贴不能粘贴word中的图片是一个很头疼的问题,在我们的业务场景中客户要求必须使用ueditor并且支持word的图片粘贴,因为这个需求头疼了半个月,因为前端方面因为安......
  • 文件监控利器-Jnotify
    监听的文件变化的方式有很多,但是比较完美的还是jNotifyhttps://jnotify.sourceforge.net/对比一下监控方式的优缺点方式缺点java原生watch可能对文件时间获取......