目的
nginx和php都有大文件上传限制,当我们的项目需要上传超过2M的大文件时,就会被拦截。当然可以修改这个配置,以扩大限制,但只是治标不治本,换一个环境还要重新配置。
今天在自己的laravel项目中实现一下大文件分片上传,基本原理就是把大文件切成若干片,每片都是一个小文件,再上传到服务器。由于我做的小项目并没有太高要求,本文只是在已有项目的基础上,新建几个文件的简单实现,并没有积极考虑效率问题。如果对项目效率要求比较高,推荐直接使用开源的laravel拓展包:AetherUpload-Laravel
请保证你已经有一个可以正常运行的laravel项目
问题情景
假设你已经写了一个控制器方法upload_my_file(Request $request);,当你上传一个100KB的文件时,你成功执行完了所有请求。但是,当你上传一个10MB的文件时,你发现被系统拦截了,原因是文件太大!那么你就可以通过下面的方案解决!
换句话说,如果你已经写好了可以上传小文件的完整功能,那么请继续看下面的解决方案!
大文件解决方案(4步)
- A. 在laravel项目中新建两个文件
1. 项目根下执行php artisan make:controller UploadController产生控制器,编辑它(app/Http/Controllers/UploadController)。该控制器不需要路由,在你正常情况下提交form表单的那个控制器中调用下面的upload方法即可。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
/*
* 保存request传过来的一块文件,指定保存位置与文件名
*/
public function upload(Request $request,$save_dir,$save_name){
$temp_save_dir='upload_big_temp/'.(Auth::check()?Auth::id():'guest');
if(!Storage::exists($temp_save_dir)){ //临时文件夹
Storage::makeDirectory($temp_save_dir);
}
$block=$request->file('block');
$block_id=intval( $request->input('block_id') ); //0~tot-1
$block_tot=intval( $request->input('block_tot') );
$block->move(storage_path('app/'.$temp_save_dir),$block_id); //以块号为名保存当前块
if($block_id == $block_tot-1){ //整个文件上传完成
for($i=0;$i<$block_tot;$i++){
if(!Storage::exists($save_dir)){ //保存文件夹
Storage::makeDirectory($save_dir);
}
$content=Storage::get($temp_save_dir.'/'.$i);
file_put_contents(storage_path('app/'.$save_dir.'/'.$save_name),$content,$i?FILE_APPEND:FILE_TEXT);//追加:覆盖
}
Storage::deleteDirectory($temp_save_dir); //删除临时文件
return true; //标记上传完成
}
return false;
}
}
2. 新建文件:项目根目录/public/js/uploadBig.js
function uploadBig(obj) {
var args={
url:obj.url, //必须,相当于form的action
_token:obj._token, //必须,laravel token:'{{csrf_token()}}'
files:obj.files, //必须,上传的文件列表
data:obj.data, //可选,除files外的其他数据
blockSize:1024*(obj.blockSize!==undefined?obj.blockSize:900), //可选,每块的大小,默认900KB
before: obj.before, //可选,上传前执行函数
uploading:obj.uploading, //可选,上传中执行函数
success: obj.success, //可选,上传成功执行函数
error: obj.error, //可选,上传出错执行函数
};
function dfs_ajax(index=0,start=0) {
var formData = new FormData();
formData.append('filename',args.files[index].name); //文件原始名
formData.append('block_id',Math.round(start/args.blockSize)); //块号
formData.append('block_tot',Math.ceil(args.files[index].size/args.blockSize));//块数
formData.append('block',args.files[index].slice(start,start+args.blockSize)); //文件块
if(args.data!==undefined && start+args.blockSize>=args.files[index].size)//要上传最后一块了
{
for(let key of Object.keys(args.data))
formData.append(key,args.data[key]); //除文件外的附加数据
}
$.ajax({
headers: {'X-CSRF-TOKEN': args._token},
url: args.url ,
type: 'post',
data: formData,
processData: false,
contentType: false,
success:function(ret){
// 只有ret返回0,才代表文件需要继续上传
if(ret!==0 || index===args.files.length-1 && start+args.blockSize>=args.files[index].size)//最后一个上传完毕
{
//上传成功...回调函数[文件总数,控制器返回值]
if(args.success!==undefined)
args.success(args.files.length,ret);
}
else
{
//上传中...回调函数,参数[文件总数,当前第几个,当前文件已上传大小KB]
if(args.uploading!==undefined)
args.uploading(args.files.length,index+1,(start+args.blockSize)/1024,args.files[index].size/1024);
if(start+args.blockSize>=args.files[index].size)//跳到下一个文件
dfs_ajax(index+1,0);//递归
else
dfs_ajax(index,start+args.blockSize);//递归
}
},
error:function(xhr,status,err){
if(args.error!==undefined)
args.error(xhr,status,err);
}
});
}
//上传开始前回调函数,参数[文件数量,合计大小KB]
if(args.before!==undefined){
var total_size=0;
for(var file_temp of args.files)total_size+=file_temp.size;
args.before(args.files.length,total_size);
}
dfs_ajax(); //递归按顺序开始执行ajax
}
- B. 稍加修改你的html和控制器函数
3. 仿照如下修改你的前端代码。do_upload中3个参数url,_token,files是必需的,其余的可选。
<form method="post" onsubmit="return do_upload()">
@csrf
<div class="form-inline">
<label>文件:
<input type="file" id="file_xml" required>
</label>
<button type="submit">上传</button>
</div>
</form>
<script src="{{asset('js/uploadBig.js')}}"></script>
<script>
function do_upload() {
uploadBig({
url:"{{route('test_upload')}}", //表单相当于表单action
_token:"{{csrf_token()}}", //laravel必需的
files:document.getElementById("file_xml").files, //使用jquery获取文件列表
data:{
'other_data':'除了文件外的数据,放到data里传给后台'
},
before:function (file_count, total_size) {
console.log("准备上传!\n文件总数:"+file_count+"\n总大小:"+total_size+"KB")
},
uploading: function (file_count,index,up_size,fsize) {
console.log("正在上传!\n文件总数:"+file_count+"\n当前第"+index+"个,已上传/该文件大小:"+up_size+"KB/"+fsize+"KB");
},
success:function (file_count,ret) {
console.log("上传成功!\n文件总数:"+file_count+"\n");
console.log("控制器返回值:"+ret);
},
error:function (xhr,status,err) {
console.log("上传出错!\n");
console.log(xhr);
console.log(status);
console.log(err);
}
});
return false;
}
</script>
4. 修改你的控制器函数,函数的开头加入下面三行代码
$uc=new UploadController;
$isUploaded=$uc->upload($request,'保存路径','文件名');
if(!$isUploaded)return 0;
注:'保存路径'填相对于storage/app/的文件夹名字,'文件名'不要含有中文或/
如果提示函数upload不存在,就在控制器文件顶端加一行:use App\Http\Controllers\UploadController;
总结
基本原理就是将大文件切成一个个的小文件,所有的小文件上传完成再继续执行你的剩余代码。把这个过程封装到另一个控制器中,而在需要上传大文件时,只需调用上面这三行代码即可。
上面的代码完全可以根据自身需要修改。