317 lines
10 KiB
PHP
317 lines
10 KiB
PHP
![]() |
<?php
|
|||
|
/*
|
|||
|
* @link http://kodcloud.com/
|
|||
|
* @author warlee | e-mail:kodcloud@qq.com
|
|||
|
* @copyright warlee 2014.(Shanghai)Co.,Ltd
|
|||
|
* @license http://kodcloud.com/tools/license/license.txt
|
|||
|
*/
|
|||
|
|
|||
|
class Uploader{
|
|||
|
public $fileName;
|
|||
|
public $uploadFile;
|
|||
|
public $tempFile;
|
|||
|
|
|||
|
public function __construct(){
|
|||
|
global $in;
|
|||
|
$this->in = &$in;
|
|||
|
if (!empty($_FILES)) {
|
|||
|
$files = $_FILES['file'];
|
|||
|
$this->uploadFile = $files["tmp_name"];
|
|||
|
if(!$this->uploadFile && $files['error']> 0){
|
|||
|
show_json($this->errorInfo($files['error']),false);
|
|||
|
}
|
|||
|
}else if (isset($in["name"])) {
|
|||
|
$this->uploadFile = "php://input";
|
|||
|
}
|
|||
|
// $fp = @fopen("php://input","rb");$head = fread($fp, 1024*10);fclose($fp);$S = $_SERVER;
|
|||
|
// write_log([$in,$this->uploadFile,@filesize($this->uploadFile),$S['CONTENT_LENGTH'],$S['CONTENT_TYPE'],$head],'test');
|
|||
|
|
|||
|
if(isset($in['base64Upload']) && isset($in['base64str']) && $in['base64str']){
|
|||
|
$this->uploadFile = 'base64';
|
|||
|
}
|
|||
|
|
|||
|
$this->fileName = self::fileName();
|
|||
|
$this->statusData = false;
|
|||
|
$this->checkSize();
|
|||
|
$this->tempPathInit();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 文件上传处理。大文件支持分片上传
|
|||
|
* post上传:base64Upload=1;file=base64str;name=filename
|
|||
|
*
|
|||
|
* chunk,chunks; 选填; 没有或chunks小于等于1则认为不分片;
|
|||
|
* 分片则 size 必须传入
|
|||
|
*/
|
|||
|
public function upload(){
|
|||
|
if($_SERVER['REQUEST_METHOD'] == 'OPTIONS' ){exit;};//跨域请求时,浏览器发送options接口会影响正常流程;
|
|||
|
$chunk = isset($this->in["chunk"]) ? intval($this->in["chunk"]) : 0;
|
|||
|
$chunks = isset($this->in["chunks"]) ? intval($this->in["chunks"]) : 1;
|
|||
|
$dest = $this->tempFile.'.part'.$chunk;
|
|||
|
$size = isset($this->in["size"]) ? intval($this->in["size"]) : 0;
|
|||
|
$chunkSize = isset($this->in["chunkSize"]) ? intval($this->in["chunkSize"]) : 0;
|
|||
|
if( $chunks > 1 && $chunkSize <= 0 ){
|
|||
|
show_json('chunkSize error!',false);
|
|||
|
}
|
|||
|
|
|||
|
if($chunkSize > $size){$chunks = 1;}
|
|||
|
if($chunks <= 1) {// 没有分片;
|
|||
|
// 清除秒传or断点续传请求checkChunk写入的数据;
|
|||
|
$this->statusSet(false);
|
|||
|
$this->tempFile = $this->tempFile.rand_string(5);
|
|||
|
$result = $this->moveUploadedFile($this->tempFile);
|
|||
|
return $this->uploadResult($result);
|
|||
|
}
|
|||
|
|
|||
|
$fileHashSimple = '';
|
|||
|
$fileChunkSize = ($chunk < $chunks - 1) ? $chunkSize : ($size - ($chunks - 1) * $chunkSize);
|
|||
|
//分片上传必须携带文件大小,及分片切分大小;
|
|||
|
CacheLock::lock($this->tempFile,30);
|
|||
|
$this->initFileTemp();
|
|||
|
CacheLock::unlock($this->tempFile);
|
|||
|
|
|||
|
// 流式上传性能优化;不写入临时文件;直接写入到目标占位文件中;
|
|||
|
if($this->uploadFile == "php://input"){
|
|||
|
$chunkFile = $this->uploadFile;
|
|||
|
}else{
|
|||
|
$chunkFile = $this->moveUploadedFile($dest);
|
|||
|
if($size > 0 && filesize($chunkFile) == 0){$this->showJson('0 Byte upload',false);}
|
|||
|
if(!$chunkFile){$this->showJson(LNG('explorer.moveError'),false);}
|
|||
|
$fileHashSimple = IO::hashSimple($chunkFile);
|
|||
|
}
|
|||
|
$offset = $chunk * $chunkSize;
|
|||
|
if(!$outFp = @fopen($this->tempFile, "r+")){
|
|||
|
$this->showJson('fopen file error:'.$this->tempFile,false);
|
|||
|
}
|
|||
|
fseek_64($outFp,$offset);
|
|||
|
$success = $this->writeTo($chunkFile,$outFp,$this->tempFile);
|
|||
|
if(!$fileHashSimple){
|
|||
|
$outFp = fopen($this->tempFile,'r');
|
|||
|
if(!$outFp){$this->showJson('fopen file error',false);}
|
|||
|
fseek_64($outFp,$offset);
|
|||
|
$fileHashSimple = PathDriverStream::hash($outFp,$fileChunkSize);
|
|||
|
fclose($outFp);
|
|||
|
}
|
|||
|
@unlink($chunkFile);
|
|||
|
if(!$success){$this->showJson('chunk_error:'.$chunk,false);}
|
|||
|
|
|||
|
//分片成功:
|
|||
|
CacheLock::lock($this->tempFile.'.statusGet',30);
|
|||
|
$data = $this->statusGet();
|
|||
|
$data['chunkTotal'] = $chunks;
|
|||
|
$data['chunkArray']['chunk'.$chunk] = array(
|
|||
|
'offset' => $offset,
|
|||
|
'index' => $chunk,
|
|||
|
'size' => $fileChunkSize,
|
|||
|
'hashSimple'=> $fileHashSimple,
|
|||
|
);
|
|||
|
$this->statusSet($data);
|
|||
|
CacheLock::unlock($this->tempFile.'.statusGet');
|
|||
|
|
|||
|
if(count($data['chunkArray']) != $data['chunkTotal'] ){
|
|||
|
$this->showJson('chunk_success_'.$chunk,true);
|
|||
|
}
|
|||
|
|
|||
|
// 所有分片完成,检测分片hash值一致性;
|
|||
|
if(!$this->checkChunkHash($data)){
|
|||
|
$this->showJson(LNG('explorer.upload.error')."[chunk hash error!]",false);
|
|||
|
}
|
|||
|
return $this->uploadResult($this->tempFile);
|
|||
|
}
|
|||
|
|
|||
|
// 上传完成临时文件检测;
|
|||
|
private function uploadResult($file){
|
|||
|
$this->statusSet(false);//上传成功,清空相关配置;
|
|||
|
$size = isset($this->in["size"]) ? intval($this->in["size"]) : 0;
|
|||
|
$hash = isset($this->in["checkHashSimple"]) ? $this->in["checkHashSimple"] : '';
|
|||
|
if(!file_exists($file)){
|
|||
|
show_json(LNG('explorer.upload.error').' [temp not exist!]',false);
|
|||
|
}
|
|||
|
if($size && $size != filesize($file)){
|
|||
|
@unlink($file);
|
|||
|
show_json(LNG('explorer.upload.error').'[temp size error]',false);
|
|||
|
}
|
|||
|
return $file;
|
|||
|
}
|
|||
|
|
|||
|
private function checkSize(){
|
|||
|
if(phpBuild64() || $this->in['size'] < PHP_INT_MAX) return;
|
|||
|
show_json(LNG('explorer.uploadSizeError'),false);
|
|||
|
}
|
|||
|
private function showJson($data,$code){
|
|||
|
CacheLock::unlock($this->tempFile);
|
|||
|
if(!$code){$this->clearData();}
|
|||
|
show_json($data,$code);
|
|||
|
}
|
|||
|
|
|||
|
public function clearData(){
|
|||
|
$this->statusSet(false);
|
|||
|
if(file_exists($this->tempFile)){
|
|||
|
@unlink($this->tempFile);return;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 上传临时目录; 优化: 默认存储io为本地时,临时目录切换到对应目录的temp/下;(减少从头temp读取->写入到存储i)
|
|||
|
private function tempPathGet(){
|
|||
|
// return '/mnt/usb/tmp/';
|
|||
|
$tempPath = TEMP_FILES;
|
|||
|
$path = isset($GLOBALS['in']['path']) ? $GLOBALS['in']['path']:'';
|
|||
|
$driverInfo = KodIO::pathDriverType($path);
|
|||
|
if($driverInfo && $driverInfo['type'] == 'local'){
|
|||
|
$truePath = rtrim($driverInfo['path'],'/').'/';
|
|||
|
$isSame = KodIO::isSameDisk($truePath,TEMP_FILES);
|
|||
|
if(!$isSame && file_exists($truePath)){$tempPath = $truePath;}
|
|||
|
}
|
|||
|
|
|||
|
if(!file_exists($tempPath)){
|
|||
|
@mk_dir($tempPath);
|
|||
|
touch($tempPath.'index.html');
|
|||
|
}
|
|||
|
return $tempPath;
|
|||
|
}
|
|||
|
|
|||
|
// 临时文件;
|
|||
|
private function tempPathInit(){
|
|||
|
$tempPath = $this->tempPathGet();
|
|||
|
$tempName = isset($this->in['checkHashSimple']) ? $this->in['checkHashSimple']:false;
|
|||
|
if(strlen($tempName) < 30){ //32+大小;
|
|||
|
$tempName = md5(USER_ID.$this->in['path'].$this->fileName.$this->in['size']);
|
|||
|
}
|
|||
|
$name = ".upload_".md5($tempName.$this->in['chunkSize']);
|
|||
|
$this->tempFile = $this->tempPathGet().$name;
|
|||
|
}
|
|||
|
// 兼容move_uploaded_file 和 流的方式上传
|
|||
|
private function moveUploadedFile($dest){
|
|||
|
$fromPath = $this->uploadFile;
|
|||
|
if($fromPath == "base64"){
|
|||
|
@file_put_contents($dest,base64_decode($_REQUEST['base64str']));
|
|||
|
}else if($fromPath == "php://input"){
|
|||
|
$out = @fopen($dest, "wb");
|
|||
|
$this->writeTo($fromPath,$out,$dest);
|
|||
|
}else{
|
|||
|
if(!move_uploaded_file($fromPath,$dest)){return false;}
|
|||
|
}
|
|||
|
return $dest;
|
|||
|
}
|
|||
|
|
|||
|
private function writeTo($from,$outFp,$outName){
|
|||
|
$lockKey = 'Uploader.writeTo'.$outName;
|
|||
|
//$isLock= CacheLock::lock($lockKey,1); //不用锁;[多个进程独立写入]
|
|||
|
$in = @fopen($from,"rb");
|
|||
|
if(!$in || !$outFp){return false;}
|
|||
|
while (!feof($in)) {
|
|||
|
fwrite($outFp, fread($in, 1024*500));
|
|||
|
}
|
|||
|
fclose($in);fclose($outFp);
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
private function statusGet(){
|
|||
|
if( is_array($this->statusData)) return $this->statusData;
|
|||
|
$file = $this->tempFile.'.cfg';
|
|||
|
$content = false;
|
|||
|
if(file_exists($file)){
|
|||
|
$content = @file_get_contents($file);
|
|||
|
}
|
|||
|
if($content){
|
|||
|
$this->statusData = json_decode($content,true);
|
|||
|
}
|
|||
|
if(!$this->statusData){
|
|||
|
$defaultData = array(
|
|||
|
'name' => $this->fileName,
|
|||
|
'chunkTotal'=> 0,
|
|||
|
'chunkArray'=> array(), // chunk2=>{offset:xxx,index:2,hashSimple:xxx}
|
|||
|
);
|
|||
|
$this->statusSet($defaultData);
|
|||
|
}
|
|||
|
return $this->statusData;
|
|||
|
}
|
|||
|
public function statusSet($data){
|
|||
|
$file = $this->tempFile.'.cfg';
|
|||
|
if(!$data){
|
|||
|
return @unlink($file);
|
|||
|
}
|
|||
|
$this->statusData = $data;
|
|||
|
return file_put_contents($file,json_encode($data));
|
|||
|
}
|
|||
|
|
|||
|
// 生成占位文件;
|
|||
|
private function initFileTemp(){
|
|||
|
if( file_exists($this->tempFile) ) return;
|
|||
|
if(!$fp = fopen($this->tempFile,'wb+')){
|
|||
|
$this->showJson('fopen file error:'.$this->tempFile,false);
|
|||
|
}
|
|||
|
fseek_64($fp,$this->in['size']-1,SEEK_SET);
|
|||
|
fwrite($fp,'0');
|
|||
|
fclose($fp);
|
|||
|
}
|
|||
|
|
|||
|
//文件分块检测是否已上传,已上传则忽略;断点续传
|
|||
|
public function checkChunk(){
|
|||
|
$result = array();
|
|||
|
CacheLock::lock($this->tempFile);
|
|||
|
$data = $this->statusGet();
|
|||
|
CacheLock::unlock($this->tempFile);
|
|||
|
foreach ($data['chunkArray'] as $item) {
|
|||
|
$hash = $item['hashSimple'];
|
|||
|
if($hash){
|
|||
|
$result['part_'.$item['index']] = $hash;
|
|||
|
}
|
|||
|
}
|
|||
|
return $result;
|
|||
|
}
|
|||
|
|
|||
|
// 所有分片完成,检测simpleHash 及md5;
|
|||
|
private function checkChunkHash($data){
|
|||
|
if(count($data['chunkArray']) != $data['chunkTotal'] ){return false;}
|
|||
|
$fileHash = _get($this->in,'checkHashSimple');
|
|||
|
if($fileHash){return IO::hashSimple($this->tempFile) == $fileHash;}
|
|||
|
|
|||
|
if(!$fp = fopen($this->tempFile,'r')) return false;
|
|||
|
$success = true;
|
|||
|
foreach ($data['chunkArray'] as $item) {
|
|||
|
fseek_64($fp,$item['offset']);
|
|||
|
$chunkHash = PathDriverStream::hash($fp,$item['size']);
|
|||
|
if($item['hashSimple'] != $chunkHash){
|
|||
|
$success = false;break;
|
|||
|
}
|
|||
|
}
|
|||
|
fclose($fp);
|
|||
|
return $success;
|
|||
|
}
|
|||
|
|
|||
|
//拍照上传等情况兼容处理;
|
|||
|
public static function fileName(){
|
|||
|
global $in;
|
|||
|
$fileName = isset($in['name']) ? $in['name'] :'';
|
|||
|
if (!empty($_FILES)) {
|
|||
|
$fileName = $fileName ? $fileName : $_FILES['file']["name"];
|
|||
|
}
|
|||
|
if(!is_wap()) return KodIO::clear($fileName);
|
|||
|
|
|||
|
//ios 上传没有文件名问题处理
|
|||
|
$time = strtotime($in['lastModifiedDate']);
|
|||
|
$time = $time ? $time : time();
|
|||
|
$beforeName = strtolower($fileName);
|
|||
|
if($beforeName == "image.jpg" || $beforeName == "image.jpeg"){
|
|||
|
$fileName = date('Ymd',$time).'_'.$in['size'].'.jpg';
|
|||
|
}else if($beforeName == "capturedvideo.mov"){
|
|||
|
$fileName = date('Ymd',$time).'_'.$in['size'].'.mov';
|
|||
|
}
|
|||
|
return KodIO::clear($fileName);
|
|||
|
}
|
|||
|
|
|||
|
private function errorInfo($error){
|
|||
|
$status = array(
|
|||
|
'UPLOAD_ERR_OK', //0 没有错误发生,文件上传成功。
|
|||
|
'UPLOAD_ERR_INI_SIZE', //1 上传的文件超过了php.ini 中 upload_max_filesize 选项限制的值。
|
|||
|
'UPLOAD_ERR_FORM_SIZE', //2 上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
|
|||
|
'UPLOAD_ERR_PARTIAL', //3 文件只有部分被上传。
|
|||
|
'UPLOAD_ERR_NO_FILE', //4 没有文件被上传。
|
|||
|
'UPLOAD_UNKNOW', //5 未定义
|
|||
|
'UPLOAD_ERR_NO_TMP_DIR',//6 找不到临时文件夹。php 4.3.10 和 php 5.0.3 引进。
|
|||
|
'UPLOAD_ERR_CANT_WRITE',//7 文件写入失败。php 5.1.0 引进。
|
|||
|
);
|
|||
|
return $error.':'.$status[$error];
|
|||
|
}
|
|||
|
}
|