浏览器太低级了

守口如瓶,笑脸相迎。叩首问路,码梦为生。 --Vace

php实现大文件切割上传,并且支持断点续传

2016/01/16    后端


php实现文件切割上传需要前端的配合,相关前端代码见javascript中二进制数据和文件的操作,全部代码可以从这里查看并且下载: github

后端实现比较简单,需要对前端发送的每一段数据进行保存

文件上传欲准备 上传文件之前使用GET请求发送文件基本信息,根据文件的基本信息查找服务器上是否存在此文件的上传记录,如果存在返回上传日志,告诉前端哪些块已经上传了,不需要再上传了.不存在记录,前端重新上传

上传步骤 1.$_POST中保存有文件基本信息描述 * filename 文件名称 * filesize 文件大小 * filetype 文件类型 * modified 文件最后更改时间 * trunksize 分块大小 根据以上基本信息序列化后的md5生成文件唯一日志标识 md5filename.log (这是比较简单的,可以使用文件hash保证文件唯一性)

2.前端上传的blob对象为二进制,后端可以在$_FILES中获取到文件保存的临时路径,$_FILES[0]['tmp_name'],移动临时文件到等待合并的目录中,保存文件名为 md5filename.part.trunid,其中trunid标识当前块的编号

3.写入当前文件的上传日志md5filename.log

4.从上传日志中检查是否所有文件都成功上传了,如果所有文件都成功上传了执行合并操作

5.合并完成后删除上传的临时块文件

/**
 * [helper 取值简便方法]
 * @param  [String] $query   [post.arg get.arg server.arg cookie.arg]
 * @param  [type] $default [默认值]
 * @return [type]          [description]
 */
function helper($query,$default=null){
	$qus = explode('.', $query);
	if(count($qus) === 1){
		$type = strtolower($_SERVER['REQUEST_METHOD']);
	}else{
		$type = strtolower($qus[0]);
		$query = $qus[1];
	}
	switch ($type) {
		case 'post':
			return isset($_POST[$query]) ? $_POST[$query] : $default;
		case 'get':
			return isset($_GET[$query]) ? $_GET[$query] : $default;
		case 'server':
			return isset($_SERVER[$query]) ? $_SERVER[$query] : $default;
		case 'cookie':
			return isset($_COOKIE[$query]) ? $_COOKIE[$query] : $default;
		default:
			return $default;
	}
}
date_default_timezone_set('PRC');
header('Content-type: application/json');
//上传文件
if ($_SERVER['REQUEST_METHOD'] === 'POST'){

	if (!empty($_FILES)){
		foreach ($_FILES as $file) {
			$uploader = new Uploader();
			$info = $uploader->upload($file);
			//目前块只包含一个文件
			echo json_encode($uploader->getResult());
			die;
		}
	}
}else if($_SERVER['REQUEST_METHOD'] === 'GET'){
	//断点上传
	//获取文件基本信息
	$uploader = new Uploader();
	$upload = $uploader->getUploadInfo();
	echo json_encode($upload);
}





/**
 * 分隔文件处理
 */
class Uploader{

	/**
	 * 临时文件上传保存目录
	 */
	const UPLOAD_TMP_PATH = './_tmp/';
	/**
	 * 合并后的文件保存目录
	 */
	const UPLOAD_REAL_PATH = './upload/';

	/**
	 * 文件上传成功状态
	 */
	const STATUS_SUCCESS = 0;

	/**
	 * 文件上传失败状态
	 */
	const STATUS_MOVE_ERROR = 1;

	/**
	 * [$error 错误消息]
	 * @var string
	 */
	protected $error = '';
	/**
	 * [$code 消息代码]
	 * @var integer
	 */
	protected $code = 0;

	/**
	 * [
	 * __construct 构造方法,主要获取上传的基本信息.和服务端的数据进行对比.
	 * NOTE:可以结合文件的hash值实现文件秒传功能,原理差不多了
	 * ]
	 */
	public function __construct(){
		$this->config = array(
			'filename' => helper('filename'),
			'filesize' => intval(helper('filesize')),
			'filetype' => helper('filetype'),
			'modified' => intval(helper('modified')),
			'trunksize'=> intval(helper('trunksize'))
		);
		$this->hash = md5(serialize($this->config));
	}

