399 lines
15 KiB
PHP
399 lines
15 KiB
PHP
![]() |
<?php
|
|||
|
|
|||
|
/**
|
|||
|
* webdav client IO;
|
|||
|
*
|
|||
|
* 支持kodserver支持协议特性(文件属性扩展extendFileInfo,文件列表扩展extendFileList)
|
|||
|
*
|
|||
|
* //kodserver: 1, 2, 3, sabredav-partialupdate, extended-mkcol, extended-kodbox
|
|||
|
* //sabre : 1, 3, extended-mkcol, 2, sabredav-partialupdate
|
|||
|
* //patch文件更新: https://sabre.io/dav/http-patch/ curl请求传递contentType会丢失,暂不支持该情况
|
|||
|
*/
|
|||
|
class PathDriverWebdav extends PathDriverBase {
|
|||
|
public function __construct($config) {
|
|||
|
parent::__construct();
|
|||
|
|
|||
|
// 挂载插件开关处理;
|
|||
|
$pluginOption = Model("Plugin")->getConfig('webdav');
|
|||
|
if(!$pluginOption || $pluginOption['mountWebdav'] == '0'){$config = array();}
|
|||
|
|
|||
|
$this->config = $config;
|
|||
|
$this->dav = new webdavClient($config);// host/user/password/basePath
|
|||
|
|
|||
|
$this->ioFileOutServer = $config['ioFileOutServer'] != '0'; // 下载是否中转
|
|||
|
$this->ioUploadServer = $config['ioUploadServer'] != '0'; // 上传是否中转
|
|||
|
$this->davServerKod = false;
|
|||
|
$this->uploadChunkSize = 1024*1024*5; // patch分片上传时; 分片大小;
|
|||
|
|
|||
|
if(!is_string($config['dav'])){return;}
|
|||
|
$davSupport = $config['dav'] ? $config['dav']:'';
|
|||
|
$davSupport = explode(',',$davSupport);
|
|||
|
foreach($davSupport as $key => $type){$davSupport[$key] = trim($type);}
|
|||
|
if(in_array('extended-kodbox',$davSupport)){$this->davServerKod = true;}
|
|||
|
}
|
|||
|
public function mkdir($dir,$repeat=REPEAT_SKIP){
|
|||
|
$parent = $dir;$add = array();// 循环创建文件夹;
|
|||
|
while($parent && $parent != '/' && !$this->exist($parent)){
|
|||
|
$name = get_path_this($parent);
|
|||
|
$parent = get_path_father($parent);
|
|||
|
// 避免 'test/'类型路径parent为test的情况;
|
|||
|
$add[] = array($parent,$name);
|
|||
|
if(count(explode('/',trim($parent,'/'))) == 1){break;}
|
|||
|
}
|
|||
|
$add = array_reverse($add);
|
|||
|
for($i=0; $i < count($add); $i++) {
|
|||
|
$path = rtrim($add[$i][0],'/').'/'.$add[$i][1];
|
|||
|
if(!$this->dav->mkdir($path)){break;}
|
|||
|
}
|
|||
|
if(count($add) == 0 || $i == count($add) ){
|
|||
|
return $this->getPathOuter($dir);
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
private function _copyMove($action,$from,$to,$repeat=REPEAT_REPLACE,$newName=''){
|
|||
|
if(!$this->exist($from)) return false;
|
|||
|
$this->mkdir($this->pathFather($to));
|
|||
|
if(!$newName){
|
|||
|
$newName = get_path_this($from);
|
|||
|
$newName = $this->fileNameAuto($to,$newName,$repeat,false);
|
|||
|
}
|
|||
|
|
|||
|
$destPath = rtrim($to,'/').'/'.$newName;
|
|||
|
if($action == 'copy'){$result = $this->dav->copy($from,$destPath);}
|
|||
|
if($action == 'move'){$result = $this->dav->move($from,$destPath);}
|
|||
|
return $this->exist($destPath) ? $this->getPathOuter($destPath):false;
|
|||
|
}
|
|||
|
public function moveSameAllow(){}
|
|||
|
public function move($from,$to,$repeat=REPEAT_REPLACE) {
|
|||
|
return $this->_copyMove('move',$from,$to,$repeat);
|
|||
|
}
|
|||
|
public function copy($from,$to,$repeat=REPEAT_REPLACE) {
|
|||
|
return $this->_copyMove('copy',$from,$to,$repeat);
|
|||
|
}
|
|||
|
|
|||
|
public function copyFile($from,$to){return $this->copy($from,$to);}
|
|||
|
public function moveFile($from,$to){return $this->move($from,$to);}
|
|||
|
public function remove($path,$toRecycle=true){return $this->dav->delete($path);}
|
|||
|
public function rename($from,$newName){
|
|||
|
$to = get_path_father($from);
|
|||
|
return $this->_copyMove('move',$from,$to,REPEAT_SKIP,$newName);
|
|||
|
}
|
|||
|
public function has($path,$count=false,$isFolder=false){
|
|||
|
$info = $this->listPath($path);
|
|||
|
if(!$info) return false;
|
|||
|
$children = array(
|
|||
|
'hasFolder' => count($info['folderList']),
|
|||
|
'hasFile' => count($info['folderList']),
|
|||
|
);
|
|||
|
if($count){return $children;}
|
|||
|
return $isFolder ? $children['hasFolder'] : $children['hasFile'];
|
|||
|
}
|
|||
|
public function listAll($path){
|
|||
|
// 优先处理按kod进行尝试请求;
|
|||
|
$data = $this->dav->propfind($path,'0','X-DAV-ACTION: kodListAll');
|
|||
|
$current = $this->_pathInfoParse(_get($data,'data.response',false));
|
|||
|
if($current && isset($current['listAllChildren'])){
|
|||
|
foreach ($current['listAllChildren'] as &$item){
|
|||
|
$arr = explode('/',trim($item['path'],'/'));
|
|||
|
$pathShow = implode('/',array_slice($arr,1)).($item['folder'] ? '/':'');
|
|||
|
$item["path"] = rtrim($current['path'],'/').'/'.$pathShow; // 都使用包含上层的方式,兼容1.45前版本;
|
|||
|
unset($item["filePath"]);
|
|||
|
};unset($item);
|
|||
|
return $current['listAllChildren'];
|
|||
|
}
|
|||
|
|
|||
|
$result = array();
|
|||
|
$this->listAllMake($path,$result);
|
|||
|
return $result;
|
|||
|
}
|
|||
|
|
|||
|
public function canRead($path) {return $this->exist($path);}
|
|||
|
public function canWrite($path) {return $this->exist($path);}
|
|||
|
public function getContent($file){return $this->fileSubstr($file, 0, -1);}
|
|||
|
public function fileSubstr($file, $start, $length){
|
|||
|
$start = $start ? $start : 0;
|
|||
|
$end = $start + $length - 1;
|
|||
|
$range = $length > 0 ? 'bytes='.$start.'-'.$end: '';
|
|||
|
|
|||
|
$tempFile = $this->tempFile();
|
|||
|
$result = $this->dav->get($file,$tempFile,$range);
|
|||
|
$content = $result ? file_get_contents($tempFile):false;
|
|||
|
$this->tempFileRemve($tempFile);
|
|||
|
return $content;
|
|||
|
}
|
|||
|
|
|||
|
public function mkfile($path,$content='',$repeat = REPEAT_RENAME){
|
|||
|
$tempFile = $this->tempFile('',$content);
|
|||
|
$result = $this->upload($path,$tempFile,false,$repeat);
|
|||
|
$this->tempFileRemve($tempFile);
|
|||
|
|
|||
|
// io 添加时检测; 根目录新建空文件则放过
|
|||
|
if(trim($path,'/') == 'index.html' && !$content){return $this->getPathOuter('/index.html');}
|
|||
|
return $result;
|
|||
|
}
|
|||
|
public function setContent($file, $content = ''){
|
|||
|
return $this->mkfile($file,$content,REPEAT_REPLACE) ? true : false;
|
|||
|
}
|
|||
|
public function upload($destPath,$localPath,$moveFile=false,$repeat=REPEAT_REPLACE){
|
|||
|
if($this->davServerKod && filesize($localPath) >= $this->uploadChunkSize ){
|
|||
|
return $this->uploadChunk($destPath,$localPath,$moveFile,$repeat);
|
|||
|
}
|
|||
|
$savePath = $this->pathFather($destPath);$this->mkdir($savePath);
|
|||
|
$saveName = $this->fileNameAuto($savePath,$this->pathThis($destPath),$repeat,false);
|
|||
|
$destPath = rtrim($savePath,'/').'/'.$saveName;
|
|||
|
$result = $this->dav->put($destPath,$localPath);
|
|||
|
// write_log([$destPath,$localPath,$result],'dav');
|
|||
|
return $result['status'] ? $this->getPathOuter($destPath) : false;
|
|||
|
}
|
|||
|
|
|||
|
// 如若是kod,前端直传;
|
|||
|
public function uploadLink($destPath,$size=0){
|
|||
|
$pose = strpos($this->config['host'],'/index.php/plugin/webdav/');
|
|||
|
$server = $pose ? substr($pose,0,$pose+1):'';
|
|||
|
if($this->ioUploadServer) return;
|
|||
|
if(!$this->davServerKod || !$server || $size <= 1024*10) return;
|
|||
|
|
|||
|
$in = $GLOBALS['in'];
|
|||
|
$args = array(
|
|||
|
'size' => $size,
|
|||
|
'uploadWeb' =>'1',
|
|||
|
'fullPath' => _get($in,'fullPath',''),
|
|||
|
'checkType' => _get($in,'checkType',''),// 必须是'checkHash',
|
|||
|
'checkHashSimple' => _get($in,'checkHashSimple',''),
|
|||
|
);
|
|||
|
if($args['checkType'] != 'checkHash') return;
|
|||
|
|
|||
|
$result = $this->uploadSend($destPath,'',$args);
|
|||
|
if(!$result['status'] || !is_array($result['data']['info'])){
|
|||
|
show_json(IO::getLastError(),false);
|
|||
|
}
|
|||
|
$uploadInfo = $result['data']['info'];
|
|||
|
$uploadInfo['webdavUploadTo'] = $uploadInfo['addUploadParam'];
|
|||
|
unset($uploadInfo['addUploadParam']);
|
|||
|
// write_log($uploadInfo,'upload');
|
|||
|
return $uploadInfo;
|
|||
|
}
|
|||
|
|
|||
|
private function uploadChunk($destPath,$localPath,$moveFile=false,$repeat=REPEAT_REPLACE){
|
|||
|
$savePath = $this->pathFather($destPath);$this->mkdir($savePath);
|
|||
|
$saveName = $this->fileNameAuto($savePath,$this->pathThis($destPath),$repeat,false);
|
|||
|
$destPath = rtrim($savePath,'/').'/'.$saveName;
|
|||
|
|
|||
|
// 秒传检测处理;
|
|||
|
$checkResult = $this->uploadHashCheck($destPath,$localPath);
|
|||
|
if($checkResult == false){
|
|||
|
$checkResult = $this->uploadWithChunk($destPath,$localPath);
|
|||
|
}
|
|||
|
return $checkResult['code'] ? $this->getPathOuter($destPath) : false;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 上传处理
|
|||
|
*
|
|||
|
* 1. 上传请求 simpleHash; [checkType=checkHash, path,name,checkHashSimple]
|
|||
|
* 2. 如果simpleHash匹配到simpleHash则秒传请求; [checkType=matchMd5, path,name,checkHashMd5]
|
|||
|
*
|
|||
|
* 正常分片上传 [path,name,size,chunkSize,chunks,chunk];
|
|||
|
* data.info字段是否为非空字符串(目标文件路径),来确认文件秒传是否成功
|
|||
|
*/
|
|||
|
private function uploadHashCheck($destPath,$localPath){
|
|||
|
// 检测hashSimple
|
|||
|
$hashSimple = IO::hashSimple($localPath);
|
|||
|
$args = array('checkType'=>'checkHash','checkHashSimple'=>$hashSimple);
|
|||
|
$result = $this->uploadSend($destPath,'',$args);
|
|||
|
|
|||
|
if(!is_array($result['data']) || !array_key_exists('code',$result['data'])) return false;
|
|||
|
if(!$result['data']['code']) return $result['data'];//结束;
|
|||
|
$hashInfo = _get($result['data'],'info.checkFileHash');
|
|||
|
if(!is_array($hashInfo) || !$hashInfo['hashMd5']) return false;
|
|||
|
if($hashInfo['uploadChunkSize']){$this->uploadChunkSize = $hashInfo['uploadChunkSize'];}
|
|||
|
$fileMd5 = IO::hashMd5($localPath);
|
|||
|
if($hashInfo['hashMd5'] != $fileMd5) return false;
|
|||
|
|
|||
|
// 秒传检测
|
|||
|
$args = array('checkType'=>'matchMd5','checkHashMd5'=>$fileMd5,'checkHashSimple'=>$hashSimple);
|
|||
|
$result = $this->uploadSend($destPath,'',$args);
|
|||
|
if(!is_array($result['data'])) return array('code'=>false,'data'=>$result['error']);
|
|||
|
return $result['data'];
|
|||
|
}
|
|||
|
private function uploadWithChunk($destPath,$localPath){
|
|||
|
$totalSize = @filesize($localPath);
|
|||
|
$chunkSize = $this->uploadChunkSize;
|
|||
|
$chunkCount = ceil($totalSize / $chunkSize);$chunkIndex = 0;
|
|||
|
while($chunkIndex < $chunkCount){
|
|||
|
$content = IO::fileSubstr($localPath,$chunkIndex * $chunkSize,$chunkSize);
|
|||
|
$tempFile = $this->tempFile('',$content);
|
|||
|
|
|||
|
$args = array('size'=>$totalSize,'chunkSize'=>$chunkSize,'chunks'=>$chunkCount,'chunk'=>$chunkIndex);
|
|||
|
$result = $this->uploadSend($destPath,$tempFile,$args);$chunkIndex++;
|
|||
|
if(!is_array($result['data'])) return array('code'=>false,'data'=>$result['error']);
|
|||
|
if(!$result['data']['code']) return $result['data'];//有失败情况,则直接结束;
|
|||
|
}
|
|||
|
return $result;
|
|||
|
}
|
|||
|
private function uploadSend($destPath,$tempFile,$args){
|
|||
|
$this->dav->setHeader('X-DAV-UPLOAD','kodbox');
|
|||
|
$this->dav->setHeader('X-DAV-ARGS',base64_encode(json_encode($args)));
|
|||
|
return $this->dav->put($destPath,$tempFile);
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public function download($file, $destFile){
|
|||
|
$result = $this->dav->get($file,$destFile);
|
|||
|
return $result ? $destFile : false;
|
|||
|
}
|
|||
|
public function listPath($path,$simple=false){
|
|||
|
$data = $this->dav->propfind($path);
|
|||
|
if(!$data['status'] || !$data['data'] || !$data['data']['response']) return false;
|
|||
|
|
|||
|
$list = $data['data']['response'];
|
|||
|
$current = isset($list[0]) ? $list[0]:$list;
|
|||
|
$current = $this->_pathInfoParse($current);
|
|||
|
$result = array('fileList'=>array(),'folderList'=>array(),'current'=>$current);
|
|||
|
if(!isset($list[0])) return $result;
|
|||
|
|
|||
|
foreach($list as $index => $val){
|
|||
|
if($index == 0 ) continue;
|
|||
|
$item = $this->_pathInfoParse($val);
|
|||
|
$type = $item['type'] == 'file' ? 'fileList':'folderList';
|
|||
|
$result[$type][] = $item;
|
|||
|
|
|||
|
$key = trim($item['path'],'/');
|
|||
|
$this->infoCache[$key] = $item;
|
|||
|
}
|
|||
|
// 追加信息处理;extendFileList
|
|||
|
$extendData = _get($data,'data.extendFileList');
|
|||
|
$extendData = $extendData ? $extendData : _get($data,'header.X-extendFileList');
|
|||
|
$extendData = $extendData ? $extendData : _get($data,'header.X-EXTENDFILELIST');
|
|||
|
if($extendData){
|
|||
|
$arr = json_decode(base64_decode($extendData),true);
|
|||
|
$result = array_merge($result,$arr ? $arr:array());
|
|||
|
}
|
|||
|
if(is_array($result['pageInfo']) && $result['pageInfo']['pageNum'] < 100){
|
|||
|
unset($result['pageInfo']);
|
|||
|
}
|
|||
|
// pr($data,$result);exit;
|
|||
|
return $result;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public function fileOut($path, $download = false, $downFilename = false, $etag=''){
|
|||
|
if($this->fileOutKod($path)) return;
|
|||
|
$this->fileOutServer($path, $download, $downFilename, $etag);
|
|||
|
}
|
|||
|
public function fileOutServer($path, $download = false, $downFilename = false, $etag=''){
|
|||
|
parent::fileOut($path, $download, $downFilename, $etag);
|
|||
|
}
|
|||
|
public function fileOutImage($path,$width=250){
|
|||
|
if($this->fileOutKod($path)) return;
|
|||
|
parent::fileOutImage($path,$width);
|
|||
|
}
|
|||
|
public function fileOutImageServer($path,$width=250){
|
|||
|
parent::fileOutImage($path,$width);
|
|||
|
}
|
|||
|
private function fileOutKod($path){
|
|||
|
if(!$this->davServerKod) return false;
|
|||
|
if($this->isFileOutServer()) return false;
|
|||
|
$data = $this->info($path);
|
|||
|
if(!$data || !$data['fileOutLink']) return false;
|
|||
|
|
|||
|
$link = $data['fileOutLink'];
|
|||
|
$param = parse_url_query(this_url());
|
|||
|
$disableKey = array('accessToken','path');
|
|||
|
foreach($param as $key=>$val){
|
|||
|
if(!$val || in_array($key,$disableKey)) continue;
|
|||
|
$link .= '&'.$key.'='.$val;
|
|||
|
}
|
|||
|
$this->fileOutLink($link);
|
|||
|
}
|
|||
|
|
|||
|
// 缓存处理;
|
|||
|
public static $infoCache = array();
|
|||
|
public static $listCache = array();
|
|||
|
private function _pathInfo($path,$cacheInfo=false){
|
|||
|
if(!$this->pathCheck($path)) return false;
|
|||
|
$key = trim($path,'/');
|
|||
|
if($cacheInfo){self::$infoCache[$key] = $cacheInfo;return;}
|
|||
|
if(isset(self::$infoCache[$key])) return self::$infoCache[$key];
|
|||
|
|
|||
|
$data = $this->listPath($path);
|
|||
|
$pathInfo = $data ? $data['current']:false;
|
|||
|
if($pathInfo){self::$infoCache[$key] = $pathInfo;}
|
|||
|
return $pathInfo;
|
|||
|
}
|
|||
|
// 文件属性; name/path/type/size/createTime/modifyTime/
|
|||
|
private function _pathInfoParse($item){
|
|||
|
if(!$item || !isset($item['href'])){return array();}
|
|||
|
$path = $this->dav->uriToPath($item['href']);
|
|||
|
$info = array('name'=>get_path_this($path),'path'=>$path,'type'=>'folder');
|
|||
|
$prop = _get($item,'propstat.prop',array());
|
|||
|
if(is_array($item['propstat']) && is_array($item['propstat'][0])){
|
|||
|
$prop = $item['propstat'][0]['prop'];
|
|||
|
}
|
|||
|
if(isset($prop['getlastmodified'])){
|
|||
|
$info['modifyTime'] = strtotime($prop['getlastmodified']);
|
|||
|
}
|
|||
|
if(isset($prop['creationdate'])){
|
|||
|
$info['createTime'] = strtotime($prop['creationdate']);
|
|||
|
}
|
|||
|
if(isset($prop['getcontentlength'])){
|
|||
|
$info['size'] = $prop['getcontentlength'];
|
|||
|
}
|
|||
|
$info['type'] = $prop['resourcetype'] == '' ? 'file':'folder';
|
|||
|
$info['name'] = $info['name'] ? $info['name']:'/';
|
|||
|
$info['path'] = $this->getPathOuter($info['path']);
|
|||
|
$mimeType = $prop['getcontenttype'];
|
|||
|
if($mimeType){$info['type'] = ($mimeType == 'httpd/unix-directory') ? 'folder':'file';}
|
|||
|
|
|||
|
$info['_infoSimple'] = true;
|
|||
|
if($info['type'] == 'file'){
|
|||
|
$info['ext'] = get_path_ext($info['name']);
|
|||
|
}
|
|||
|
if(isset($prop['extendFileInfo'])){
|
|||
|
$arr = json_decode(base64_decode($prop['extendFileInfo']),true);
|
|||
|
$info = array_merge($info,$arr ? $arr:array());
|
|||
|
}
|
|||
|
return $info;
|
|||
|
}
|
|||
|
|
|||
|
public function fileInfo($path,$simple=false,$fileInfo = array()) {
|
|||
|
return $this->_pathInfo($path);
|
|||
|
}
|
|||
|
public function folderInfo($path,$simple=false,$itemInfo=array()) {
|
|||
|
return $this->_pathInfo($path);
|
|||
|
}
|
|||
|
public function size($path){
|
|||
|
$info = $this->_pathInfo($path);
|
|||
|
return $info ? $info['size'] : 0;
|
|||
|
}
|
|||
|
public function info($path){
|
|||
|
return $this->_pathInfo($path);
|
|||
|
}
|
|||
|
public function infoWithChildren($path){
|
|||
|
$this->dav->setHeader('X-DAV-ACTION','infoChildren');
|
|||
|
$info = $this->_pathInfo($path);
|
|||
|
if($this->davServerKod) return $info;
|
|||
|
if(is_array($info) && is_array($info['children'])) return $info;
|
|||
|
|
|||
|
return parent::infoWithChildren($path);
|
|||
|
}
|
|||
|
|
|||
|
public function exist($path){
|
|||
|
$info = $this->_pathInfo($path);
|
|||
|
return $info ? true : false;
|
|||
|
}
|
|||
|
public function isFile($path){
|
|||
|
$info = $this->_pathInfo($path);
|
|||
|
return ($info && $info['type'] == 'file') ? true : false;
|
|||
|
}
|
|||
|
public function isFolder($path){
|
|||
|
$info = $this->_pathInfo($path);
|
|||
|
return ($info && $info['type'] == 'folder') ? true : false;
|
|||
|
}
|
|||
|
private function pathCheck($path){
|
|||
|
$PATH_LENGTH_MAX = 4096;//路径最长限制;
|
|||
|
return strlen($path) >= $PATH_LENGTH_MAX ? false:true;
|
|||
|
}
|
|||
|
}
|