	/**
	 * [upload 上传文件]
	 * @param  [type] $file [$_FILES[0]前端上传的FILE]
	 * @return [type]       [description]
	 */
	public function upload($file){
		// print_r($this->config);
		$trunk = intval(helper('post.trunk'));
		$total = helper('post.total');
		// print_r($file);
		//文件移动成功,记录文件上传指针
		$trunkname = self::UPLOAD_TMP_PATH . $this->hash . '.part.'.$trunk;

		if(@move_uploaded_file($file['tmp_name'], $trunkname)){

			static::log('success','文件'.$this->config['filename'].',块'.$trunk.'移动成功');

			$upload = $this->_uploadSuccess($trunk,$trunkname);
			if(count($upload) == $total){
				//合并文件
				$this->combineTrunkFile();
			}
			
			$this->setCode(self::STATUS_SUCCESS,'ok');
			return true;
		}

		static::log('error','文件'.$this->config['filename'].',块'.$trunk.'移动失败');

		$this->setCode(self::STATUS_MOVE_ERROR,'移动上传块'.$trunk.'失败');
		return false;
	}

	/**
	 * [setCode 设置返回消息]
	 * @param [type] $code [code]
	 * @param string $msg  [消息]
	 */
	protected function setCode($code,$msg=''){
		$this->error = $msg;
		$this->code = $code;
	}

	/**
	 * [getResult 获取结果]
	 * @return [type] [result]
	 */
	public function getResult(){
		return [
			'code'  => $this->code,
			'error' => $this->error
		];
	}

	/**
	 * [_uploadSuccess 记录上传成功的块]
	 * @param  [type] $trunk [块id]
	 * @return [type]        [返回上传所有已经上传的块]
	 */
	protected function _uploadSuccess($trunk,$trunkname){

		$log = self::UPLOAD_TMP_PATH .$this->hash.'.log';

		$uploadLog = $this->getUploadInfo();


		if(false === array_key_exists($trunk, $uploadLog['record']) 
			|| $uploadLog['record'][$trunk] < $uploadLog['trunksize']){
			$uploadLog['record'][ $trunk ] = filesize($trunkname);
			file_put_contents( $log,serialize($uploadLog) );
		}

		return $uploadLog['record'];
	}

	/**
	 * [getUploadInfo 获取服务端保存的文件上传信息]
	 * @return [type] [description]
	 */
	public function getUploadInfo(){
		$log = self::UPLOAD_TMP_PATH .$this->hash.'.log';

		if(file_exists($log)){
			$uploadLog = unserialize(file_get_contents($log));
		}else{
			$uploadLog = $this->config;
			$uploadLog['total'] = helper('post.total',0);
			$uploadLog['record'] = [];
		}

		return $uploadLog;
	}

	/**
	 * [combineTrunkFile 合并文件]
	 * @param  [type] $config [文件碎片信息]
	 * @return [type]         [description]
	 */
	public function combineTrunkFile(){
		$config = $this->getUploadInfo();

		$partname = self::UPLOAD_TMP_PATH . $this->hash . '.part.';

		$fp = fopen(self::UPLOAD_REAL_PATH . $config['filename'], 'w');
		//注意,切割的文件编号从1开始的
		for ($i=1; $i <= $config['total']; $i++) { 
			fwrite($fp, file_get_contents( $partname.$i ));
			//产出文件
			unlink($partname.$i);
		}

		static::log('success','文件'.$config['filename'].'合并成功');

		fclose($fp);
	}

	/**
	 * [log 记录上传日志]
	 * @param  string $type [description]
	 * @param  string $msg  [description]
	 * @return [type]       [description]
	 */
	public static function log($type='', $msg=''){
		$str =  sprintf('[%s] %s => %s'.PHP_EOL,date('Y-m-d H:i:s'),strtoupper($type),$msg);

		if (($fp = fopen('upload_log.log', 'a+')) !== false) {
	        fputs($fp, $str);
	        fclose($fp);
	    }

	}


}

评论加载中...